Skip to content

build(deps): take zod 4 across protocol, server and worker - #65

Merged
semics-tech merged 1 commit into
mainfrom
build/zod-4
Jul 31, 2026
Merged

build(deps): take zod 4 across protocol, server and worker#65
semics-tech merged 1 commit into
mainfrom
build/zod-4

Conversation

@semics-tech

Copy link
Copy Markdown
Owner

Closes #41. zod is a dependency of protocol, server and worker, 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.

-appRoleMap: z.record(z.enum(ROLES)),
+appRoleMap: z.record(z.string(), z.enum(ROLES)),

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 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. Beyond what the typechecker sees, it catches a restated default carrying the wrong value.

Verified

typecheck 7/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 high clean

🤖 Generated with Claude Code

https://claude.ai/code/session_01AyYg2j8FVkLjiaVcj5HCkj

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
@semics-tech
semics-tech merged commit 5473661 into main Jul 31, 2026
9 checks passed
@semics-tech
semics-tech deleted the build/zod-4 branch July 31, 2026 12:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant