import * as z from "zod/mini"; import { type Generation, generationSchema } from "./v3-generation.js"; const canonicalIdStringSchema = z.string().check(z.trim(), z.minLength(1)); const routedMessageEnvelopeSchema = z.looseObject({ t: z.string().check(z.startsWith("getuserfeedback:")), gen: generationSchema, viewId: z.optional(canonicalIdStringSchema), }); type RoutedMessageEnvelope = z.output; const VIEW_HOST_CONTROL_MESSAGE_TYPES = [ "getuserfeedback:action:viewConnect", "getuserfeedback:event:viewSize", "getuserfeedback:action:requestCloseView", ] as const; const VIEW_TO_CORE_RELAY_MESSAGE_TYPES = [ "getuserfeedback:event:message", "getuserfeedback:event:viewStateSnapshot", "getuserfeedback:rpc:request", "getuserfeedback:rpc:response", ] as const; const CORE_TO_VIEW_RELAY_MESSAGE_TYPES = [ "getuserfeedback:event:message", "getuserfeedback:event:colorSchemeUpdate", "getuserfeedback:event:restoreSessionSnapshot", "getuserfeedback:event:viewportSize", "getuserfeedback:rpc:request", "getuserfeedback:rpc:response", ] as const; type ViewHostControlMessageType = (typeof VIEW_HOST_CONTROL_MESSAGE_TYPES)[number]; type ViewToCoreRelayMessageType = (typeof VIEW_TO_CORE_RELAY_MESSAGE_TYPES)[number]; type CoreToViewRelayMessageType = (typeof CORE_TO_VIEW_RELAY_MESSAGE_TYPES)[number]; type ViewHostControlMessage = RoutedMessageEnvelope & { t: ViewHostControlMessageType; }; type ViewToCoreRelayMessage = RoutedMessageEnvelope & { t: ViewToCoreRelayMessageType; }; type CoreToViewRelayMessage = RoutedMessageEnvelope & { t: CoreToViewRelayMessageType; viewId: string; }; type ViewChannelOptions = { generation: Generation | number; viewId: string; }; type CoreChannelOptions = { generation: Generation | number; }; function isMessageType( message: RoutedMessageEnvelope, types: readonly T[], ): message is RoutedMessageEnvelope & { t: T } { return (types as readonly string[]).includes(message.t); } function parseEnvelope( raw: unknown, expectedGeneration: Generation | number, ): RoutedMessageEnvelope | null { const parsed = routedMessageEnvelopeSchema.safeParse(raw); if (!parsed.success || parsed.data.gen !== expectedGeneration) { return null; } return parsed.data; } function bindViewIdentity( message: RoutedMessageEnvelope, viewId: string, ): (RoutedMessageEnvelope & { viewId: string }) | null { if (message.viewId !== undefined && message.viewId !== viewId) { return null; } return { ...message, viewId }; } /** * Classifies a host-controlled envelope and binds the trusted channel * identity. The endpoint validates the message's semantic payload. */ export function bindViewHostControlEnvelope( raw: T, options: ViewChannelOptions, ): (T & { viewId: string }) | null; export function bindViewHostControlEnvelope( raw: unknown, options: ViewChannelOptions, ): ViewHostControlMessage | null; export function bindViewHostControlEnvelope( raw: T, options: ViewChannelOptions, ): (T & { viewId: string }) | null { const message = parseEnvelope(raw, options.generation); if (!message || !isMessageType(message, VIEW_HOST_CONTROL_MESSAGE_TYPES)) { return null; } const bound = bindViewIdentity(message, options.viewId); return bound ? ({ ...raw, ...bound } as T & { viewId: string }) : null; } /** * Classifies a View-to-Core relay envelope and binds the channel-owned viewId. * A supplied viewId is accepted only when it agrees with the channel. The * endpoint validates the message's semantic payload. */ export function bindViewToCoreRelayEnvelope( raw: T, options: ViewChannelOptions, ): (T & { viewId: string }) | null; export function bindViewToCoreRelayEnvelope( raw: unknown, options: ViewChannelOptions, ): ViewToCoreRelayMessage | null; export function bindViewToCoreRelayEnvelope( raw: T, options: ViewChannelOptions, ): (T & { viewId: string }) | null { const message = parseEnvelope(raw, options.generation); if (!message || !isMessageType(message, VIEW_TO_CORE_RELAY_MESSAGE_TYPES)) { return null; } const bound = bindViewIdentity(message, options.viewId); return bound ? ({ ...raw, ...bound } as T & { viewId: string }) : null; } /** * Classifies a targeted Core-to-View relay envelope and returns its exact * target. Core must provide viewId explicitly; hosts never infer or broadcast * a target here. The endpoint validates the message's semantic payload. */ export function parseCoreToViewRelayEnvelope( raw: unknown, options: CoreChannelOptions, ): { viewId: string; message: CoreToViewRelayMessage } | null { const message = parseEnvelope(raw, options.generation); if ( !message || !isMessageType(message, CORE_TO_VIEW_RELAY_MESSAGE_TYPES) || message.viewId === undefined ) { return null; } return { viewId: message.viewId, message: { ...message, viewId: message.viewId, t: message.t }, }; }