import * as z from "zod/mini"; import { appEventCustomerOccurrenceSchema, appEventExternalIdSchema, } from "./app-event.js"; import { listInstanceFeedbackMineRequestSchema } from "./instance-feedback.js"; import { instanceFeedbackInputSchema } from "./internal/instance-feedback-input.js"; import { responseMetadataInputSchema } from "./response-metadata.js"; import { type ConfigureOptions, configureOptionsSchema, type InitOptions, initOptionsSchema, } from "./widget-config.js"; const flowHandleIdSchema = z.string().check(z.trim(), z.minLength(1)); const flowIdSchema = z.string().check(z.trim(), z.minLength(1)); /** Public command kinds accepted from host / SDK before core dispatch. */ const publicWidgetCommandKinds = [ "open", "prefetch", "prerender", "identify", "updateHostContext", "emitHostSignal", "setContainer", "setDefaultContainerPolicy", "configure", "init", "close", "reset", "track", "instanceFeedback.capture", "instanceFeedback.listMine", ] as const; export const publicWidgetCommandKindSchema = z.enum(publicWidgetCommandKinds); const isHTMLElementLike = (value: unknown): value is HTMLElement => { if (typeof value !== "object" || value === null) { return false; } if (typeof HTMLElement !== "undefined" && value instanceof HTMLElement) { return true; } if (!("nodeType" in value) || !("appendChild" in value)) { return false; } const nodeType = value.nodeType; const appendChild = value.appendChild; return nodeType === 1 && typeof appendChild === "function"; }; const htmlElementOrNullSchema = z.custom( (value: unknown) => value === null || isHTMLElementLike(value), { message: "Container must be null or an HTMLElement with nodeType=1", }, ); const identifyTraitsSchema = z.record(z.string(), z.unknown()); const identifyOptionsSchema = z.strictObject({ externalIds: z.optional( z.array(appEventExternalIdSchema).check(z.maxLength(20)), ), }); const identifyPayloadSchema = z.union([ z.strictObject({ kind: z.literal("identify"), traits: identifyTraitsSchema, options: z.optional(identifyOptionsSchema), }), z.strictObject({ kind: z.literal("identify"), userId: z.string().check(z.trim(), z.minLength(1)), traits: z.optional(identifyTraitsSchema), options: z.optional(identifyOptionsSchema), }), ]); const hostContextSchema = z.strictObject({ url: z.string().check(z.url()), env: z.optional(z.record(z.string(), z.string())), storage: z.optional(z.record(z.string(), z.string())), page: z.optional( z.strictObject({ path: z.optional(z.string()), referrer: z.optional(z.string()), search: z.optional(z.string()), }), ), }); // TODO(migration): After telemetry confirms no remaining `open.containerRequirement` in host command queues, remove the optional field and transform so unknown keys fail under `.strict()` again. // [from=legacy-open-container-requirement] [to=strict-public-open-only] [scope=slice] [priority=low] [impact=medium] [risk=low] const legacyOpenContainerRequirementSchema = z.enum(["any", "hostOnly"]); export const containerPolicySchema = z.discriminatedUnion("kind", [ z.strictObject({ kind: z.literal("floating"), }), z.strictObject({ kind: z.literal("hostContainer"), host: htmlElementOrNullSchema, sharing: z.enum(["shared", "perFlowRun"]), }), ]); const openPublicPayloadSchema = z.pipe( z.strictObject({ kind: z.literal("open"), flowId: flowIdSchema, flowHandleId: z.optional(flowHandleIdSchema), container: z.optional(htmlElementOrNullSchema), metadata: z.optional(responseMetadataInputSchema), /** Accepted for legacy browser SDK payloads; stripped before dispatch. */ containerRequirement: z.optional(legacyOpenContainerRequirementSchema), hideCloseButton: z.optional(z.boolean()), }), z.transform(({ containerRequirement: _legacy, ...rest }) => rest), ); export const publicCommandPayloadSchema = z.union([ z.discriminatedUnion("kind", [ openPublicPayloadSchema, z.strictObject({ kind: z.literal("prefetch"), flowId: flowIdSchema, flowHandleId: z.optional(flowHandleIdSchema), }), z.strictObject({ kind: z.literal("prerender"), flowId: flowIdSchema, flowHandleId: z.optional(flowHandleIdSchema), hideCloseButton: z.optional(z.boolean()), }), z.strictObject({ kind: z.literal("updateHostContext"), context: hostContextSchema, }), z.strictObject({ kind: z.literal("emitHostSignal"), name: z.string().check(z.trim(), z.minLength(1)), data: z.optional(z.unknown()), }), z.strictObject({ kind: z.literal("setContainer"), flowHandleId: flowHandleIdSchema, container: htmlElementOrNullSchema, }), z.strictObject({ kind: z.literal("setDefaultContainerPolicy"), policy: containerPolicySchema, }), z.strictObject({ kind: z.literal("configure"), opts: configureOptionsSchema, }), z.strictObject({ kind: z.literal("init"), opts: initOptionsSchema, }), z.strictObject({ kind: z.literal("close"), flowHandleId: z.optional(flowHandleIdSchema), }), z.strictObject({ kind: z.literal("reset"), }), z.strictObject({ kind: z.literal("instanceFeedback.capture"), payload: instanceFeedbackInputSchema, }), z.strictObject({ kind: z.literal("instanceFeedback.listMine"), payload: listInstanceFeedbackMineRequestSchema, }), z.extend(appEventCustomerOccurrenceSchema, { kind: z.literal("track"), }), ]), identifyPayloadSchema, ]); export type PublicCommandPayload = z.output; export type ContainerPolicy = z.output; export function parseInitOptions(input: unknown): InitOptions { return initOptionsSchema.parse(input); } export function parseConfigureOptions(input: unknown): ConfigureOptions { return configureOptionsSchema.parse(input); } export function parsePublicCommand(input: unknown): PublicCommandPayload { return publicCommandPayloadSchema.parse(input); }