# schemas/messages _(no submodules)_ ## Purpose This module declares the Zod boundary schemas for `@suaveplan/broadcast-channel`. `ChannelMessageSchema` validates every inbound frame produced by the underlying `ChannelLike` transport before it is allowed into the per-type handler registry. `ChannelConfigSchema` validates the construction-time config object so consumers see a meaningful Zod issue list instead of an opaque runtime failure when a field is missing or non-numeric. Boundary validation is the suaveplan-mandated fix for the enterprise gap "No Zod validation on incoming `ChannelMessage` — a rogue tab could send garbage." A peer that posts a frame missing `id` or with a negative `timestamp` no longer reaches user code; the channel drops the frame, increments `channel.message.dropped`, and warns through the injected `LoggerLike`. ## Features - Two schemas: `ChannelMessageSchema`, `ChannelConfigSchema`. - Default-import Zod (`import z from "zod"`) to satisfy the suaveplan Bun-safe rule. - `payload` is intentionally `z.unknown()` so callers can layer their own per-type validation downstream. - Stable required-field set: `{ type, payload, id, senderId, timestamp }`. - Inferred input types via `z.infer`. ## Usage: Basic ```ts import { ChannelMessageSchema } from "@suaveplan/broadcast-channel"; const parsed = ChannelMessageSchema.safeParse(incoming); if (!parsed.success) { return; // drop } ``` ## Usage: Advanced ```ts import z from "zod"; import { ChannelMessageSchema } from "@suaveplan/broadcast-channel"; // Layer a per-type payload validator over the boundary schema. const UserSignedInPayload = z.object({ userId: z.string().min(1) }); const incoming: unknown = receiveOffWire(); const env = ChannelMessageSchema.safeParse(incoming); if (!env.success) return; if (env.data.type === "user:signed-in") { const payload = UserSignedInPayload.safeParse(env.data.payload); if (!payload.success) return; // payload.data.userId is typed and validated } ``` ## API Reference - `const ChannelMessageSchema` — `z.object({ type: z.string().min(1), payload: z.unknown(), id: z.string().min(1), senderId: z.string().min(1), timestamp: z.number().int().nonnegative() })`. - `const ChannelConfigSchema` — `z.object({ name: z.string().min(1), heartbeatInterval: z.number().int().positive(), tabTimeout: z.number().int().positive(), maxMessageBytes: z.number().int().positive() })`. - `type ChannelMessageInput = z.infer`. - `type ChannelConfigInput = z.infer`. ## Implementation Notes `payload` is left as `z.unknown()` because the channel is type-agnostic on the wire: per-type validation belongs in the application layer, not in the transport boundary. Forcing a discriminated union here would push every caller into either casting or extending the schema, and would force a major version bump every time an application added a new message type — exactly the opposite of the loose-coupling story the channel is meant to provide. The `.min(1)` constraints on `type` / `id` / `senderId` catch the realistic failure mode (empty strings produced by a malformed encoder) without the cost of regex matching. `timestamp` uses `.int().nonnegative()` so frames produced by clocks at zero (a fresh `FakeClock`) still validate while frames with garbage negative values are rejected.