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
JavaScript
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;
}