UNPKG

noa-receipt

Version:

NOA Agent Action Receipt — open, offline-verifiable provenance for AI-agent actions. The governance/receipt organ only; the NOA brain is separate and proprietary.

235 lines (234 loc) • 7.21 kB
const SAFE_JSON_ERRORS = new WeakSet(); import { weakSetHas, weakSetAdd, setHas, setAdd, strSlice, arrayPush, objectCreateNull, objectDefineProperty, strCharCodeAt, strIsWellFormed, strFromCharCode, strStartsWithAt, parseIntRadix, toNumber, isSafeInteger, newSet, } from "./intrinsics.js"; import { inertArray } from "./inert.js"; import { isHex4 } from "./scan.js"; export class SafeJsonError extends Error { pos; constructor(message, pos) { super(`${message} (at position ${pos})`); this.pos = pos; this.name = "SafeJsonError"; weakSetAdd(SAFE_JSON_ERRORS, this); } } export function isSafeJsonError(value) { return weakSetHas(SAFE_JSON_ERRORS, value); } function isForbiddenKey(key) { return key === "__proto__" || key === "prototype" || key === "constructor"; } export function safeParse(text, opts = {}) { const maxDepth = opts.maxDepth ?? 64; const maxLength = opts.maxLength ?? 16 * 1024 * 1024; if (text.length > maxLength) { throw new SafeJsonError("input exceeds maximum length", text.length); } let i = 0; const n = text.length; function err(msg) { throw new SafeJsonError(msg, i); } function skipWs() { while (i < n) { const c = strCharCodeAt(text, i); if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d) i++; else break; } } function parseValue(depth) { if (depth > maxDepth) err("maximum nesting depth exceeded"); skipWs(); if (i >= n) err("unexpected end of input"); const c = text[i]; switch (c) { case "{": return parseObject(depth); case "[": return parseArray(depth); case '"': return parseString(); case "t": case "f": return parseBool(); case "n": return parseNull(); default: if (c === "-" || (c >= "0" && c <= "9")) return parseNumber(); err(`unexpected character '${c}'`); } } function parseObject(depth) { i++; const obj = objectCreateNull(); const seen = newSet(); skipWs(); if (text[i] === "}") { i++; return obj; } for (;;) { skipWs(); if (text[i] !== '"') err("expected object key string"); const key = parseString(); if (isForbiddenKey(key)) err(`forbidden object key '${key}'`); if (setHas(seen, key)) err(`duplicate object key '${key}'`); setAdd(seen, key); skipWs(); if (text[i] !== ":") err("expected ':' after object key"); i++; const val = parseValue(depth + 1); objectDefineProperty(obj, key, { value: val, enumerable: true, writable: true, configurable: true }); skipWs(); const ch = text[i]; if (ch === ",") { i++; continue; } if (ch === "}") { i++; return obj; } err("expected ',' or '}' in object"); } } function parseArray(depth) { i++; const arr = []; skipWs(); if (text[i] === "]") { i++; return inertArray(arr); } for (;;) { arrayPush(arr, parseValue(depth + 1)); skipWs(); const ch = text[i]; if (ch === ",") { i++; continue; } if (ch === "]") { i++; return inertArray(arr); } err("expected ',' or ']' in array"); } } function parseString() { i++; let out = ""; for (;;) { if (i >= n) err("unterminated string"); const c = text[i]; if (c === '"') { i++; if (!strIsWellFormed(out)) err("unpaired surrogate in string"); return out; } if (c === "\\") { i++; const e = text[i]; switch (e) { case '"': out += '"'; break; case "\\": out += "\\"; break; case "/": out += "/"; break; case "b": out += "\b"; break; case "f": out += "\f"; break; case "n": out += "\n"; break; case "r": out += "\r"; break; case "t": out += "\t"; break; case "u": { const hex = strSlice(text, i + 1, i + 5); if (!isHex4(hex)) err("invalid \\u escape"); out += strFromCharCode(parseIntRadix(hex, 16)); i += 4; break; } default: err(`invalid escape '\\${e}'`); } i++; continue; } const code = strCharCodeAt(text, i); if (code < 0x20) err("unescaped control character in string"); out += c; i++; } } function parseNumber() { const start = i; if (text[i] === "-") i++; if (text[i] === "0") { i++; } else if (text[i] >= "1" && text[i] <= "9") { while (i < n && text[i] >= "0" && text[i] <= "9") i++; } else { err("invalid number"); } if (text[i] === "." || text[i] === "e" || text[i] === "E") { err("non-integer (float/exponent) number not allowed"); } const raw = strSlice(text, start, i); const num = toNumber(raw); if (!isSafeInteger(num)) err("integer outside safe range"); return num; } function parseBool() { if (strStartsWithAt(text, "true", i)) { i += 4; return true; } if (strStartsWithAt(text, "false", i)) { i += 5; return false; } err("invalid literal"); } function parseNull() { if (strStartsWithAt(text, "null", i)) { i += 4; return null; } err("invalid literal"); } const value = parseValue(0); skipWs(); if (i !== n) err("trailing characters after JSON value"); return value; }