import z from "zod"; /** * Wire envelope for every WebSocket frame that flows between the host and a * plugin. Unknown top-level keys are rejected so clients cannot smuggle * fields past the host's validation gate. * * The shape is intentionally minimal: * - `id` correlates request/response/ack frames. * - `type` is the message discriminator (plugin-defined namespace). * - `topic` names the subscription (optional for non-subscription frames). * - `payload` is the opaque body — plugins narrow with their own Zod schema. * - `ts` is an optional ISO-8601 producer timestamp. */ export const wsEnvelopeSchema = z .object({ id: z.string().min(1), type: z.string().min(1), topic: z.string().min(1).optional(), payload: z.unknown().optional(), ts: z.string().min(1).optional(), }) .strict(); export type WsEnvelope = z.infer; /** * Type-narrowing helper: returns `true` (and narrows `value` to * `WsEnvelope`) when the candidate parses successfully. */ export function isWsEnvelope(value: unknown): value is WsEnvelope { return wsEnvelopeSchema.safeParse(value).success; } /** * Strict parser that throws the underlying `ZodError` on failure. Callers * that prefer a typed thrown error should wrap with `WsError`. */ export function parseWsEnvelope(value: unknown): WsEnvelope { return wsEnvelopeSchema.parse(value); }