Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Refactor: typed message router (webext-messenger) + zod validation at the page→worker boundary#241

Merged
twschiller merged 1 commit into
mainfrom
refactor/typed-message-router
Jun 10, 2026
Merged

Refactor: typed message router (webext-messenger) + zod validation at the page→worker boundary#241
twschiller merged 1 commit into
mainfrom
refactor/typed-message-router

Conversation

@twschiller

Copy link
Copy Markdown
Contributor

What

Replaces background.ts's hand-rolled ~190-line chrome.runtime.onMessage switch (12 message types, ~7 blind as unknown as casts, a per-handler guard each) with a typed webext-messenger router, and adds uniform zod validation of every content→worker payload.

This is the implementation of the "untyped message router" review note — and crucially, it closes the security half that note flagged, not just the typing half.

Why

Two separable problems were bundled in that note:

  1. Untyped routing / no exhaustiveness — a renamed message type failed silently at runtime. Now every cross-context call is a named method on a shared MessengerMethods contract; a rename/mistype is a compile error. The blind casts are gone.
  2. ~50% payload validationrule-detection was trusted wholesale, recording attacker-influenceable host/url/difficulty straight into the popup-facing map. Now lib/message-schemas.ts decodes every content-script payload through a zod schema before it touches the popup maps or IndexedDB.

webext-messenger gives (1); it explicitly does not give (2) — its types are erased at runtime — so zod is applied where each handler is registered. The two compose: the library handles transport + compile-time types + fire-and-forget; zod handles the runtime trust boundary.

Design

  • zod stays out of the content & popup bundles (verified against dist/): the shared messenger.ts contract is dependency-free; validation lives in the background-only message-schemas.ts. The trust boundary is the page→worker hop, so the worker is the only context that validates.
  • Fire-and-forget paths (recordDetection, reportRuleCounts, reportDebugTraceEvent, requestPageWorldInject) use getNotifier — no response, no retries, swallows errors — matching the old sendMessage(...).catch(() => {}) posture.
  • Request/response paths (getTabRuleCounts, getTabPause, getTabUrl, getTabDebugTrace, openOptions) use getMethod; callers already fall back to an empty state on throw.
  • The tab-pause push broadcasts via frameId: "allFrames", matching the prior all-frames tabs.sendMessage.
  • The old hand-rolled rule-count sanitization (drop unknown ids, floor, drop non-positive) is preserved exactly inside ruleCountsSchema.

Cleanups carried along

  • Dropped the dead get-tab-detections handler (no sender anywhere in the tree) and the now-obsolete { type: ... } envelope/request interfaces from detection-messages.ts (now purely the payload/data contract).
  • The three inject-* messages collapse into one validated requestPageWorldInject(kind) over a zod enum.

Testing

  • New message-schemas.test.ts pins the validation boundary: happy path + attacker-shaped-payload rejection for each message (unknown kind, out-of-range difficulty, missing fields, non-object counts, …).
  • Rule/recorder tests now mock lib/messenger and assert the semantic call (recordDetection({...}), requestPageWorldInject("webdriver-probe")) rather than a wire envelope.
  • webext-messenger is ESM-only, so it gets a test mock via moduleNameMapper — same treatment as webext-storage/abort-utils. messenger.ts is coverage-excluded as transport glue (same posture as effective-enforcement.ts).
  • ✅ 2042 tests · typecheck · biome+eslint · knip · build (+ background-purity check) all green.

🤖 Generated with Claude Code

…at the page→worker boundary

Replaces the hand-rolled 190-line `chrome.runtime.onMessage` switch in
background.ts (12 message types, ~7 blind `as unknown as` casts, a
per-handler guard each) with a typed `webext-messenger` router and uniform
zod validation of every content→worker payload.

Addresses both halves of the review note:

- Untyped routing → every cross-context call is now a named method on a
  shared `MessengerMethods` contract (`lib/messenger.ts`). A renamed or
  mistyped message is a compile error, not a silent runtime no-op. The
  blind casts are gone.

- ~50% payload validation → `lib/message-schemas.ts` decodes every
  content-script payload (detection host/url/difficulty, rule counts,
  debug-trace entries, page-world inject kind) through a zod schema before
  it touches the popup-facing maps or IndexedDB. `recordDetection`'s
  attacker-influenceable payload is now validated, not trusted wholesale.
  The old hand-rolled rule-count sanitization is preserved exactly (drop
  unknown ids, floor, drop non-positive) inside `ruleCountsSchema`.

Design notes:
- zod stays OUT of the content and popup bundles (verified): the shared
  `messenger.ts` contract is dependency-free; validation lives in the
  background-only `message-schemas.ts`. The trust boundary is the
  page→worker hop, so the worker is the only context that validates.
- Fire-and-forget paths use `getNotifier` (no response, no retries,
  swallows errors) — same posture as the old `sendMessage(...).catch()`.
- The tab-pause push broadcasts via `frameId: "allFrames"`, matching the
  prior all-frames `tabs.sendMessage`.
- Dropped the dead `get-tab-detections` handler (no sender anywhere) and
  the now-obsolete `{ type: ... }` envelope interfaces.
- The three `inject-*` messages collapse into one validated
  `requestPageWorldInject(kind)` over a zod enum.

Testing:
- New `message-schemas.test.ts` pins the validation boundary (happy path +
  attacker-shaped-payload rejection for each message).
- Rule/recorder tests now mock `lib/messenger` and assert the semantic call
  rather than a wire envelope.
- `webext-messenger` (ESM-only) gets a test mock via moduleNameMapper, same
  as webext-storage/abort-utils; `messenger.ts` is coverage-excluded as
  transport glue, like effective-enforcement.ts.
- 2042 tests pass; typecheck, biome+eslint, knip, build (+ background
  purity) all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agent-browser-shield-demo-site Ready Ready Preview, Comment Jun 10, 2026 12:11am

Request Review

@twschiller twschiller merged commit 4194999 into main Jun 10, 2026
7 checks passed
@twschiller twschiller deleted the refactor/typed-message-router branch June 10, 2026 00:18
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