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.
105 lines (104 loc) • 4.36 kB
JavaScript
import { verifyChain, DEFAULT_MAX_RECEIPTS } from "../verify.js";
import { parseDocument } from "../bytes.js";
import { inertOptions } from "../opts.js";
import { frozenTable } from "../inert.js";
import { isArray, arrayLength, isFiniteNumber } from "../intrinsics.js";
import { verifyCompletenessParsed, } from "./acceptance.js";
const INVALID_HEAD = frozenTable({ chain: "", seq: -1, hash: "" });
function deriveHead(parsed) {
if (!isArray(parsed) || arrayLength(parsed) === 0)
return INVALID_HEAD;
const pn = arrayLength(parsed);
let head = null;
let maxSeq = -Infinity;
for (let pi = 0; pi < pn; pi++) {
const r = parsed[pi];
if (typeof r !== "object" || r === null)
continue;
const chainField = r.chain;
if (typeof chainField !== "object" || chainField === null)
continue;
const seq = chainField.seq;
if (typeof seq !== "number" || !isFiniteNumber(seq))
continue;
if (seq > maxSeq) {
maxSeq = seq;
head = r;
}
}
if (head === null)
return INVALID_HEAD;
const scope = head.scope;
const chainObj = head.chain;
const chainId = scope?.chain;
const seq = chainObj.seq;
const hash = chainObj.hash;
return {
chain: typeof chainId === "string" ? chainId : "",
seq: typeof seq === "number" ? seq : -1,
hash: typeof hash === "string" ? hash : "",
};
}
export function verifyChainWitnessed(chain, keyring, opts) {
const admitted = inertOptions(WITNESSED_OPTION_SCHEMA, opts, "options");
if (!admitted.ok)
return failClosed(admitted.reason);
const o = admitted.value;
if (o.anchors === undefined || o.trustSet === undefined) {
return failClosed("options: anchors and trustSet are required (federation-spec §4)");
}
const aParsed = parseDocument(o.anchors, "anchors");
if (!aParsed.ok)
return failClosed(aParsed.reason);
const tParsed = parseDocument(o.trustSet, "trustSet");
if (!tParsed.ok)
return failClosed(tParsed.reason);
let freshness;
if (o.freshness !== undefined) {
const f = o.freshness;
if (f.now === undefined || f.maxAgeMs === undefined) {
return failClosed("opts.freshness must be an object { now, maxAgeMs, skewMs? }");
}
freshness = f;
}
const verifyOpts = {};
if (keyring !== undefined)
verifyOpts.keyring = keyring;
if (o.checkpoint !== undefined)
verifyOpts.checkpoint = o.checkpoint;
if (o.identityManifest !== undefined)
verifyOpts.identityManifest = o.identityManifest;
if (o.maxReceipts !== undefined)
verifyOpts.maxReceipts = o.maxReceipts;
if (o.requireTenantConsistency !== undefined)
verifyOpts.requireTenantConsistency = o.requireTenantConsistency;
const chainResult = verifyChain(chain, verifyOpts);
const maxReceipts = o.maxReceipts ?? DEFAULT_MAX_RECEIPTS;
let head = INVALID_HEAD;
const parsed = parseDocument(chain, "receipts");
if (parsed.ok && isArray(parsed.value) && arrayLength(parsed.value) <= maxReceipts) {
head = deriveHead(parsed.value);
}
const witness = verifyCompletenessParsed(head, aParsed.value, tParsed.value, freshness === undefined ? {} : { freshness });
return { chain: chainResult, witness };
}
const FRESHNESS_OPTION_SCHEMA = Object.freeze(Object.assign(Object.create(null), {
now: { kind: "count", max: Number.MAX_SAFE_INTEGER },
maxAgeMs: { kind: "count", max: Number.MAX_SAFE_INTEGER },
skewMs: { kind: "count", max: Number.MAX_SAFE_INTEGER },
}));
const WITNESSED_OPTION_SCHEMA = Object.freeze(Object.assign(Object.create(null), {
anchors: { kind: "document" },
trustSet: { kind: "document" },
checkpoint: { kind: "document" },
identityManifest: { kind: "document" },
maxReceipts: { kind: "count", max: DEFAULT_MAX_RECEIPTS },
requireTenantConsistency: { kind: "boolean" },
freshness: { kind: "nested", schema: FRESHNESS_OPTION_SCHEMA },
}));
function failClosed(reason) {
return {
chain: { status: "MALFORMED", chain: null, count: 0, signaturesVerified: false, tailChecked: false, reason, warnings: [] },
witness: verifyCompletenessParsed(INVALID_HEAD, [], { witnesses: [], quorum: 0 }),
};
}