build(deps): take zod 4 across protocol, server and worker - #65
Merged
Conversation
Closes dependabot #41. zod is a dependency of all three packages and they share schemas, so it moves in one step or not at all. Two zod 4 changes needed edits. `z.record()` now reads a lone argument as the *key* schema rather than the value schema. This is the one that mattered. Under zod 3 `z.record(z.enum(ROLES))` meant "Entra app role name -> dashboard role"; read the zod 4 way the same line means "dashboard role -> unknown", so the mapping inverts and the values stop being checked. It is the Entra sign-in role map, so it decides what an authenticated user may do. TypeScript caught it, along with the two other single-argument records; all three now name both halves, with a note saying why. `.default()` no longer parses its argument — it is returned as-is, so it must be the full output type. Three `.default({})` sites in the worker config stopped compiling and are `.prefault({})` now, which is the zod 4 spelling of what zod 3 did. The two that still compiled, `.default({ mode: 'token' })` and `.default({ enabled: true })`, moved too, for consistency and so the defaults are stated once beside the fields rather than restated in a second place that can drift. To be clear about what that is and is not worth: TypeScript does reject a `.default()` missing a field, so a stale one is a build error, not a silent drop. I checked rather than assumed — an earlier draft of this message claimed otherwise. The choice is about not writing the numbers twice. Adds packages/worker/test/config-defaults.test.ts, which had no equivalent. It asserts what a minimal worker.yaml resolves to, which is the shape an enrolled worker actually runs with — poll intervals, reconnect backoff, and maxCapability defaulting to readOnly rather than anything higher. The gap it covers beyond the typechecker is a restated default with the wrong value in it. Verified: typecheck 7/7, lint, 361 unit, 18 integration, worker bundle builds and self-tests standalone, SEA executable builds and self-tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AyYg2j8FVkLjiaVcj5HCkj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #41. zod is a dependency of
protocol,serverandworker, and they share schemas, so it moves in one step or not at all.The one that mattered:
z.record()zod 4 reads a lone argument as the key schema rather than the value schema.
Under zod 3 that line meant "Entra app role name → dashboard role". Read the zod 4 way, the same characters mean "dashboard role → unknown" — the mapping inverts and the values stop being validated. This is the Entra sign-in role map, so it decides what an authenticated user is allowed to do.
TypeScript caught it (
Record<"Viewer" | "Operator" | "Editor" | "Admin", unknown>), along with the two other single-argument records in the notification sender and channel store. All three now name both halves, with a comment saying why.The mechanical one:
.default().default()no longer parses its argument — it is returned as-is, so it must now be the full output type. Three.default({})sites in the worker config stopped compiling and are.prefault({})now, which is the zod 4 spelling of what zod 3 did. The two that still compiled moved with them, so the defaults are stated once beside the fields rather than restated somewhere that can drift.Worth being precise about: TypeScript does reject a
.default()that is missing a field, so a stale one is a build error, not a silent drop. I verified that with a probe rather than assuming it — my first draft of this claimed the opposite.New test
packages/worker/test/config-defaults.test.ts— the worker config had no test at all. It asserts what a minimalworker.yamlresolves to, which is the shape an enrolled worker actually runs with: poll intervals, reconnect backoff, andmaxCapabilitydefaulting toreadOnlyrather than anything higher. Beyond what the typechecker sees, it catches a restated default carrying the wrong value.Verified
typecheck7/7 ·lint· 361 unit · 18 integration · worker bundle builds and self-tests with nothing beside it · SEA executable builds and self-tests ·pnpm audit --audit-level highclean🤖 Generated with Claude Code
https://claude.ai/code/session_01AyYg2j8FVkLjiaVcj5HCkj