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.

105 lines (104 loc) • 4.36 kB
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 }), }; }