import { generateKeyPairSync, sign } from 'node:crypto'; import { describe, expect, test } from 'vitest'; import { computeApprovalClosure, verifyApprovalProjectionAttestation } from '../../src/security/approval-closure.ts'; import { type ApprovalPayload } from '../../src/security/approval-envelope.ts'; import { canonicalJsonDigest, canonicalValueBytes } from '../../src/security/canonical-digest.ts'; const h = (c: string) => c.repeat(64); const keys = generateKeyPairSync('ed25519'); const trust = { now: '2026-09-02T00:00:00Z', latestLedgerHeadHash: h('9'), latestLedgerRevision: 3, trustedKeys: [{ keyId: 'key', issuerEpoch: 1, scope: 'portfolio-approval-decision/v1' as const, authorizedRoles: ['approver'], publicKey: keys.publicKey }], }; function envelope(payload: ApprovalPayload) { return { schema: 'portfolio-approval-envelope/v1' as const, payload, signature: { algorithm: 'Ed25519' as const, encoding: 'base64url-nopad' as const, value: sign(null, canonicalValueBytes(payload), keys.privateKey).toString('base64url') }, }; } function base(recordClass: 'content' | 'asset', id: string): ApprovalPayload { return { schema: 'portfolio-approval-payload/v1', keyId: 'key', issuerEpoch: 1, authorizedRole: 'approver', evidenceFingerprints: [h('1')], recordClass, recordType: recordClass === 'content' ? 'profile' : '', publicRecordIdOrAssetId: id, approvedPublicRecordHashOrManifestRecordHash: h(recordClass === 'content' ? '2' : '3'), sanitizedOutputHashOrNull: recordClass === 'asset' ? h('4') : null, decision: 'approve', issuedAt: '2026-09-01T00:00:00Z', expiresAt: '2026-10-01T00:00:00Z', previousLedgerHeadHash: h('8'), ledgerRevision: 2, policyVersion: 'p1', }; } describe('approval closure', () => { test('computes exact sorted record, asset, and proof roots', () => { const content = base('content', 'profile-main'); const asset = base('asset', 'asset-main'); const result = computeApprovalClosure({ contentRecords: [{ recordType: 'profile', publicRecordId: 'profile-main', approvedPublicRecordHash: h('2'), envelope: envelope(content) }], assetRecords: [{ assetId: 'asset-main', approvedManifestRecordHash: h('3'), sanitizedOutputHash: h('4'), envelope: envelope(asset) }], trust, }); const contentEnvelopeDigest = canonicalJsonDigest('portfolio-approval-envelope-digest/v1', envelope(content)); const assetEnvelopeDigest = canonicalJsonDigest('portfolio-approval-envelope-digest/v1', envelope(asset)); expect(result.approvedRecordRoot).toBe(canonicalJsonDigest('portfolio-record-closure/v1', { records: [['profile', 'profile-main', h('2'), contentEnvelopeDigest]], })); expect(result.approvedAssetRoot).toBe(canonicalJsonDigest('portfolio-asset-closure/v1', { assets: [['asset-main', h('3'), h('4'), assetEnvelopeDigest]], })); expect(result.approvalProofSetDigest).toBe(canonicalJsonDigest('portfolio-approval-proof-set/v1', { envelopes: [['asset', '', 'asset-main', assetEnvelopeDigest], ['content', 'profile', 'profile-main', contentEnvelopeDigest]], })); }); test('rejects duplicate IDs and stale protected ledger input', () => { const content = base('content', 'profile-main'); const record = { recordType: 'profile', publicRecordId: 'profile-main', approvedPublicRecordHash: h('2'), envelope: envelope(content) }; expect(() => computeApprovalClosure({ contentRecords: [record, record], assetRecords: [], trust })).toThrow(); expect(() => computeApprovalClosure({ contentRecords: [record], assetRecords: [], trust: { ...trust, latestLedgerRevision: 1 } })).toThrow(); }); test('verifies a scoped projection attestation against protected state and trusted time', () => { const payload = { schema: 'portfolio-approval-projection-attestation/v1' as const, keyId: 'projection-key', issuerEpoch: 4, scope: 'portfolio-approval-projection-attestation/v1' as const, approvedRecordRoot: h('1'), approvedAssetRoot: h('2'), approvalProofSetDigest: h('3'), inputDigests: { PortfolioContent: h('4'), PublicAssetManifest: h('5'), PreparedAssetSet: h('6'), SiteConfig: h('7') }, productionInputRoot: 'e19fd4083ba973074449e5ecc9364c2b339eadea32e61d8dd2f0d8e3d6e91c85', liveLedgerHeadHash: h('8'), liveLedgerRevision: 42, policyVersion: 'projection-policy-1', issuedAt: '2026-09-02T00:00:00Z', expiresAt: '2026-09-03T00:00:00Z', earliestApprovalExpiry: '2026-09-04T00:00:00Z', }; const projectionKeys = generateKeyPairSync('ed25519'); const attestation = { schema: 'portfolio-approval-projection-attestation-envelope/v1' as const, payload, signature: { algorithm: 'Ed25519' as const, encoding: 'base64url-nopad' as const, value: sign(null, canonicalValueBytes(payload), projectionKeys.privateKey).toString('base64url') }, }; expect(verifyApprovalProjectionAttestation(attestation, { now: '2026-09-02T01:00:00Z', latestLedgerHeadHash: h('8'), latestLedgerRevision: 42, trustedKey: { keyId: 'projection-key', issuerEpoch: 4, scope: 'portfolio-approval-projection-attestation/v1', publicKey: projectionKeys.publicKey }, }).liveLedgerRevision).toBe(42); expect(() => verifyApprovalProjectionAttestation(attestation, { now: '2026-09-02T01:00:00Z', latestLedgerHeadHash: h('9'), latestLedgerRevision: 42, trustedKey: { keyId: 'projection-key', issuerEpoch: 4, scope: 'portfolio-approval-projection-attestation/v1', publicKey: projectionKeys.publicKey }, })).toThrow(); }); });