Files
Portfolio/tests/unit/approval-closure.test.ts

87 lines
5.4 KiB
TypeScript

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();
});
});