import { OBSERVER_STATUSES, type ObservedThread, type ObserverOutput, type ObserverStatus } from "./types.ts"; const MAX_THREADS = 64; const MAX_REQUEST_LENGTH = 2_000; const STATUS_SET = new Set(OBSERVER_STATUSES); const threadProperties = { canonicalRequest: { type: "string" }, status: { type: "string", enum: [...OBSERVER_STATUSES] }, } as const; export const OBSERVER_JSON_SCHEMA = { type: "object", additionalProperties: false, properties: { existing: { type: "array", items: { type: "object", additionalProperties: false, properties: { id: { type: "string" }, ...threadProperties }, required: ["id", "canonicalRequest", "status"], }, }, additions: { type: "array", items: { type: "object", additionalProperties: false, properties: threadProperties, required: ["canonicalRequest", "status"], }, }, }, required: ["existing", "additions"], } as const; const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null && !Array.isArray(value); function parseThread(value: unknown, requireId: boolean): ObservedThread | undefined { if (!isRecord(value)) return undefined; const keys = Object.keys(value); const allowed = requireId ? new Set(["id", "canonicalRequest", "status"]) : new Set(["canonicalRequest", "status"]); if (keys.some((key) => !allowed.has(key))) return undefined; if ( typeof value.canonicalRequest !== "string" || value.canonicalRequest.trim().length === 0 || value.canonicalRequest.length > MAX_REQUEST_LENGTH || typeof value.status !== "string" || !STATUS_SET.has(value.status) || (requireId && typeof value.id !== "string") ) { return undefined; } return { ...(requireId ? { id: value.id as string } : {}), canonicalRequest: value.canonicalRequest.trim(), status: value.status as ObserverStatus, }; } export function parseObserverOutput( raw: string, knownThreads: ReadonlyMap, allowUserChanges: boolean, ): ObserverOutput | undefined { let parsed: unknown; try { parsed = JSON.parse(raw); } catch { return undefined; } if (!isRecord(parsed) || !Array.isArray(parsed.existing) || !Array.isArray(parsed.additions)) return undefined; if (parsed.existing.length > MAX_THREADS || parsed.additions.length > MAX_THREADS) return undefined; if (!allowUserChanges && parsed.additions.length > 0) return undefined; const existing = parsed.existing.map((value) => parseThread(value, true)); const additions = parsed.additions.map((value) => parseThread(value, false)); if (existing.some((thread) => thread === undefined) || additions.some((thread) => thread === undefined)) { return undefined; } const ids = existing.map((thread) => thread!.id!); if (new Set(ids).size !== ids.length || ids.some((id) => !knownThreads.has(id))) return undefined; if (ids.length !== knownThreads.size || [...knownThreads.keys()].some((id) => !ids.includes(id))) return undefined; if ( !allowUserChanges && (existing as ObservedThread[]).some( (thread) => thread.canonicalRequest !== knownThreads.get(thread.id!), ) ) { return undefined; } return { existing: existing as ObservedThread[], additions: additions as ObservedThread[], }; }