import { generateKeyPairSync, sign } from 'node:crypto'; import { describe, expect, test } from 'vitest'; import { type ApprovalPayload, parseApprovalEnvelopeJson, verifyApprovalEnvelope, } from '../../src/security/approval-envelope.ts'; import { canonicalValueBytes } from '../../src/security/canonical-digest.ts'; const hash = (character: string) => character.repeat(64); const payload = (): ApprovalPayload => ({ schema: 'portfolio-approval-payload/v1', keyId: 'approval-2026', issuerEpoch: 3, authorizedRole: 'portfolio-approver', evidenceFingerprints: [hash('a')], recordClass: 'content', recordType: 'profile', publicRecordIdOrAssetId: 'profile-main', approvedPublicRecordHashOrManifestRecordHash: hash('b'), sanitizedOutputHashOrNull: null, decision: 'approve', issuedAt: '2026-09-01T08:00:00Z', expiresAt: '2026-10-01T08:00:00Z', previousLedgerHeadHash: hash('c'), ledgerRevision: 12, policyVersion: 'approval-policy-1', }); function signedEnvelope(overrides: Partial = {}) { const keys = generateKeyPairSync('ed25519'); const signedPayload = { ...payload(), ...overrides }; const signature = sign(null, canonicalValueBytes(signedPayload), keys.privateKey).toString('base64url'); return { envelope: { schema: 'portfolio-approval-envelope/v1' as const, payload: signedPayload, signature: { algorithm: 'Ed25519' as const, encoding: 'base64url-nopad' as const, value: signature }, }, publicKey: keys.publicKey, }; } describe('approval envelopes', () => { test('verifies an exact, current, scoped Ed25519 approval', () => { const { envelope, publicKey } = signedEnvelope(); const verified = verifyApprovalEnvelope(envelope, { now: '2026-09-02T00:00:00Z', latestLedgerHeadHash: hash('d'), latestLedgerRevision: 13, trustedKeys: [{ keyId: 'approval-2026', issuerEpoch: 3, scope: 'portfolio-approval-decision/v1', authorizedRoles: ['portfolio-approver'], publicKey, }], }); expect(verified.publicRecordIdOrAssetId).toBe('profile-main'); }); test.each([ ['expired', { expiresAt: '2026-09-01T09:00:00Z' }], ['non-UTC timestamp', { issuedAt: '2026-09-01T10:00:00+02:00' }], ['wrong epoch', { issuerEpoch: 2 }], ['rejection', { decision: 'reject' }], ] as const)('rejects a %s envelope', (_label, override) => { const { envelope, publicKey } = signedEnvelope(override); expect(() => verifyApprovalEnvelope(envelope, { now: '2026-09-02T00:00:00Z', latestLedgerHeadHash: hash('d'), latestLedgerRevision: 13, trustedKeys: [{ keyId: 'approval-2026', issuerEpoch: 3, scope: 'portfolio-approval-decision/v1', authorizedRoles: ['portfolio-approver'], publicKey }], })).toThrow(); }); test('rejects unknown fields, duplicate keys, and alternate signature encodings at ingest', () => { const { envelope } = signedEnvelope(); expect(() => parseApprovalEnvelopeJson(JSON.stringify({ ...envelope, extra: true }))).toThrow(); expect(() => parseApprovalEnvelopeJson('{"schema":"portfolio-approval-envelope/v1","schema":"portfolio-approval-envelope/v1"}')).toThrow(); expect(() => parseApprovalEnvelopeJson(JSON.stringify({ ...envelope, signature: { ...envelope.signature, value: `${envelope.signature.value}=` }, }))).toThrow(); }); });