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.

86 lines (85 loc) • 4.34 kB
import { parseDocument } from "./bytes.js"; import { arrayIncludes, dateParse, isArray, isNaNValue, jsonStringify, objectCreateNull, objectGetOwnPropertyNames, } from "./intrinsics.js"; import { isRfc3339Instant } from "./scan.js"; export const SIGNING_KEY_LIFECYCLE_SPEC = "noa.signing-key-lifecycle/0.1"; export function parseVerificationKeyring(document, label = "keyring") { const parsed = parseDocument(document, label); if (!parsed.ok) return parsed; const value = parsed.value; if (typeof value !== "object" || value === null || isArray(value)) { return { ok: false, reason: `${label} must be an object (static kid map or atomic signing key lifecycle)` }; } const top = value; const topNames = objectGetOwnPropertyNames(top); const hasLifecycleSpec = arrayIncludes(topNames, "spec") && top.spec === SIGNING_KEY_LIFECYCLE_SPEC; const hasStructuredKeys = arrayIncludes(topNames, "keys") && typeof top.keys !== "string"; const looksLikeLifecycle = hasLifecycleSpec || hasStructuredKeys; const keyring = objectCreateNull(); const retiredKids = objectCreateNull(); if (!looksLikeLifecycle) { for (let i = 0; i < topNames.length; i++) { const kid = topNames[i]; const publicKey = top[kid]; if (typeof publicKey !== "string" || publicKey.length === 0) { return { ok: false, reason: `${label} public key for signing key ${jsonStringify(kid)} must be a non-empty string` }; } keyring[kid] = publicKey; } return { ok: true, value: { keyring, retiredKids, lifecycle: false } }; } if (top.spec !== SIGNING_KEY_LIFECYCLE_SPEC || topNames.length !== 2 || !arrayIncludes(topNames, "spec") || !arrayIncludes(topNames, "keys")) { return { ok: false, reason: "malformed signing key lifecycle" }; } const entries = top.keys; if (typeof entries !== "object" || entries === null || isArray(entries)) { return { ok: false, reason: "signing key lifecycle keys must be an object" }; } const kids = objectGetOwnPropertyNames(entries); if (kids.length === 0) return { ok: false, reason: "signing key lifecycle must contain at least one key" }; for (let i = 0; i < kids.length; i++) { const kid = kids[i]; const entry = entries[kid]; if (typeof entry !== "object" || entry === null || isArray(entry)) { return { ok: false, reason: `lifecycle entry for signing key ${jsonStringify(kid)} must be an object` }; } const fields = objectGetOwnPropertyNames(entry); if (fields.length !== 2 || !arrayIncludes(fields, "publicKey") || !arrayIncludes(fields, "retiredAt")) { return { ok: false, reason: `lifecycle entry for signing key ${jsonStringify(kid)} must contain exactly publicKey + retiredAt` }; } const publicKey = entry.publicKey; const retiredAt = entry.retiredAt; if (typeof publicKey !== "string" || publicKey.length === 0) { return { ok: false, reason: `lifecycle publicKey for signing key ${jsonStringify(kid)} must be a non-empty string` }; } if (retiredAt !== null && (typeof retiredAt !== "string" || !isRfc3339Instant(retiredAt) || isNaNValue(dateParse(retiredAt)))) { return { ok: false, reason: `lifecycle retiredAt for signing key ${jsonStringify(kid)} must be null or a parseable RFC 3339 instant` }; } keyring[kid] = publicKey; if (retiredAt !== null) retiredKids[kid] = true; } return { ok: true, value: { keyring, retiredKids, lifecycle: true } }; } export function resolveVerificationKey(document, kid) { const parsed = parseVerificationKeyring(document); if (!parsed.ok) return parsed; if (parsed.value.retiredKids[kid] === true) { return { ok: false, reason: `signing key ${jsonStringify(kid)} is retired; signer-chosen artifact time is not an independent witness`, }; } const publicKey = parsed.value.keyring[kid]; if (!publicKey) return { ok: false, reason: `signing key ${jsonStringify(kid)} not in keyring` }; return { ok: true, publicKey, lifecycle: parsed.value.lifecycle }; }