#!/usr/bin/env node import { createPublicKey } from 'node:crypto'; import { readFile } from 'node:fs/promises'; import { pathToFileURL } from 'node:url'; import { computeApprovalClosure } from '../../src/security/approval-closure.ts'; import { canonicalValueBytes, isSha256Hex, parseCanonicalJson } from '../../src/security/canonical-digest.ts'; import { computeProductionInputRoot } from '../../src/security/production-input-root.ts'; const expectedRequestFields = [ 'schema', 'now', 'latestLedgerHeadHash', 'latestLedgerRevision', 'trustedKeys', 'contentRecords', 'assetRecords', 'inputDigests', ].sort(); const expectedKeyFields = ['keyId', 'issuerEpoch', 'scope', 'authorizedRoles', 'publicKeyPem'].sort(); const expectedInputs = ['PortfolioContent', 'PreparedAssetSet', 'PublicAssetManifest', 'SiteConfig']; function exactObject(value, fields, label) { if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} must be an object.`); if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify(fields)) throw new Error(`${label} contains missing or unknown fields.`); return value; } function requestPath(args) { if (args.length !== 2 || args[0] !== '--request' || args[1].length === 0) throw new Error('Usage: verify-approvals.mjs --request '); return args[1]; } export function verifyApprovalRequest(source) { const request = exactObject(parseCanonicalJson(source), expectedRequestFields, 'request'); if (request.schema !== 'portfolio-approval-verification-request/v1') throw new Error('Request schema is invalid.'); if (!Array.isArray(request.trustedKeys) || !Array.isArray(request.contentRecords) || !Array.isArray(request.assetRecords)) throw new Error('Approval record lists are invalid.'); const trustedKeys = request.trustedKeys.map((candidate) => { const key = exactObject(candidate, expectedKeyFields, 'trusted key'); if (key.scope !== 'portfolio-approval-decision/v1' || typeof key.keyId !== 'string' || !Number.isSafeInteger(key.issuerEpoch) || !Array.isArray(key.authorizedRoles) || key.authorizedRoles.some((role) => typeof role !== 'string') || typeof key.publicKeyPem !== 'string') { throw new Error('Trusted approval key is invalid.'); } return { keyId: key.keyId, issuerEpoch: key.issuerEpoch, scope: key.scope, authorizedRoles: key.authorizedRoles, publicKey: createPublicKey(key.publicKeyPem), }; }); const inputDigests = exactObject(request.inputDigests, expectedInputs, 'inputDigests'); if (Object.values(inputDigests).some((digest) => !isSha256Hex(digest))) throw new Error('Input digest is invalid.'); const closure = computeApprovalClosure({ contentRecords: request.contentRecords, assetRecords: request.assetRecords, trust: { now: request.now, latestLedgerHeadHash: request.latestLedgerHeadHash, latestLedgerRevision: request.latestLedgerRevision, trustedKeys, }, }); const productionInputRoot = computeProductionInputRoot({ portfolioContentDigest: inputDigests.PortfolioContent, publicAssetManifestDigest: inputDigests.PublicAssetManifest, preparedAssetSetDigest: inputDigests.PreparedAssetSet, siteConfigDigest: inputDigests.SiteConfig, }); return { schema: 'portfolio-approval-verification-result/v1', approvedRecordRoot: closure.approvedRecordRoot, approvedAssetRoot: closure.approvedAssetRoot, approvalProofSetDigest: closure.approvalProofSetDigest, productionInputRoot, latestLedgerHeadHash: request.latestLedgerHeadHash, latestLedgerRevision: request.latestLedgerRevision, earliestApprovalExpiry: closure.earliestApprovalExpiry, }; } async function main() { const path = requestPath(process.argv.slice(2)); const result = verifyApprovalRequest(await readFile(path)); process.stdout.write(Buffer.from(canonicalValueBytes(result))); process.stdout.write('\n'); } if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) { main().catch((error) => { const message = error instanceof Error ? error.message : 'Unknown verification failure.'; process.stderr.write(`Approval verification failed: ${message}\n`); process.exitCode = 1; }); }