// Strict P1 JSON domain/parser, shared algorithm with router codexLinkValidation. // Browser-safe; never apply to the opaque native/provider JSON body. type Data = Record export class CodexLinkValidationError extends Error { readonly code: string constructor(code: string) { super(code) this.code = code this.name = 'CodexLinkValidationError' } } function requireValue(condition: unknown, code: string): asserts condition { if (!condition) { throw new CodexLinkValidationError(code) } } function object(value: unknown): Data { requireValue(value !== null && typeof value === 'object' && !Array.isArray(value), 'schema_rejected') const prototype = Object.getPrototypeOf(value) requireValue(prototype === Object.prototype || prototype === null, 'schema_rejected') return value as Data } function integer(value: unknown, positive = false): asserts value is number { requireValue(Number.isSafeInteger(value) && (value as number) >= (positive ? 1 : 0), 'schema_rejected') } function id(value: unknown): asserts value is string { requireValue(typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/.test(value), 'schema_rejected') } function exactKeys(value: Data, keys: string[]) { requireValue( Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key)), 'schema_rejected' ) } function unicode(value: string) { for (let i = 0; i < value.length; i++) { const code = value.charCodeAt(i) if (code >= 0xd800 && code <= 0xdbff) { const low = value.charCodeAt(++i) requireValue(low >= 0xdc00 && low <= 0xdfff, 'invalid_unicode') } else { requireValue(code < 0xdc00 || code > 0xdfff, 'invalid_unicode') } } } /** P1 canonical domain: ASCII object keys, safe integers, exact Unicode text. */ export function canonicalJson(value: unknown): string { const ancestors = new Set() function encode(item: unknown, depth: number): string { requireValue(depth <= 256, 'invalid_json') if (item === null || typeof item === 'boolean') { return JSON.stringify(item) } if (typeof item === 'string') { unicode(item) return JSON.stringify(item) } if (typeof item === 'number') { requireValue(Number.isInteger(item), 'floats_or_non_json_type') requireValue(Number.isSafeInteger(item), 'unsafe_integer') return JSON.stringify(item) } requireValue(typeof item === 'object' && item !== null, 'floats_or_non_json_type') requireValue(!ancestors.has(item), 'invalid_json') ancestors.add(item) let result: string if (Array.isArray(item)) { result = '[' + item.map(child => encode(child, depth + 1)).join(',') + ']' } else { const data = object(item) const keys = Object.keys(data).sort() requireValue( keys.every(key => [...key].every(char => char.charCodeAt(0) <= 127)), 'non_ascii_key' ) result = '{' + keys.map(key => JSON.stringify(key) + ':' + encode(data[key], depth + 1)).join(',') + '}' } ancestors.delete(item) return result } return encode(value, 0) } /** A bounded parser that detects duplicates before JSON.parse can erase them. */ export function parseStrictJson(raw: string | Uint8Array, maxBytes: number): unknown { integer(maxBytes, true) const rawBytes = typeof raw === 'string' ? new TextEncoder().encode(raw).byteLength : raw.byteLength requireValue(rawBytes <= maxBytes, 'transport_bytes_exceeded') let text: string try { text = typeof raw === 'string' ? raw : new TextDecoder('utf-8', { fatal: true }).decode(raw) } catch { throw new CodexLinkValidationError('invalid_json') } unicode(text) let position = 0 function whitespace() { while (/[ \t\r\n]/.test(text[position] ?? '') && position < text.length) { position++ } } function quoted(): string { const start = position++ while (position < text.length) { const next = text[position++] if (next === '\\') { position++ continue } if (next === '"') { try { const value = JSON.parse(text.slice(start, position)) unicode(value) return value } catch (error) { if (error instanceof CodexLinkValidationError) { throw error } throw new CodexLinkValidationError('invalid_json') } } } throw new CodexLinkValidationError('invalid_json') } function read(depth: number): unknown { requireValue(depth <= 256, 'invalid_json') whitespace() const token = text[position] if (token === '"') { return quoted() } if (token === '{') { position++ whitespace() const value: Data = Object.create(null) if (text[position] === '}') { position++ return value } while (position < text.length) { requireValue(text[position] === '"', 'invalid_json') const key = quoted() requireValue( [...key].every(char => char.charCodeAt(0) <= 127), 'non_ascii_key' ) requireValue(!Object.hasOwn(value, key), 'duplicate_json_key') whitespace() requireValue(text[position++] === ':', 'invalid_json') value[key] = read(depth + 1) whitespace() const separator = text[position++] if (separator === '}') { return value } requireValue(separator === ',', 'invalid_json') whitespace() } } else if (token === '[') { position++ whitespace() const value: unknown[] = [] if (text[position] === ']') { position++ return value } while (position < text.length) { value.push(read(depth + 1)) whitespace() const separator = text[position++] if (separator === ']') { return value } requireValue(separator === ',', 'invalid_json') } } else { for (const [word, value] of [ ['true', true], ['false', false], ['null', null] ] as const) { if (text.startsWith(word, position)) { position += word.length return value } } if (/^(?:NaN|[-+]?Infinity)/.test(text.slice(position))) { throw new CodexLinkValidationError('floats_or_non_json_type') } const match = /^-?(?:0|[1-9]\d*)/.exec(text.slice(position)) if (match) { position += match[0].length requireValue(!/[.eE]/.test(text[position] ?? ''), 'floats_or_non_json_type') const number = Number(match[0]) requireValue(Number.isSafeInteger(number), 'unsafe_integer') return number } } throw new CodexLinkValidationError('invalid_json') } const value = read(0) whitespace() requireValue(position === text.length, 'invalid_json') canonicalJson(value) return value }