/** * Message-layer zod schemas: the four wire full forms + error body + * carrier receipt. The payload slot is unknown in the full-form schemas — business payloads * get a second parse dispatched by method (two-level parse discipline). * Brand cast point: rpcIdSchema, and only there. */ import { z } from 'zod' import type { z as zCore } from 'zod' type ZodIssue = zCore.core.$ZodIssue import type { ClientRequest, ClientResponse, RpcError, RpcId, RpcReceipt, ServerRequest, ServerResponse } from './rpc.ts' /** * Wire widening of a contract type: widens every property (deeply) to `original | undefined`. * The repo enables exactOptionalPropertyTypes while zod `.optional()` outputs `T | undefined`, * so `satisfies z.ZodType` is unusable across the board; anchoring is always * written `satisfies z.ZodType>` — the widening only adds undefined, so * missing fields / wrong types still fail to compile. On the JSON wire, "absent" and * "value undefined" serialize identically, so the widening loses no validation semantics. */ export type Wire = T extends readonly (infer E)[] ? Wire[] : T extends object ? { [K in keyof T]: Wire | undefined } : T /** * RpcId: one brand cast after shape validation (the only cast point in this * file). No min-length: the id is an opaque echo token, and rejecting shapes * here would only turn a correlatable error report into a client-side parse * failure (the handler substitutes a sentinel when a request's id is unreadable). */ export const rpcIdSchema = z.string() as unknown as z.ZodType /** Error body: discriminated by code, per-branch details aligned to RpcErrorDetailsMap; details is required. */ export const rpcErrorSchema: z.ZodType = z.discriminatedUnion('code', [ z.object({ code: z.literal('bad-request'), message: z.string(), details: z.object({ issues: z.array(z.custom()) }) }), z.object({ code: z.literal('cancelled'), message: z.string(), details: z.object({}) }), z.object({ code: z.literal('session-not-found'), message: z.string(), details: z.object({ sessionId: z.string() }) }), z.object({ code: z.literal('model-unavailable'), message: z.string(), details: z.object({ provider: z.string(), model: z.string() }) }), z.object({ code: z.literal('session-conflict'), message: z.string(), details: z.object({ sessionId: z.string(), requestedCwd: z.string(), existingCwd: z.string().optional() }) }), z.object({ code: z.literal('workspace-attach-failed'), message: z.string(), details: z.object({ sessionId: z.string(), workspaceId: z.string() }) }), z.object({ code: z.literal('workspace-not-found'), message: z.string(), details: z.object({ workspaceId: z.string() }) }), z.object({ code: z.literal('workspace-invalid-path'), message: z.string(), details: z.object({ path: z.string() }) }), z.object({ code: z.literal('workspace-name-conflict'), message: z.string(), details: z.object({ name: z.string() }) }), z.object({ code: z.literal('workspace-move-invalid'), message: z.string(), details: z.object({ workspaceId: z.string(), sessionId: z.string(), beforeSessionId: z.string().optional() }) }), z.object({ code: z.literal('directory-unreadable'), message: z.string(), details: z.object({ path: z.string() }) }), z.object({ code: z.literal('directory-exists'), message: z.string(), details: z.object({ path: z.string() }) }), z.object({ code: z.literal('directory-create-failed'), message: z.string(), details: z.object({ path: z.string() }) }), z.object({ code: z.literal('directory-picker-unavailable'), message: z.string(), details: z.object({ capability: z.string() }) }), z.object({ code: z.literal('agent-busy'), message: z.string(), details: z.object({ reason: z.string() }) }), z.object({ code: z.literal('command-error'), message: z.string(), details: z.object({}) }), z.object({ code: z.literal('unknown-command'), message: z.string(), details: z.object({}) }), z.object({ code: z.literal('internal'), message: z.string(), details: z.object({}) }), ]) as unknown as z.ZodType /** * Business success/failure result schema (generic, reusable). * @param value - Schema for the business value. * @returns Schema for RpcResult. */ export function rpcResultSchema(value: z.ZodType): z.ZodUnion { return z.union([ z.object({ ok: z.literal(true), value }), z.object({ ok: z.literal(false), error: rpcErrorSchema }), ]) } // ---- The four wire full-form schemas (payload/result.value slots stay wide — business layer does the second parse) ---- /** ClientRequest full form (payload stays wide — the business layer runs the second parse). */ export const clientRequestSchema = z.object({ type: z.literal('client-request'), rpcId: rpcIdSchema, method: z.string(), payload: z.unknown(), }) as unknown as z.ZodType /** ServerResponse full form (result.value stays wide). */ export const serverResponseSchema = z.object({ type: z.literal('server-response'), rpcId: rpcIdSchema, result: rpcResultSchema(z.unknown()), }) as unknown as z.ZodType /** ServerRequest full form (payload stays wide). */ export const serverRequestSchema = z.object({ type: z.literal('server-request'), rpcId: rpcIdSchema, method: z.string(), payload: z.unknown(), }) as unknown as z.ZodType /** ClientResponse full form (result.value stays wide). */ export const clientResponseSchema = z.object({ type: z.literal('client-response'), rpcId: rpcIdSchema, result: rpcResultSchema(z.unknown()), }) as unknown as z.ZodType /** Wire full-form union (discriminated by type). */ export const rpcMessageSchema = z.discriminatedUnion('type', [ clientRequestSchema as unknown as z.ZodObject, serverResponseSchema as unknown as z.ZodObject, serverRequestSchema as unknown as z.ZodObject, clientResponseSchema as unknown as z.ZodObject, ]) /** Carrier receipt schema. */ export const rpcReceiptSchema = z.union([ z.object({ accepted: z.literal(true) }), z.object({ accepted: z.literal(false), reason: z.union([z.literal('not-pending'), z.literal('bad-response')]) }), ]) satisfies z.ZodType>