Files
suaveplan/packages/api-stack/api/api-ws/src/envelope/envelope.ts
2026-05-27 23:29:53 +01:00

42 lines
1.4 KiB
TypeScript

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<typeof wsEnvelopeSchema>;
/**
* 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);
}