@clawbureau/clawverify-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +40 -0
  3. package/dist/crypto.d.ts +27 -0
  4. package/dist/crypto.d.ts.map +1 -0
  5. package/dist/crypto.js +124 -0
  6. package/dist/crypto.js.map +1 -0
  7. package/dist/index.d.ts +27 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +24 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/jcs.d.ts +13 -0
  12. package/dist/jcs.d.ts.map +1 -0
  13. package/dist/jcs.js +43 -0
  14. package/dist/jcs.js.map +1 -0
  15. package/dist/model-identity.d.ts +46 -0
  16. package/dist/model-identity.d.ts.map +1 -0
  17. package/dist/model-identity.js +233 -0
  18. package/dist/model-identity.js.map +1 -0
  19. package/dist/schema-registry.d.ts +99 -0
  20. package/dist/schema-registry.d.ts.map +1 -0
  21. package/dist/schema-registry.js +259 -0
  22. package/dist/schema-registry.js.map +1 -0
  23. package/dist/schema-validation.d.ts +35 -0
  24. package/dist/schema-validation.d.ts.map +1 -0
  25. package/dist/schema-validation.js +156 -0
  26. package/dist/schema-validation.js.map +1 -0
  27. package/dist/schema-validators.generated.d.ts +158 -0
  28. package/dist/schema-validators.generated.d.ts.map +1 -0
  29. package/dist/schema-validators.generated.js +19186 -0
  30. package/dist/schema-validators.generated.js.map +1 -0
  31. package/dist/types.d.ts +910 -0
  32. package/dist/types.d.ts.map +1 -0
  33. package/dist/types.js +33 -0
  34. package/dist/types.js.map +1 -0
  35. package/dist/verify-audit-result-attestation.d.ts +32 -0
  36. package/dist/verify-audit-result-attestation.d.ts.map +1 -0
  37. package/dist/verify-audit-result-attestation.js +396 -0
  38. package/dist/verify-audit-result-attestation.js.map +1 -0
  39. package/dist/verify-derivation-attestation.d.ts +30 -0
  40. package/dist/verify-derivation-attestation.d.ts.map +1 -0
  41. package/dist/verify-derivation-attestation.js +371 -0
  42. package/dist/verify-derivation-attestation.js.map +1 -0
  43. package/dist/verify-execution-attestation.d.ts +32 -0
  44. package/dist/verify-execution-attestation.d.ts.map +1 -0
  45. package/dist/verify-execution-attestation.js +578 -0
  46. package/dist/verify-execution-attestation.js.map +1 -0
  47. package/dist/verify-export-bundle.d.ts +14 -0
  48. package/dist/verify-export-bundle.d.ts.map +1 -0
  49. package/dist/verify-export-bundle.js +307 -0
  50. package/dist/verify-export-bundle.js.map +1 -0
  51. package/dist/verify-log-inclusion-proof.d.ts +16 -0
  52. package/dist/verify-log-inclusion-proof.d.ts.map +1 -0
  53. package/dist/verify-log-inclusion-proof.js +216 -0
  54. package/dist/verify-log-inclusion-proof.js.map +1 -0
  55. package/dist/verify-proof-bundle.d.ts +48 -0
  56. package/dist/verify-proof-bundle.d.ts.map +1 -0
  57. package/dist/verify-proof-bundle.js +1708 -0
  58. package/dist/verify-proof-bundle.js.map +1 -0
  59. package/dist/verify-receipt.d.ts +30 -0
  60. package/dist/verify-receipt.d.ts.map +1 -0
  61. package/dist/verify-receipt.js +408 -0
  62. package/dist/verify-receipt.js.map +1 -0
  63. package/dist/verify-web-receipt.d.ts +21 -0
  64. package/dist/verify-web-receipt.d.ts.map +1 -0
  65. package/dist/verify-web-receipt.js +341 -0
  66. package/dist/verify-web-receipt.js.map +1 -0
  67. package/package.json +54 -0
@@ -0,0 +1,1708 @@
1
+ /**
2
+ * Proof Bundle Verification
3
+ * CVF-US-007: Verify proof bundles for trust tier computation
4
+ * POH-US-003: Validate proof bundles against PoH schema, verify receipts
5
+ * with clawproxy DID, verify event-chain hash linkage, and
6
+ * return trust tier based on validated components.
7
+ *
8
+ * Validates:
9
+ * - Proof bundle payload against PoH schema (proof_bundle.v1)
10
+ * - URM (Universal Resource Manifest) structure
11
+ * - Event chain hash linkage and run_id consistency
12
+ * - Gateway receipt envelopes (cryptographic verification)
13
+ * - Attestations
14
+ *
15
+ * Computes trust tier based on which components are present and valid.
16
+ * Fail-closed: unknown or malformed payloads always result in 'unknown' tier.
17
+ */
18
+ import { isAllowedVersion, isAllowedType, isAllowedAlgorithm, isAllowedHashAlgorithm, isValidDidFormat, isValidBase64Url, isValidIsoDate, } from './schema-registry.js';
19
+ import { computeHash, base64UrlDecode, extractPublicKeyFromDidKey, verifySignature, } from './crypto.js';
20
+ import { verifyReceipt } from './verify-receipt.js';
21
+ import { computeModelIdentityTierFromReceipts } from './model-identity.js';
22
+ import { jcsCanonicalize } from './jcs.js';
23
+ import { validateProofBundleEnvelopeV1, validateUrmV1, validatePromptPackV1, validateSystemPromptReportV1, } from './schema-validation.js';
24
+ // CVF-US-025: size/count hardening
25
+ const MAX_EVENT_CHAIN_ENTRIES = 1000;
26
+ const MAX_RECEIPTS = 1000;
27
+ const MAX_ATTESTATIONS = 100;
28
+ const MAX_METADATA_BYTES = 16 * 1024;
29
+ function jsonByteSize(value) {
30
+ try {
31
+ const bytes = new TextEncoder().encode(JSON.stringify(value));
32
+ return bytes.byteLength;
33
+ }
34
+ catch {
35
+ return Number.POSITIVE_INFINITY;
36
+ }
37
+ }
38
+ /**
39
+ * Validate envelope structure for proof bundle
40
+ */
41
+ function validateEnvelopeStructure(envelope) {
42
+ if (typeof envelope !== 'object' || envelope === null) {
43
+ return false;
44
+ }
45
+ const e = envelope;
46
+ return ('envelope_version' in e &&
47
+ 'envelope_type' in e &&
48
+ 'payload' in e &&
49
+ 'payload_hash_b64u' in e &&
50
+ 'hash_algorithm' in e &&
51
+ 'signature_b64u' in e &&
52
+ 'algorithm' in e &&
53
+ 'signer_did' in e &&
54
+ 'issued_at' in e);
55
+ }
56
+ /**
57
+ * Validate proof bundle payload structure against PoH schema (proof_bundle.v1).
58
+ *
59
+ * Schema constraints enforced:
60
+ * - bundle_version: const "1"
61
+ * - bundle_id: string, minLength 1
62
+ * - agent_did: string, pattern ^did:
63
+ * - At least one of: urm, event_chain, receipts, attestations
64
+ */
65
+ function validateBundlePayload(payload) {
66
+ if (typeof payload !== 'object' || payload === null) {
67
+ return { valid: false, error: 'Payload must be an object' };
68
+ }
69
+ const p = payload;
70
+ // Required fields per schema
71
+ if (p.bundle_version !== '1') {
72
+ return { valid: false, error: 'bundle_version must be "1"' };
73
+ }
74
+ if (typeof p.bundle_id !== 'string' || p.bundle_id.length === 0) {
75
+ return { valid: false, error: 'bundle_id is required and must be non-empty' };
76
+ }
77
+ if (typeof p.agent_did !== 'string' || !/^did:/.test(p.agent_did)) {
78
+ return { valid: false, error: 'agent_did must be a string starting with "did:"' };
79
+ }
80
+ // At least one component must be present (schema anyOf)
81
+ const hasUrm = p.urm !== undefined;
82
+ const hasEventChain = Array.isArray(p.event_chain) && p.event_chain.length > 0;
83
+ const hasReceipts = Array.isArray(p.receipts) && p.receipts.length > 0;
84
+ const hasAttestations = Array.isArray(p.attestations) && p.attestations.length > 0;
85
+ if (!hasUrm && !hasEventChain && !hasReceipts && !hasAttestations) {
86
+ return { valid: false, error: 'At least one of urm, event_chain, receipts, or attestations is required' };
87
+ }
88
+ return { valid: true };
89
+ }
90
+ /**
91
+ * Type guard helper (thin wrapper for backward compatibility)
92
+ */
93
+ function isBundlePayload(payload) {
94
+ return validateBundlePayload(payload).valid;
95
+ }
96
+ /**
97
+ * Validate URM reference structure per PoH schema (proof_bundle.v1 → urm).
98
+ *
99
+ * Schema constraints:
100
+ * - urm_version: const "1"
101
+ * - urm_id: string, minLength 1
102
+ * - resource_type: string, minLength 1
103
+ * - resource_hash_b64u: base64url string, minLength 8
104
+ */
105
+ function validateURM(urm) {
106
+ if (typeof urm !== 'object' || urm === null)
107
+ return false;
108
+ const u = urm;
109
+ return (u.urm_version === '1' &&
110
+ typeof u.urm_id === 'string' &&
111
+ u.urm_id.length >= 1 &&
112
+ typeof u.resource_type === 'string' &&
113
+ u.resource_type.length >= 1 &&
114
+ typeof u.resource_hash_b64u === 'string' &&
115
+ u.resource_hash_b64u.length >= 8 &&
116
+ isValidBase64Url(u.resource_hash_b64u));
117
+ }
118
+ /**
119
+ * Validate event chain entries and hash chain integrity per PoH schema.
120
+ *
121
+ * Schema constraints per event_chain.v1 / proof_bundle.v1:
122
+ * - event_id, run_id, event_type: string, minLength 1
123
+ * - timestamp: ISO 8601 date-time
124
+ * - payload_hash_b64u, event_hash_b64u: base64url, minLength 8
125
+ * - prev_hash_b64u: base64url (minLength 8) or null for the first event
126
+ * - Hash chain: first event has null prev_hash, subsequent events link
127
+ * - run_id consistency across all events
128
+ */
129
+ function validateEventChain(events) {
130
+ if (events.length === 0) {
131
+ return { valid: false, error: 'Empty event chain' };
132
+ }
133
+ let prevHash = null;
134
+ let expectedRunId = null;
135
+ let chainRootHash = null;
136
+ for (let i = 0; i < events.length; i++) {
137
+ const event = events[i];
138
+ // Validate required fields with minLength constraints
139
+ if (typeof event.event_id !== 'string' || event.event_id.length < 1) {
140
+ return { valid: false, error: `Event ${i}: missing or empty event_id` };
141
+ }
142
+ if (typeof event.run_id !== 'string' || event.run_id.length < 1) {
143
+ return { valid: false, error: `Event ${i}: missing or empty run_id` };
144
+ }
145
+ if (typeof event.event_type !== 'string' || event.event_type.length < 1) {
146
+ return { valid: false, error: `Event ${i}: missing or empty event_type` };
147
+ }
148
+ if (!isValidIsoDate(event.timestamp)) {
149
+ return { valid: false, error: `Event ${i}: invalid timestamp` };
150
+ }
151
+ if (!isValidBase64Url(event.payload_hash_b64u) ||
152
+ event.payload_hash_b64u.length < 8) {
153
+ return { valid: false, error: `Event ${i}: invalid payload_hash_b64u (must be base64url, minLength 8)` };
154
+ }
155
+ if (!isValidBase64Url(event.event_hash_b64u) ||
156
+ event.event_hash_b64u.length < 8) {
157
+ return { valid: false, error: `Event ${i}: invalid event_hash_b64u (must be base64url, minLength 8)` };
158
+ }
159
+ // Enforce run_id consistency
160
+ if (expectedRunId === null) {
161
+ expectedRunId = event.run_id;
162
+ }
163
+ else if (event.run_id !== expectedRunId) {
164
+ return {
165
+ valid: false,
166
+ error: `Event ${i}: inconsistent run_id (expected ${expectedRunId})`,
167
+ };
168
+ }
169
+ // Validate hash chain linkage
170
+ const eventPrevHash = event.prev_hash_b64u;
171
+ if (i === 0) {
172
+ // First event should have null prev_hash
173
+ if (eventPrevHash !== null && eventPrevHash !== '') {
174
+ return {
175
+ valid: false,
176
+ error: 'First event should have null prev_hash_b64u',
177
+ };
178
+ }
179
+ chainRootHash = event.event_hash_b64u;
180
+ }
181
+ else {
182
+ // Non-first events: prev_hash must be base64url, minLength 8
183
+ if (typeof eventPrevHash !== 'string' ||
184
+ !isValidBase64Url(eventPrevHash) ||
185
+ eventPrevHash.length < 8) {
186
+ return {
187
+ valid: false,
188
+ error: `Event ${i}: invalid prev_hash_b64u (must be base64url, minLength 8)`,
189
+ };
190
+ }
191
+ // Must link to previous event's hash
192
+ if (eventPrevHash !== prevHash) {
193
+ return {
194
+ valid: false,
195
+ error: `Event ${i}: hash chain break detected`,
196
+ };
197
+ }
198
+ }
199
+ prevHash = event.event_hash_b64u;
200
+ }
201
+ return { valid: true, chain_root_hash: chainRootHash ?? undefined };
202
+ }
203
+ /**
204
+ * Validate attestation references per PoH schema (proof_bundle.v1 → attestations).
205
+ *
206
+ * Schema constraints:
207
+ * - attestation_id: string, minLength 1
208
+ * - attestation_type: enum ["owner", "third_party"]
209
+ * - attester_did: string, pattern ^did:
210
+ * - subject_did: string, pattern ^did:
211
+ * - signature_b64u: base64url, minLength 8
212
+ * - expires_at: optional ISO 8601 date-time
213
+ */
214
+ function validateAttestation(attestation) {
215
+ if (typeof attestation !== 'object' || attestation === null)
216
+ return false;
217
+ const a = attestation;
218
+ // Fail-closed: reject unknown fields (schemas use additionalProperties:false)
219
+ const allowedKeys = new Set([
220
+ 'attestation_id',
221
+ 'attestation_type',
222
+ 'attester_did',
223
+ 'subject_did',
224
+ 'expires_at',
225
+ 'signature_b64u',
226
+ ]);
227
+ for (const k of Object.keys(a)) {
228
+ if (!allowedKeys.has(k))
229
+ return false;
230
+ }
231
+ // Check required fields with schema constraints
232
+ if (typeof a.attestation_id !== 'string' || a.attestation_id.length < 1)
233
+ return false;
234
+ if (a.attestation_type !== 'owner' && a.attestation_type !== 'third_party')
235
+ return false;
236
+ if (!isValidDidFormat(a.attester_did))
237
+ return false;
238
+ if (!isValidDidFormat(a.subject_did))
239
+ return false;
240
+ if (!isValidBase64Url(a.signature_b64u) ||
241
+ a.signature_b64u.length < 8)
242
+ return false;
243
+ // Check expiry if present
244
+ if (a.expires_at !== undefined) {
245
+ if (!isValidIsoDate(a.expires_at))
246
+ return false;
247
+ const expiryDate = new Date(a.expires_at);
248
+ if (expiryDate < new Date()) {
249
+ return false; // Expired — fail closed
250
+ }
251
+ }
252
+ return true;
253
+ }
254
+ async function verifyAttestationReference(attestation, expectedSubjectDid, allowlistedAttesterDids) {
255
+ const allowlisted = Array.isArray(allowlistedAttesterDids) &&
256
+ allowlistedAttesterDids.includes(attestation.attester_did);
257
+ const subjectValid = attestation.subject_did === expectedSubjectDid;
258
+ const pub = extractPublicKeyFromDidKey(attestation.attester_did);
259
+ if (!pub) {
260
+ return {
261
+ valid: false,
262
+ signature_valid: false,
263
+ allowlisted,
264
+ subject_valid: subjectValid,
265
+ attester_did: attestation.attester_did,
266
+ error: 'Unable to extract Ed25519 public key from attester_did (expected did:key with 0xed01 multicodec prefix)',
267
+ };
268
+ }
269
+ let canonical;
270
+ try {
271
+ const canonicalObject = {
272
+ ...attestation,
273
+ signature_b64u: '',
274
+ };
275
+ canonical = jcsCanonicalize(canonicalObject);
276
+ }
277
+ catch (err) {
278
+ return {
279
+ valid: false,
280
+ signature_valid: false,
281
+ allowlisted,
282
+ subject_valid: subjectValid,
283
+ attester_did: attestation.attester_did,
284
+ error: `Attestation canonicalization failed: ${err instanceof Error ? err.message : 'unknown error'}`,
285
+ };
286
+ }
287
+ let signatureValid = false;
288
+ try {
289
+ const sigBytes = base64UrlDecode(attestation.signature_b64u);
290
+ if (sigBytes.length !== 64) {
291
+ return {
292
+ valid: false,
293
+ signature_valid: false,
294
+ allowlisted,
295
+ subject_valid: subjectValid,
296
+ attester_did: attestation.attester_did,
297
+ error: 'Invalid attestation signature length (expected 64 bytes for Ed25519)',
298
+ };
299
+ }
300
+ const msgBytes = new TextEncoder().encode(canonical);
301
+ signatureValid = await verifySignature('Ed25519', pub, sigBytes, msgBytes);
302
+ }
303
+ catch (err) {
304
+ return {
305
+ valid: false,
306
+ signature_valid: false,
307
+ allowlisted,
308
+ subject_valid: subjectValid,
309
+ attester_did: attestation.attester_did,
310
+ error: `Attestation signature verification error: ${err instanceof Error ? err.message : 'unknown error'}`,
311
+ };
312
+ }
313
+ const valid = signatureValid && allowlisted && subjectValid;
314
+ if (!signatureValid) {
315
+ return {
316
+ valid: false,
317
+ signature_valid: false,
318
+ allowlisted,
319
+ subject_valid: subjectValid,
320
+ attester_did: attestation.attester_did,
321
+ error: 'Attestation signature verification failed',
322
+ };
323
+ }
324
+ if (!allowlisted) {
325
+ return {
326
+ valid: false,
327
+ signature_valid: true,
328
+ allowlisted,
329
+ subject_valid: subjectValid,
330
+ attester_did: attestation.attester_did,
331
+ error: 'Attester DID is not allowlisted',
332
+ };
333
+ }
334
+ if (!subjectValid) {
335
+ return {
336
+ valid: false,
337
+ signature_valid: true,
338
+ allowlisted,
339
+ subject_valid: subjectValid,
340
+ attester_did: attestation.attester_did,
341
+ error: 'Attestation subject_did does not match proof bundle agent_did',
342
+ };
343
+ }
344
+ return {
345
+ valid,
346
+ signature_valid: signatureValid,
347
+ allowlisted,
348
+ subject_valid: subjectValid,
349
+ attester_did: attestation.attester_did,
350
+ };
351
+ }
352
+ /**
353
+ * Verify a gateway receipt envelope cryptographically *and* ensure it is bound
354
+ * to the proof bundle's event chain.
355
+ *
356
+ * Security note (POH-US-010):
357
+ * - A receipt that is signature-valid but not bound to this bundle's run/event
358
+ * chain MUST NOT count toward gateway-tier trust. Otherwise, receipts can be
359
+ * replayed across bundles.
360
+ *
361
+ * Binding rules (fail-closed for counting):
362
+ * - Proof bundle must include a valid event_chain
363
+ * - receipt.payload.binding.run_id must equal the bundle run_id
364
+ * - receipt.payload.binding.event_hash_b64u must reference an event_hash_b64u
365
+ * present in the bundle event_chain
366
+ */
367
+ async function verifyReceiptEnvelope(receipt, allowlistedSignerDids, bindingContext) {
368
+ const verification = await verifyReceipt(receipt, { allowlistedSignerDids });
369
+ if (verification.result.status !== 'VALID') {
370
+ return {
371
+ valid: false,
372
+ signature_valid: false,
373
+ binding_valid: false,
374
+ error: verification.error?.message ?? verification.result.reason,
375
+ };
376
+ }
377
+ if (!bindingContext) {
378
+ return {
379
+ valid: false,
380
+ signature_valid: true,
381
+ binding_valid: false,
382
+ provider: verification.provider,
383
+ model: verification.model,
384
+ gateway_id: verification.gateway_id,
385
+ signer_did: verification.result.signer_did,
386
+ error: 'Receipt binding cannot be verified: proof bundle event_chain is missing or invalid',
387
+ };
388
+ }
389
+ const env = receipt;
390
+ const binding = env.payload.binding;
391
+ if (!binding || typeof binding !== 'object') {
392
+ return {
393
+ valid: false,
394
+ signature_valid: true,
395
+ binding_valid: false,
396
+ provider: verification.provider,
397
+ model: verification.model,
398
+ gateway_id: verification.gateway_id,
399
+ signer_did: verification.result.signer_did,
400
+ error: 'Receipt is missing binding (expected run_id + event_hash_b64u)',
401
+ };
402
+ }
403
+ const runId = binding.run_id;
404
+ const eventHash = binding.event_hash_b64u;
405
+ if (typeof runId !== 'string' || runId.trim().length === 0) {
406
+ return {
407
+ valid: false,
408
+ signature_valid: true,
409
+ binding_valid: false,
410
+ provider: verification.provider,
411
+ model: verification.model,
412
+ gateway_id: verification.gateway_id,
413
+ signer_did: verification.result.signer_did,
414
+ error: 'Receipt binding.run_id is missing or invalid',
415
+ };
416
+ }
417
+ if (runId !== bindingContext.expectedRunId) {
418
+ return {
419
+ valid: false,
420
+ signature_valid: true,
421
+ binding_valid: false,
422
+ provider: verification.provider,
423
+ model: verification.model,
424
+ gateway_id: verification.gateway_id,
425
+ signer_did: verification.result.signer_did,
426
+ error: 'Receipt binding.run_id does not match proof bundle run_id',
427
+ };
428
+ }
429
+ if (typeof eventHash !== 'string' ||
430
+ eventHash.length < 8 ||
431
+ !isValidBase64Url(eventHash)) {
432
+ return {
433
+ valid: false,
434
+ signature_valid: true,
435
+ binding_valid: false,
436
+ provider: verification.provider,
437
+ model: verification.model,
438
+ gateway_id: verification.gateway_id,
439
+ signer_did: verification.result.signer_did,
440
+ error: 'Receipt binding.event_hash_b64u is missing or invalid',
441
+ };
442
+ }
443
+ if (!bindingContext.allowedEventHashes.has(eventHash)) {
444
+ return {
445
+ valid: false,
446
+ signature_valid: true,
447
+ binding_valid: false,
448
+ provider: verification.provider,
449
+ model: verification.model,
450
+ gateway_id: verification.gateway_id,
451
+ signer_did: verification.result.signer_did,
452
+ error: 'Receipt binding.event_hash_b64u does not reference an event in the proof bundle event chain',
453
+ };
454
+ }
455
+ return {
456
+ valid: true,
457
+ signature_valid: true,
458
+ binding_valid: true,
459
+ provider: verification.provider,
460
+ model: verification.model,
461
+ gateway_id: verification.gateway_id,
462
+ signer_did: verification.result.signer_did,
463
+ };
464
+ }
465
+ /**
466
+ * Compute trust tier based on validated components
467
+ *
468
+ * Trust Tier Levels:
469
+ * - unknown: No valid components
470
+ * - basic: Valid envelope signature only
471
+ * - verified: Valid event chain or receipts
472
+ * - attested: Valid allowlisted signature-verified attestations
473
+ * - full: All components valid (URM + events + receipts + attestations)
474
+ */
475
+ function computeTrustTier(components) {
476
+ if (!components.envelope_valid) {
477
+ return 'unknown';
478
+ }
479
+ // Full trust: all components present and valid
480
+ if (components.urm_valid &&
481
+ components.event_chain_valid &&
482
+ components.receipts_valid &&
483
+ components.attestations_valid) {
484
+ return 'full';
485
+ }
486
+ // Attested: has valid attestations
487
+ if (components.attestations_valid) {
488
+ return 'attested';
489
+ }
490
+ // Verified: has valid event chain or receipts
491
+ if (components.event_chain_valid || components.receipts_valid) {
492
+ return 'verified';
493
+ }
494
+ // Basic: envelope is valid but no strong proofs
495
+ return 'basic';
496
+ }
497
+ /**
498
+ * Compute canonical proof tier (marketplace-facing) based on verified components.
499
+ *
500
+ * NOTE: This is intentionally *not* the same as trust_tier. For example, an
501
+ * event_chain-only bundle may be trust_tier=verified but proof_tier=self.
502
+ */
503
+ function computeProofTier(components) {
504
+ if (!components.envelope_valid)
505
+ return 'unknown';
506
+ // Higher tiers win. Proof tiers are based on *at least one* verified component,
507
+ // not on the all-or-nothing `*_valid` booleans.
508
+ if ((components.attestations_verified_count ?? 0) > 0)
509
+ return 'sandbox';
510
+ if ((components.receipts_verified_count ?? 0) > 0)
511
+ return 'gateway';
512
+ return 'self';
513
+ }
514
+ /**
515
+ * Verify a proof bundle envelope
516
+ *
517
+ * Acceptance Criteria:
518
+ * - Validate URM + event chain + receipts + attestations
519
+ * - Fail closed on unknown schema/version
520
+ * - Return computed trust tier
521
+ */
522
+ export async function verifyProofBundle(envelope, options = {}) {
523
+ const now = new Date().toISOString();
524
+ // 1. Validate envelope structure
525
+ if (!validateEnvelopeStructure(envelope)) {
526
+ return {
527
+ result: {
528
+ status: 'INVALID',
529
+ reason: 'Malformed envelope: missing required fields',
530
+ verified_at: now,
531
+ },
532
+ error: {
533
+ code: 'MALFORMED_ENVELOPE',
534
+ message: 'Envelope is missing required fields or has invalid structure',
535
+ },
536
+ };
537
+ }
538
+ // 2. Fail-closed: reject unknown envelope version
539
+ if (!isAllowedVersion(envelope.envelope_version)) {
540
+ return {
541
+ result: {
542
+ status: 'INVALID',
543
+ reason: `Unknown envelope version: ${envelope.envelope_version}`,
544
+ verified_at: now,
545
+ },
546
+ error: {
547
+ code: 'UNKNOWN_ENVELOPE_VERSION',
548
+ message: `Envelope version "${envelope.envelope_version}" is not in the allowlist`,
549
+ field: 'envelope_version',
550
+ },
551
+ };
552
+ }
553
+ // 3. Fail-closed: reject unknown envelope type
554
+ if (!isAllowedType(envelope.envelope_type)) {
555
+ return {
556
+ result: {
557
+ status: 'INVALID',
558
+ reason: `Unknown envelope type: ${envelope.envelope_type}`,
559
+ verified_at: now,
560
+ },
561
+ error: {
562
+ code: 'UNKNOWN_ENVELOPE_TYPE',
563
+ message: `Envelope type "${envelope.envelope_type}" is not in the allowlist`,
564
+ field: 'envelope_type',
565
+ },
566
+ };
567
+ }
568
+ // 4. Verify this is a proof_bundle envelope
569
+ if (envelope.envelope_type !== 'proof_bundle') {
570
+ return {
571
+ result: {
572
+ status: 'INVALID',
573
+ reason: `Expected proof_bundle envelope, got: ${envelope.envelope_type}`,
574
+ verified_at: now,
575
+ },
576
+ error: {
577
+ code: 'UNKNOWN_ENVELOPE_TYPE',
578
+ message: 'This endpoint only accepts proof_bundle envelopes',
579
+ field: 'envelope_type',
580
+ },
581
+ };
582
+ }
583
+ // 5. Fail-closed: reject unknown signature algorithm
584
+ if (!isAllowedAlgorithm(envelope.algorithm)) {
585
+ return {
586
+ result: {
587
+ status: 'INVALID',
588
+ reason: `Unknown signature algorithm: ${envelope.algorithm}`,
589
+ verified_at: now,
590
+ },
591
+ error: {
592
+ code: 'UNKNOWN_ALGORITHM',
593
+ message: `Signature algorithm "${envelope.algorithm}" is not in the allowlist`,
594
+ field: 'algorithm',
595
+ },
596
+ };
597
+ }
598
+ // 6. Fail-closed: reject unknown hash algorithm
599
+ if (!isAllowedHashAlgorithm(envelope.hash_algorithm)) {
600
+ return {
601
+ result: {
602
+ status: 'INVALID',
603
+ reason: `Unknown hash algorithm: ${envelope.hash_algorithm}`,
604
+ verified_at: now,
605
+ },
606
+ error: {
607
+ code: 'UNKNOWN_HASH_ALGORITHM',
608
+ message: `Hash algorithm "${envelope.hash_algorithm}" is not in the allowlist`,
609
+ field: 'hash_algorithm',
610
+ },
611
+ };
612
+ }
613
+ // 7. Validate DID format
614
+ if (!isValidDidFormat(envelope.signer_did)) {
615
+ return {
616
+ result: {
617
+ status: 'INVALID',
618
+ reason: `Invalid DID format: ${envelope.signer_did}`,
619
+ verified_at: now,
620
+ },
621
+ error: {
622
+ code: 'INVALID_DID_FORMAT',
623
+ message: 'Signer DID does not match expected format (did:key:... or did:web:...)',
624
+ field: 'signer_did',
625
+ },
626
+ };
627
+ }
628
+ // 8. Validate issued_at format
629
+ if (!isValidIsoDate(envelope.issued_at)) {
630
+ return {
631
+ result: {
632
+ status: 'INVALID',
633
+ reason: 'Invalid issued_at date format',
634
+ verified_at: now,
635
+ },
636
+ error: {
637
+ code: 'MALFORMED_ENVELOPE',
638
+ message: 'issued_at must be a valid ISO 8601 date string',
639
+ field: 'issued_at',
640
+ },
641
+ };
642
+ }
643
+ // 9. Validate base64url fields
644
+ if (!isValidBase64Url(envelope.payload_hash_b64u)) {
645
+ return {
646
+ result: {
647
+ status: 'INVALID',
648
+ reason: 'Invalid payload_hash_b64u format',
649
+ verified_at: now,
650
+ },
651
+ error: {
652
+ code: 'MALFORMED_ENVELOPE',
653
+ message: 'payload_hash_b64u must be a valid base64url string',
654
+ field: 'payload_hash_b64u',
655
+ },
656
+ };
657
+ }
658
+ if (!isValidBase64Url(envelope.signature_b64u)) {
659
+ return {
660
+ result: {
661
+ status: 'INVALID',
662
+ reason: 'Invalid signature_b64u format',
663
+ verified_at: now,
664
+ },
665
+ error: {
666
+ code: 'MALFORMED_ENVELOPE',
667
+ message: 'signature_b64u must be a valid base64url string',
668
+ field: 'signature_b64u',
669
+ },
670
+ };
671
+ }
672
+ // 9.75 Strict JSON schema validation (Ajv) for envelope + payload
673
+ // CVF-US-024: Fail closed on schema violations (additionalProperties:false, missing fields, etc.)
674
+ const schemaResult = validateProofBundleEnvelopeV1(envelope);
675
+ if (!schemaResult.valid) {
676
+ return {
677
+ result: {
678
+ status: 'INVALID',
679
+ reason: schemaResult.message,
680
+ verified_at: now,
681
+ },
682
+ error: {
683
+ code: 'SCHEMA_VALIDATION_FAILED',
684
+ message: schemaResult.message,
685
+ field: schemaResult.field,
686
+ },
687
+ };
688
+ }
689
+ // 10. Validate proof bundle payload structure against PoH schema
690
+ const payloadValidation = validateBundlePayload(envelope.payload);
691
+ if (!payloadValidation.valid) {
692
+ return {
693
+ result: {
694
+ status: 'INVALID',
695
+ reason: `Invalid proof bundle payload: ${payloadValidation.error}`,
696
+ verified_at: now,
697
+ },
698
+ error: {
699
+ code: 'MALFORMED_ENVELOPE',
700
+ message: payloadValidation.error ?? 'Proof bundle payload is missing required fields or has no components',
701
+ field: 'payload',
702
+ },
703
+ };
704
+ }
705
+ // Type assertion after schema validation
706
+ if (!isBundlePayload(envelope.payload)) {
707
+ return {
708
+ result: {
709
+ status: 'INVALID',
710
+ reason: 'Invalid proof bundle payload structure',
711
+ verified_at: now,
712
+ },
713
+ error: {
714
+ code: 'MALFORMED_ENVELOPE',
715
+ message: 'Proof bundle payload failed type guard after schema validation',
716
+ field: 'payload',
717
+ },
718
+ };
719
+ }
720
+ // CVF-US-025: enforce count/size limits and uniqueness constraints (fail-closed)
721
+ const p = envelope.payload;
722
+ if (p.event_chain && p.event_chain.length > MAX_EVENT_CHAIN_ENTRIES) {
723
+ return {
724
+ result: {
725
+ status: 'INVALID',
726
+ reason: `event_chain exceeds max length (${MAX_EVENT_CHAIN_ENTRIES})`,
727
+ verified_at: now,
728
+ },
729
+ error: {
730
+ code: 'MALFORMED_ENVELOPE',
731
+ message: `payload.event_chain length exceeds limit (${MAX_EVENT_CHAIN_ENTRIES})`,
732
+ field: 'payload.event_chain',
733
+ },
734
+ };
735
+ }
736
+ if (p.receipts && p.receipts.length > MAX_RECEIPTS) {
737
+ return {
738
+ result: {
739
+ status: 'INVALID',
740
+ reason: `receipts exceeds max length (${MAX_RECEIPTS})`,
741
+ verified_at: now,
742
+ },
743
+ error: {
744
+ code: 'MALFORMED_ENVELOPE',
745
+ message: `payload.receipts length exceeds limit (${MAX_RECEIPTS})`,
746
+ field: 'payload.receipts',
747
+ },
748
+ };
749
+ }
750
+ if (p.attestations && p.attestations.length > MAX_ATTESTATIONS) {
751
+ return {
752
+ result: {
753
+ status: 'INVALID',
754
+ reason: `attestations exceeds max length (${MAX_ATTESTATIONS})`,
755
+ verified_at: now,
756
+ },
757
+ error: {
758
+ code: 'MALFORMED_ENVELOPE',
759
+ message: `payload.attestations length exceeds limit (${MAX_ATTESTATIONS})`,
760
+ field: 'payload.attestations',
761
+ },
762
+ };
763
+ }
764
+ // Metadata byte-size limits (metadata objects are intentionally flexible; bound size to prevent DoS)
765
+ if (p.metadata && jsonByteSize(p.metadata) > MAX_METADATA_BYTES) {
766
+ return {
767
+ result: {
768
+ status: 'INVALID',
769
+ reason: `payload.metadata exceeds max size (${MAX_METADATA_BYTES} bytes)`,
770
+ verified_at: now,
771
+ },
772
+ error: {
773
+ code: 'MALFORMED_ENVELOPE',
774
+ message: `payload.metadata exceeds max size (${MAX_METADATA_BYTES} bytes)`,
775
+ field: 'payload.metadata',
776
+ },
777
+ };
778
+ }
779
+ if (p.urm?.metadata && jsonByteSize(p.urm.metadata) > MAX_METADATA_BYTES) {
780
+ return {
781
+ result: {
782
+ status: 'INVALID',
783
+ reason: `payload.urm.metadata exceeds max size (${MAX_METADATA_BYTES} bytes)`,
784
+ verified_at: now,
785
+ },
786
+ error: {
787
+ code: 'MALFORMED_ENVELOPE',
788
+ message: `payload.urm.metadata exceeds max size (${MAX_METADATA_BYTES} bytes)`,
789
+ field: 'payload.urm.metadata',
790
+ },
791
+ };
792
+ }
793
+ if (p.receipts) {
794
+ for (let i = 0; i < p.receipts.length; i++) {
795
+ const md = p.receipts[i].payload.metadata;
796
+ if (md !== undefined && jsonByteSize(md) > MAX_METADATA_BYTES) {
797
+ return {
798
+ result: {
799
+ status: 'INVALID',
800
+ reason: `payload.receipts[${i}].payload.metadata exceeds max size (${MAX_METADATA_BYTES} bytes)`,
801
+ verified_at: now,
802
+ },
803
+ error: {
804
+ code: 'MALFORMED_ENVELOPE',
805
+ message: `receipt metadata exceeds max size (${MAX_METADATA_BYTES} bytes)`,
806
+ field: `payload.receipts[${i}].payload.metadata`,
807
+ },
808
+ };
809
+ }
810
+ }
811
+ }
812
+ // Uniqueness constraints within a bundle
813
+ if (p.event_chain) {
814
+ const seenEventIds = new Set();
815
+ for (let i = 0; i < p.event_chain.length; i++) {
816
+ const id = p.event_chain[i].event_id;
817
+ if (seenEventIds.has(id)) {
818
+ return {
819
+ result: {
820
+ status: 'INVALID',
821
+ reason: 'Duplicate event_id in payload.event_chain',
822
+ verified_at: now,
823
+ },
824
+ error: {
825
+ code: 'MALFORMED_ENVELOPE',
826
+ message: 'event_id must be unique within payload.event_chain',
827
+ field: `payload.event_chain[${i}].event_id`,
828
+ },
829
+ };
830
+ }
831
+ seenEventIds.add(id);
832
+ }
833
+ }
834
+ if (p.receipts) {
835
+ const seenReceiptIds = new Set();
836
+ for (let i = 0; i < p.receipts.length; i++) {
837
+ const rid = p.receipts[i].payload.receipt_id;
838
+ if (seenReceiptIds.has(rid)) {
839
+ return {
840
+ result: {
841
+ status: 'INVALID',
842
+ reason: 'Duplicate receipt_id in payload.receipts',
843
+ verified_at: now,
844
+ },
845
+ error: {
846
+ code: 'MALFORMED_ENVELOPE',
847
+ message: 'receipt_id must be unique within payload.receipts',
848
+ field: `payload.receipts[${i}].payload.receipt_id`,
849
+ },
850
+ };
851
+ }
852
+ seenReceiptIds.add(rid);
853
+ }
854
+ }
855
+ // 11. Validate agent_did in payload matches expected format
856
+ if (!isValidDidFormat(envelope.payload.agent_did)) {
857
+ return {
858
+ result: {
859
+ status: 'INVALID',
860
+ reason: `Invalid agent_did format: ${envelope.payload.agent_did}`,
861
+ verified_at: now,
862
+ },
863
+ error: {
864
+ code: 'INVALID_DID_FORMAT',
865
+ message: 'agent_did does not match expected DID format',
866
+ field: 'payload.agent_did',
867
+ },
868
+ };
869
+ }
870
+ // CVF-US-022: Enforce envelope signer DID equals payload agent DID
871
+ if (envelope.signer_did !== envelope.payload.agent_did) {
872
+ return {
873
+ result: {
874
+ status: 'INVALID',
875
+ reason: 'Proof bundle signer_did must match payload.agent_did',
876
+ verified_at: now,
877
+ },
878
+ error: {
879
+ code: 'INVALID_DID_FORMAT',
880
+ message: 'envelope.signer_did must equal payload.agent_did',
881
+ field: 'signer_did',
882
+ },
883
+ };
884
+ }
885
+ // 12. Recompute hash and verify it matches
886
+ try {
887
+ const computedHash = await computeHash(envelope.payload, envelope.hash_algorithm);
888
+ if (computedHash !== envelope.payload_hash_b64u) {
889
+ return {
890
+ result: {
891
+ status: 'INVALID',
892
+ reason: 'Payload hash mismatch: envelope may have been tampered with',
893
+ verified_at: now,
894
+ },
895
+ error: {
896
+ code: 'HASH_MISMATCH',
897
+ message: 'Computed payload hash does not match envelope hash',
898
+ },
899
+ };
900
+ }
901
+ }
902
+ catch (err) {
903
+ return {
904
+ result: {
905
+ status: 'INVALID',
906
+ reason: `Hash computation failed: ${err instanceof Error ? err.message : 'unknown error'}`,
907
+ verified_at: now,
908
+ },
909
+ error: {
910
+ code: 'HASH_MISMATCH',
911
+ message: 'Failed to compute payload hash',
912
+ },
913
+ };
914
+ }
915
+ // 13. Extract public key from DID
916
+ const publicKeyBytes = extractPublicKeyFromDidKey(envelope.signer_did);
917
+ if (!publicKeyBytes) {
918
+ return {
919
+ result: {
920
+ status: 'INVALID',
921
+ reason: 'Could not extract public key from signer DID',
922
+ verified_at: now,
923
+ },
924
+ error: {
925
+ code: 'INVALID_DID_FORMAT',
926
+ message: 'Unable to extract Ed25519 public key from did:key. Ensure the DID uses the Ed25519 multicodec prefix.',
927
+ field: 'signer_did',
928
+ },
929
+ };
930
+ }
931
+ // 14. Verify envelope signature
932
+ try {
933
+ const signatureBytes = base64UrlDecode(envelope.signature_b64u);
934
+ const messageBytes = new TextEncoder().encode(envelope.payload_hash_b64u);
935
+ const isValid = await verifySignature(envelope.algorithm, publicKeyBytes, signatureBytes, messageBytes);
936
+ if (!isValid) {
937
+ return {
938
+ result: {
939
+ status: 'INVALID',
940
+ reason: 'Signature verification failed',
941
+ verified_at: now,
942
+ },
943
+ error: {
944
+ code: 'SIGNATURE_INVALID',
945
+ message: 'The Ed25519 signature does not match the payload hash',
946
+ },
947
+ };
948
+ }
949
+ }
950
+ catch (err) {
951
+ return {
952
+ result: {
953
+ status: 'INVALID',
954
+ reason: `Signature verification error: ${err instanceof Error ? err.message : 'unknown error'}`,
955
+ verified_at: now,
956
+ },
957
+ error: {
958
+ code: 'SIGNATURE_INVALID',
959
+ message: 'Failed to verify signature',
960
+ },
961
+ };
962
+ }
963
+ // 15. Validate individual components
964
+ const payload = envelope.payload;
965
+ const componentResults = {
966
+ envelope_valid: true,
967
+ };
968
+ // CVF-US-016: model identity is an orthogonal axis to PoH tiers.
969
+ let modelIdentityTier = 'unknown';
970
+ const modelIdentityRiskFlags = new Set();
971
+ // Validate event chain if present (verify hash linkage per POH-US-003)
972
+ if (payload.event_chain !== undefined && payload.event_chain.length > 0) {
973
+ const chainResult = validateEventChain(payload.event_chain);
974
+ if (!chainResult.valid) {
975
+ return {
976
+ result: {
977
+ status: 'INVALID',
978
+ reason: chainResult.error ?? 'Event chain validation failed',
979
+ verified_at: now,
980
+ },
981
+ error: {
982
+ code: 'MALFORMED_ENVELOPE',
983
+ message: chainResult.error ?? 'Invalid event_chain',
984
+ field: 'payload.event_chain',
985
+ },
986
+ };
987
+ }
988
+ // CVF-US-021: Recompute event_hash_b64u from canonical event headers (fail-closed)
989
+ // Canonical header key order per ADAPTER_SPEC_v1 §4.2.
990
+ for (let i = 0; i < payload.event_chain.length; i++) {
991
+ const e = payload.event_chain[i];
992
+ const canonical = {
993
+ event_id: e.event_id,
994
+ run_id: e.run_id,
995
+ event_type: e.event_type,
996
+ timestamp: e.timestamp,
997
+ payload_hash_b64u: e.payload_hash_b64u,
998
+ prev_hash_b64u: e.prev_hash_b64u ?? null,
999
+ };
1000
+ let expectedHash;
1001
+ try {
1002
+ expectedHash = await computeHash(canonical, 'SHA-256');
1003
+ }
1004
+ catch (err) {
1005
+ return {
1006
+ result: {
1007
+ status: 'INVALID',
1008
+ reason: `Event ${i}: event hash recomputation failed`,
1009
+ verified_at: now,
1010
+ },
1011
+ error: {
1012
+ code: 'HASH_MISMATCH',
1013
+ message: `Failed to recompute event hash: ${err instanceof Error ? err.message : 'unknown error'}`,
1014
+ field: `payload.event_chain[${i}]`,
1015
+ },
1016
+ };
1017
+ }
1018
+ if (expectedHash !== e.event_hash_b64u) {
1019
+ return {
1020
+ result: {
1021
+ status: 'INVALID',
1022
+ reason: `Event ${i}: event_hash_b64u mismatch`,
1023
+ verified_at: now,
1024
+ },
1025
+ error: {
1026
+ code: 'HASH_MISMATCH',
1027
+ message: 'event_hash_b64u does not match SHA-256 hash of the canonical event header',
1028
+ field: `payload.event_chain[${i}].event_hash_b64u`,
1029
+ },
1030
+ };
1031
+ }
1032
+ }
1033
+ componentResults.event_chain_valid = true;
1034
+ if (chainResult.chain_root_hash) {
1035
+ componentResults.chain_root_hash = chainResult.chain_root_hash;
1036
+ }
1037
+ }
1038
+ // POH-US-016/017: Prompt commitments (optional; fail-closed when present)
1039
+ //
1040
+ // These are *hash-only* objects carried in payload.metadata that commit to:
1041
+ // - prompt pack inputs (prompt_pack.prompt_root_hash_b64u)
1042
+ // - per-llm_call rendered system prompt hashes (system_prompt_report)
1043
+ //
1044
+ // They do not uplift proof tier; they are evidence for replay/audit safety.
1045
+ let promptPackRootHashB64u = null;
1046
+ const md = payload.metadata;
1047
+ const mdRecord = md && typeof md === 'object' && md !== null && !Array.isArray(md)
1048
+ ? md
1049
+ : null;
1050
+ const promptPackRaw = mdRecord ? mdRecord.prompt_pack : undefined;
1051
+ if (promptPackRaw !== undefined) {
1052
+ const schemaResult = validatePromptPackV1(promptPackRaw);
1053
+ if (!schemaResult.valid) {
1054
+ return {
1055
+ result: {
1056
+ status: 'INVALID',
1057
+ reason: schemaResult.message,
1058
+ verified_at: now,
1059
+ },
1060
+ error: {
1061
+ code: 'SCHEMA_VALIDATION_FAILED',
1062
+ message: schemaResult.message,
1063
+ field: schemaResult.field
1064
+ ? `payload.metadata.prompt_pack.${schemaResult.field}`
1065
+ : 'payload.metadata.prompt_pack',
1066
+ },
1067
+ };
1068
+ }
1069
+ const pp = promptPackRaw;
1070
+ const claimed = typeof pp.prompt_root_hash_b64u === 'string' ? pp.prompt_root_hash_b64u.trim() : null;
1071
+ const entries = Array.isArray(pp.entries) ? pp.entries : [];
1072
+ const canonicalEntries = entries
1073
+ .filter((e) => typeof e === 'object' && e !== null && !Array.isArray(e))
1074
+ .map((e) => {
1075
+ const er = e;
1076
+ return {
1077
+ entry_id: typeof er.entry_id === 'string' ? er.entry_id.trim() : '',
1078
+ content_hash_b64u: typeof er.content_hash_b64u === 'string' ? er.content_hash_b64u.trim() : '',
1079
+ };
1080
+ })
1081
+ .filter((e) => e.entry_id.length > 0 && e.content_hash_b64u.length > 0)
1082
+ .sort((a, b) => a.entry_id.localeCompare(b.entry_id));
1083
+ const canonical = {
1084
+ prompt_pack_version: '1',
1085
+ entries: canonicalEntries,
1086
+ };
1087
+ let computed;
1088
+ try {
1089
+ computed = await computeHash(canonical, 'SHA-256');
1090
+ }
1091
+ catch (err) {
1092
+ return {
1093
+ result: {
1094
+ status: 'INVALID',
1095
+ reason: 'Failed to compute prompt_pack root hash',
1096
+ verified_at: now,
1097
+ },
1098
+ error: {
1099
+ code: 'HASH_MISMATCH',
1100
+ message: `Failed to compute prompt_pack prompt_root_hash_b64u: ${err instanceof Error ? err.message : 'unknown error'}`,
1101
+ field: 'payload.metadata.prompt_pack',
1102
+ },
1103
+ };
1104
+ }
1105
+ if (!claimed || claimed !== computed) {
1106
+ return {
1107
+ result: {
1108
+ status: 'INVALID',
1109
+ reason: 'prompt_pack.prompt_root_hash_b64u mismatch',
1110
+ verified_at: now,
1111
+ },
1112
+ error: {
1113
+ code: 'HASH_MISMATCH',
1114
+ message: 'prompt_root_hash_b64u does not match canonical entry list hash',
1115
+ field: 'payload.metadata.prompt_pack.prompt_root_hash_b64u',
1116
+ },
1117
+ };
1118
+ }
1119
+ promptPackRootHashB64u = claimed;
1120
+ componentResults.prompt_pack_valid = true;
1121
+ }
1122
+ const systemPromptReportRaw = mdRecord ? mdRecord.system_prompt_report : undefined;
1123
+ if (systemPromptReportRaw !== undefined) {
1124
+ const schemaResult = validateSystemPromptReportV1(systemPromptReportRaw);
1125
+ if (!schemaResult.valid) {
1126
+ return {
1127
+ result: {
1128
+ status: 'INVALID',
1129
+ reason: schemaResult.message,
1130
+ verified_at: now,
1131
+ },
1132
+ error: {
1133
+ code: 'SCHEMA_VALIDATION_FAILED',
1134
+ message: schemaResult.message,
1135
+ field: schemaResult.field
1136
+ ? `payload.metadata.system_prompt_report.${schemaResult.field}`
1137
+ : 'payload.metadata.system_prompt_report',
1138
+ },
1139
+ };
1140
+ }
1141
+ if (!payload.event_chain || payload.event_chain.length === 0) {
1142
+ return {
1143
+ result: {
1144
+ status: 'INVALID',
1145
+ reason: 'system_prompt_report requires payload.event_chain',
1146
+ verified_at: now,
1147
+ },
1148
+ error: {
1149
+ code: 'MALFORMED_ENVELOPE',
1150
+ message: 'payload.event_chain is required when payload.metadata.system_prompt_report is present',
1151
+ field: 'payload.event_chain',
1152
+ },
1153
+ };
1154
+ }
1155
+ const spr = systemPromptReportRaw;
1156
+ const sprRunId = typeof spr.run_id === 'string' ? spr.run_id.trim() : null;
1157
+ const sprAgentDid = typeof spr.agent_did === 'string' ? spr.agent_did.trim() : null;
1158
+ const expectedRunId = payload.event_chain[0].run_id;
1159
+ if (!sprRunId || sprRunId !== expectedRunId) {
1160
+ return {
1161
+ result: {
1162
+ status: 'INVALID',
1163
+ reason: 'system_prompt_report.run_id mismatch',
1164
+ verified_at: now,
1165
+ },
1166
+ error: {
1167
+ code: 'PROMPT_COMMITMENT_MISMATCH',
1168
+ message: 'system_prompt_report.run_id must equal payload.event_chain[0].run_id',
1169
+ field: 'payload.metadata.system_prompt_report.run_id',
1170
+ },
1171
+ };
1172
+ }
1173
+ if (!sprAgentDid || sprAgentDid !== payload.agent_did) {
1174
+ return {
1175
+ result: {
1176
+ status: 'INVALID',
1177
+ reason: 'system_prompt_report.agent_did mismatch',
1178
+ verified_at: now,
1179
+ },
1180
+ error: {
1181
+ code: 'PROMPT_COMMITMENT_MISMATCH',
1182
+ message: 'system_prompt_report.agent_did must equal payload.agent_did',
1183
+ field: 'payload.metadata.system_prompt_report.agent_did',
1184
+ },
1185
+ };
1186
+ }
1187
+ const sprPromptRoot = typeof spr.prompt_root_hash_b64u === 'string' ? spr.prompt_root_hash_b64u.trim() : null;
1188
+ if (promptPackRootHashB64u && sprPromptRoot && sprPromptRoot !== promptPackRootHashB64u) {
1189
+ return {
1190
+ result: {
1191
+ status: 'INVALID',
1192
+ reason: 'system_prompt_report.prompt_root_hash_b64u mismatch',
1193
+ verified_at: now,
1194
+ },
1195
+ error: {
1196
+ code: 'PROMPT_COMMITMENT_MISMATCH',
1197
+ message: 'system_prompt_report.prompt_root_hash_b64u must match prompt_pack.prompt_root_hash_b64u (when both present)',
1198
+ field: 'payload.metadata.system_prompt_report.prompt_root_hash_b64u',
1199
+ },
1200
+ };
1201
+ }
1202
+ const eventsById = new Map(payload.event_chain.map((e) => [e.event_id, e]));
1203
+ const calls = Array.isArray(spr.calls) ? spr.calls : [];
1204
+ for (let i = 0; i < calls.length; i++) {
1205
+ const c = calls[i];
1206
+ if (typeof c !== 'object' || c === null || Array.isArray(c))
1207
+ continue;
1208
+ const cr = c;
1209
+ const eventId = typeof cr.event_id === 'string' ? cr.event_id.trim() : null;
1210
+ if (!eventId)
1211
+ continue;
1212
+ const evt = eventsById.get(eventId);
1213
+ if (!evt) {
1214
+ return {
1215
+ result: {
1216
+ status: 'INVALID',
1217
+ reason: 'system_prompt_report references unknown event_id',
1218
+ verified_at: now,
1219
+ },
1220
+ error: {
1221
+ code: 'PROMPT_COMMITMENT_MISMATCH',
1222
+ message: 'system_prompt_report.calls[*].event_id must refer to an event in payload.event_chain',
1223
+ field: `payload.metadata.system_prompt_report.calls[${i}].event_id`,
1224
+ },
1225
+ };
1226
+ }
1227
+ if (evt.event_type !== 'llm_call') {
1228
+ return {
1229
+ result: {
1230
+ status: 'INVALID',
1231
+ reason: 'system_prompt_report references a non-llm_call event',
1232
+ verified_at: now,
1233
+ },
1234
+ error: {
1235
+ code: 'PROMPT_COMMITMENT_MISMATCH',
1236
+ message: 'system_prompt_report.calls[*] must reference llm_call events',
1237
+ field: `payload.metadata.system_prompt_report.calls[${i}].event_id`,
1238
+ },
1239
+ };
1240
+ }
1241
+ const claimedEventHash = typeof cr.event_hash_b64u === 'string' ? cr.event_hash_b64u.trim() : null;
1242
+ if (claimedEventHash && claimedEventHash !== evt.event_hash_b64u) {
1243
+ return {
1244
+ result: {
1245
+ status: 'INVALID',
1246
+ reason: 'system_prompt_report event_hash_b64u mismatch',
1247
+ verified_at: now,
1248
+ },
1249
+ error: {
1250
+ code: 'PROMPT_COMMITMENT_MISMATCH',
1251
+ message: 'system_prompt_report.calls[*].event_hash_b64u must match payload.event_chain[event_id].event_hash_b64u',
1252
+ field: `payload.metadata.system_prompt_report.calls[${i}].event_hash_b64u`,
1253
+ },
1254
+ };
1255
+ }
1256
+ }
1257
+ componentResults.system_prompt_report_valid = true;
1258
+ }
1259
+ // POH-US-015: URM materialization + hash verification.
1260
+ // Proof bundles carry only a URM *reference* (hash). To make that meaningful,
1261
+ // callers may provide the materialized URM document bytes (as a JSON object).
1262
+ //
1263
+ // Fail-closed semantics:
1264
+ // - If URM reference is present but URM bytes are not provided, the bundle is INVALID.
1265
+ // - If URM bytes are provided but fail schema validation, binding checks, or hash verification,
1266
+ // the bundle is INVALID.
1267
+ if (payload.urm !== undefined) {
1268
+ if (!validateURM(payload.urm)) {
1269
+ componentResults.urm_valid = false;
1270
+ }
1271
+ else if (options.urm === undefined) {
1272
+ return {
1273
+ result: {
1274
+ status: 'INVALID',
1275
+ reason: 'URM document is required when payload.urm is present',
1276
+ verified_at: now,
1277
+ },
1278
+ error: {
1279
+ code: 'URM_MISSING',
1280
+ message: 'Missing URM document (provide request field: urm)',
1281
+ field: 'urm',
1282
+ },
1283
+ };
1284
+ }
1285
+ else {
1286
+ const ref = payload.urm;
1287
+ const schemaResult = validateUrmV1(options.urm);
1288
+ if (!schemaResult.valid) {
1289
+ return {
1290
+ result: {
1291
+ status: 'INVALID',
1292
+ reason: schemaResult.message,
1293
+ verified_at: now,
1294
+ },
1295
+ error: {
1296
+ code: 'SCHEMA_VALIDATION_FAILED',
1297
+ message: schemaResult.message,
1298
+ field: schemaResult.field ? `urm.${schemaResult.field}` : 'urm',
1299
+ },
1300
+ };
1301
+ }
1302
+ const u = options.urm;
1303
+ // Binding checks (fail-closed): URM must describe the same run/agent.
1304
+ const urmId = typeof u.urm_id === 'string' ? u.urm_id.trim() : null;
1305
+ const agentDid = typeof u.agent_did === 'string' ? u.agent_did.trim() : null;
1306
+ const runId = typeof u.run_id === 'string' ? u.run_id.trim() : null;
1307
+ if (!urmId || urmId !== ref.urm_id) {
1308
+ return {
1309
+ result: {
1310
+ status: 'INVALID',
1311
+ reason: 'URM urm_id does not match proof bundle URM reference',
1312
+ verified_at: now,
1313
+ },
1314
+ error: {
1315
+ code: 'URM_MISMATCH',
1316
+ message: 'urm.urm_id must equal payload.urm.urm_id',
1317
+ field: 'urm.urm_id',
1318
+ },
1319
+ };
1320
+ }
1321
+ if (!agentDid || agentDid !== payload.agent_did) {
1322
+ return {
1323
+ result: {
1324
+ status: 'INVALID',
1325
+ reason: 'URM agent_did does not match proof bundle agent_did',
1326
+ verified_at: now,
1327
+ },
1328
+ error: {
1329
+ code: 'URM_MISMATCH',
1330
+ message: 'urm.agent_did must equal payload.agent_did',
1331
+ field: 'urm.agent_did',
1332
+ },
1333
+ };
1334
+ }
1335
+ if (componentResults.event_chain_valid && payload.event_chain && payload.event_chain.length > 0) {
1336
+ const expectedRunId = payload.event_chain[0].run_id;
1337
+ if (!runId || runId !== expectedRunId) {
1338
+ return {
1339
+ result: {
1340
+ status: 'INVALID',
1341
+ reason: 'URM run_id does not match proof bundle run_id',
1342
+ verified_at: now,
1343
+ },
1344
+ error: {
1345
+ code: 'URM_MISMATCH',
1346
+ message: 'urm.run_id must equal payload.event_chain[0].run_id',
1347
+ field: 'urm.run_id',
1348
+ },
1349
+ };
1350
+ }
1351
+ const chainRoot = componentResults.chain_root_hash;
1352
+ const claimedRoot = typeof u.event_chain_root_hash_b64u === 'string' ? u.event_chain_root_hash_b64u.trim() : null;
1353
+ if (chainRoot && claimedRoot && claimedRoot !== chainRoot) {
1354
+ return {
1355
+ result: {
1356
+ status: 'INVALID',
1357
+ reason: 'URM event_chain_root_hash_b64u does not match proof bundle event chain root',
1358
+ verified_at: now,
1359
+ },
1360
+ error: {
1361
+ code: 'URM_MISMATCH',
1362
+ message: 'urm.event_chain_root_hash_b64u must match the proof bundle event chain root hash',
1363
+ field: 'urm.event_chain_root_hash_b64u',
1364
+ },
1365
+ };
1366
+ }
1367
+ }
1368
+ let computedUrmHash;
1369
+ try {
1370
+ computedUrmHash = await computeHash(options.urm, 'SHA-256');
1371
+ }
1372
+ catch (err) {
1373
+ return {
1374
+ result: {
1375
+ status: 'INVALID',
1376
+ reason: 'Failed to hash URM document',
1377
+ verified_at: now,
1378
+ },
1379
+ error: {
1380
+ code: 'HASH_MISMATCH',
1381
+ message: `Failed to compute URM hash: ${err instanceof Error ? err.message : 'unknown error'}`,
1382
+ field: 'urm',
1383
+ },
1384
+ };
1385
+ }
1386
+ if (computedUrmHash !== ref.resource_hash_b64u) {
1387
+ return {
1388
+ result: {
1389
+ status: 'INVALID',
1390
+ reason: 'URM hash mismatch',
1391
+ verified_at: now,
1392
+ },
1393
+ error: {
1394
+ code: 'HASH_MISMATCH',
1395
+ message: 'Computed URM hash does not match payload.urm.resource_hash_b64u',
1396
+ field: 'payload.urm.resource_hash_b64u',
1397
+ },
1398
+ };
1399
+ }
1400
+ componentResults.urm_valid = true;
1401
+ }
1402
+ }
1403
+ // Verify receipt envelopes cryptographically (POH-US-003)
1404
+ // Each receipt is verified with its signer DID (clawproxy DID) using full
1405
+ // signature verification — not just structural validation.
1406
+ if (payload.receipts !== undefined && payload.receipts.length > 0) {
1407
+ // POH-US-010: Require receipts to be bound to this bundle's event chain.
1408
+ // Without binding, a signature-valid receipt could be replayed across bundles.
1409
+ const bindingContext = componentResults.event_chain_valid &&
1410
+ payload.event_chain !== undefined &&
1411
+ payload.event_chain.length > 0
1412
+ ? {
1413
+ expectedRunId: payload.event_chain[0].run_id,
1414
+ allowedEventHashes: new Set(payload.event_chain.map((e) => e.event_hash_b64u)),
1415
+ }
1416
+ : null;
1417
+ const receiptResults = await Promise.all(payload.receipts.map((r) => verifyReceiptEnvelope(r, options.allowlistedReceiptSignerDids, bindingContext)));
1418
+ const signatureValidCount = receiptResults.filter((r) => r.signature_valid).length;
1419
+ const boundValidCount = receiptResults.filter((r) => r.valid).length;
1420
+ componentResults.receipts_valid = boundValidCount === payload.receipts.length;
1421
+ componentResults.receipts_count = payload.receipts.length;
1422
+ componentResults.receipts_signature_verified_count = signatureValidCount;
1423
+ componentResults.receipts_verified_count = boundValidCount;
1424
+ // CVF-US-016: Extract + verify model identity and compute an overall tier.
1425
+ try {
1426
+ const modelIdentity = await computeModelIdentityTierFromReceipts({
1427
+ receipts: payload.receipts,
1428
+ receiptResults,
1429
+ });
1430
+ modelIdentityTier = modelIdentity.model_identity_tier;
1431
+ for (const f of modelIdentity.risk_flags)
1432
+ modelIdentityRiskFlags.add(f);
1433
+ }
1434
+ catch {
1435
+ modelIdentityTier = 'unknown';
1436
+ modelIdentityRiskFlags.add('MODEL_IDENTITY_VERIFY_FAILED');
1437
+ }
1438
+ }
1439
+ // Validate tool receipts when present (CPL-US-006: fail-closed on unknown schema)
1440
+ if (payload.tool_receipts !== undefined) {
1441
+ const toolReceipts = payload.tool_receipts;
1442
+ if (!Array.isArray(toolReceipts)) {
1443
+ return {
1444
+ result: {
1445
+ status: 'INVALID',
1446
+ reason: 'tool_receipts must be an array',
1447
+ verified_at: now,
1448
+ },
1449
+ error: {
1450
+ code: 'SCHEMA_VALIDATION_FAILED',
1451
+ message: 'payload.tool_receipts must be an array',
1452
+ field: 'payload.tool_receipts',
1453
+ },
1454
+ };
1455
+ }
1456
+ const REQUIRED_TOOL_RECEIPT_FIELDS = [
1457
+ 'receipt_version', 'receipt_id', 'tool_name', 'args_hash_b64u',
1458
+ 'result_hash_b64u', 'hash_algorithm', 'agent_did', 'timestamp', 'latency_ms',
1459
+ ];
1460
+ for (let i = 0; i < toolReceipts.length; i++) {
1461
+ const tr = toolReceipts[i];
1462
+ if (typeof tr !== 'object' || tr === null || Array.isArray(tr)) {
1463
+ return {
1464
+ result: { status: 'INVALID', reason: `tool_receipts[${i}] must be an object`, verified_at: now },
1465
+ error: { code: 'SCHEMA_VALIDATION_FAILED', message: `tool_receipts[${i}] must be an object`, field: `payload.tool_receipts[${i}]` },
1466
+ };
1467
+ }
1468
+ const rec = tr;
1469
+ // Fail-closed: unknown receipt_version
1470
+ if (rec.receipt_version !== '1') {
1471
+ return {
1472
+ result: { status: 'INVALID', reason: `tool_receipts[${i}]: unknown receipt_version`, verified_at: now },
1473
+ error: { code: 'UNKNOWN_VERSION', message: `tool_receipts[${i}].receipt_version must be "1"`, field: `payload.tool_receipts[${i}].receipt_version` },
1474
+ };
1475
+ }
1476
+ // Fail-closed: unknown hash algorithm
1477
+ if (rec.hash_algorithm !== 'SHA-256') {
1478
+ return {
1479
+ result: { status: 'INVALID', reason: `tool_receipts[${i}]: unknown hash_algorithm`, verified_at: now },
1480
+ error: { code: 'UNKNOWN_HASH_ALGORITHM', message: `tool_receipts[${i}].hash_algorithm must be "SHA-256"`, field: `payload.tool_receipts[${i}].hash_algorithm` },
1481
+ };
1482
+ }
1483
+ for (const field of REQUIRED_TOOL_RECEIPT_FIELDS) {
1484
+ if (rec[field] === undefined || rec[field] === null) {
1485
+ return {
1486
+ result: { status: 'INVALID', reason: `tool_receipts[${i}]: missing ${field}`, verified_at: now },
1487
+ error: { code: 'MISSING_REQUIRED_FIELD', message: `tool_receipts[${i}].${field} is required`, field: `payload.tool_receipts[${i}].${field}` },
1488
+ };
1489
+ }
1490
+ }
1491
+ // Validate string fields
1492
+ for (const f of ['receipt_id', 'tool_name', 'args_hash_b64u', 'result_hash_b64u', 'agent_did', 'timestamp']) {
1493
+ if (typeof rec[f] !== 'string' || rec[f].length === 0) {
1494
+ return {
1495
+ result: { status: 'INVALID', reason: `tool_receipts[${i}]: invalid ${f}`, verified_at: now },
1496
+ error: { code: 'SCHEMA_VALIDATION_FAILED', message: `tool_receipts[${i}].${f} must be a non-empty string`, field: `payload.tool_receipts[${i}].${f}` },
1497
+ };
1498
+ }
1499
+ }
1500
+ if (typeof rec.latency_ms !== 'number' || rec.latency_ms < 0) {
1501
+ return {
1502
+ result: { status: 'INVALID', reason: `tool_receipts[${i}]: invalid latency_ms`, verified_at: now },
1503
+ error: { code: 'SCHEMA_VALIDATION_FAILED', message: `tool_receipts[${i}].latency_ms must be a non-negative number`, field: `payload.tool_receipts[${i}].latency_ms` },
1504
+ };
1505
+ }
1506
+ // Validate base64url hashes
1507
+ for (const f of ['args_hash_b64u', 'result_hash_b64u']) {
1508
+ if (!isValidBase64Url(rec[f])) {
1509
+ return {
1510
+ result: { status: 'INVALID', reason: `tool_receipts[${i}]: invalid ${f} format`, verified_at: now },
1511
+ error: { code: 'SCHEMA_VALIDATION_FAILED', message: `tool_receipts[${i}].${f} must be valid base64url`, field: `payload.tool_receipts[${i}].${f}` },
1512
+ };
1513
+ }
1514
+ }
1515
+ // Agent DID must match bundle agent
1516
+ if (rec.agent_did !== payload.agent_did) {
1517
+ return {
1518
+ result: { status: 'INVALID', reason: `tool_receipts[${i}]: agent_did mismatch`, verified_at: now },
1519
+ error: { code: 'PROOF_BUNDLE_AGENT_MISMATCH', message: `tool_receipts[${i}].agent_did must equal payload.agent_did`, field: `payload.tool_receipts[${i}].agent_did` },
1520
+ };
1521
+ }
1522
+ }
1523
+ componentResults.tool_receipts_count = toolReceipts.length;
1524
+ componentResults.tool_receipts_valid = true;
1525
+ }
1526
+ // Validate side-effect receipts when present (CPL-US-007: fail-closed on unknown schema)
1527
+ if (payload.side_effect_receipts !== undefined) {
1528
+ const sideEffectReceipts = payload.side_effect_receipts;
1529
+ if (!Array.isArray(sideEffectReceipts)) {
1530
+ return {
1531
+ result: { status: 'INVALID', reason: 'side_effect_receipts must be an array', verified_at: now },
1532
+ error: { code: 'SCHEMA_VALIDATION_FAILED', message: 'payload.side_effect_receipts must be an array', field: 'payload.side_effect_receipts' },
1533
+ };
1534
+ }
1535
+ const REQUIRED_SE_FIELDS = [
1536
+ 'receipt_version', 'receipt_id', 'effect_class', 'target_hash_b64u',
1537
+ 'request_hash_b64u', 'response_hash_b64u', 'hash_algorithm', 'agent_did', 'timestamp', 'latency_ms',
1538
+ ];
1539
+ const VALID_EFFECT_CLASSES = ['network_egress', 'filesystem_write', 'external_api_write'];
1540
+ for (let i = 0; i < sideEffectReceipts.length; i++) {
1541
+ const se = sideEffectReceipts[i];
1542
+ if (typeof se !== 'object' || se === null || Array.isArray(se)) {
1543
+ return {
1544
+ result: { status: 'INVALID', reason: `side_effect_receipts[${i}] must be an object`, verified_at: now },
1545
+ error: { code: 'SCHEMA_VALIDATION_FAILED', message: `side_effect_receipts[${i}] must be an object`, field: `payload.side_effect_receipts[${i}]` },
1546
+ };
1547
+ }
1548
+ const rec = se;
1549
+ if (rec.receipt_version !== '1') {
1550
+ return {
1551
+ result: { status: 'INVALID', reason: `side_effect_receipts[${i}]: unknown receipt_version`, verified_at: now },
1552
+ error: { code: 'UNKNOWN_VERSION', message: `side_effect_receipts[${i}].receipt_version must be "1"`, field: `payload.side_effect_receipts[${i}].receipt_version` },
1553
+ };
1554
+ }
1555
+ if (rec.hash_algorithm !== 'SHA-256') {
1556
+ return {
1557
+ result: { status: 'INVALID', reason: `side_effect_receipts[${i}]: unknown hash_algorithm`, verified_at: now },
1558
+ error: { code: 'UNKNOWN_HASH_ALGORITHM', message: `side_effect_receipts[${i}].hash_algorithm must be "SHA-256"`, field: `payload.side_effect_receipts[${i}].hash_algorithm` },
1559
+ };
1560
+ }
1561
+ if (!VALID_EFFECT_CLASSES.includes(rec.effect_class)) {
1562
+ return {
1563
+ result: { status: 'INVALID', reason: `side_effect_receipts[${i}]: unknown effect_class`, verified_at: now },
1564
+ error: { code: 'SCHEMA_VALIDATION_FAILED', message: `side_effect_receipts[${i}].effect_class must be one of: ${VALID_EFFECT_CLASSES.join(', ')}`, field: `payload.side_effect_receipts[${i}].effect_class` },
1565
+ };
1566
+ }
1567
+ for (const field of REQUIRED_SE_FIELDS) {
1568
+ if (rec[field] === undefined || rec[field] === null) {
1569
+ return {
1570
+ result: { status: 'INVALID', reason: `side_effect_receipts[${i}]: missing ${field}`, verified_at: now },
1571
+ error: { code: 'MISSING_REQUIRED_FIELD', message: `side_effect_receipts[${i}].${field} is required`, field: `payload.side_effect_receipts[${i}].${field}` },
1572
+ };
1573
+ }
1574
+ }
1575
+ for (const f of ['receipt_id', 'target_hash_b64u', 'request_hash_b64u', 'response_hash_b64u', 'agent_did', 'timestamp']) {
1576
+ if (typeof rec[f] !== 'string' || rec[f].length === 0) {
1577
+ return {
1578
+ result: { status: 'INVALID', reason: `side_effect_receipts[${i}]: invalid ${f}`, verified_at: now },
1579
+ error: { code: 'SCHEMA_VALIDATION_FAILED', message: `side_effect_receipts[${i}].${f} must be a non-empty string`, field: `payload.side_effect_receipts[${i}].${f}` },
1580
+ };
1581
+ }
1582
+ }
1583
+ if (typeof rec.latency_ms !== 'number' || rec.latency_ms < 0) {
1584
+ return {
1585
+ result: { status: 'INVALID', reason: `side_effect_receipts[${i}]: invalid latency_ms`, verified_at: now },
1586
+ error: { code: 'SCHEMA_VALIDATION_FAILED', message: `side_effect_receipts[${i}].latency_ms must be >= 0`, field: `payload.side_effect_receipts[${i}].latency_ms` },
1587
+ };
1588
+ }
1589
+ if (rec.agent_did !== payload.agent_did) {
1590
+ return {
1591
+ result: { status: 'INVALID', reason: `side_effect_receipts[${i}]: agent_did mismatch`, verified_at: now },
1592
+ error: { code: 'PROOF_BUNDLE_AGENT_MISMATCH', message: `side_effect_receipts[${i}].agent_did must equal payload.agent_did`, field: `payload.side_effect_receipts[${i}].agent_did` },
1593
+ };
1594
+ }
1595
+ }
1596
+ componentResults.side_effect_receipts_count = sideEffectReceipts.length;
1597
+ componentResults.side_effect_receipts_valid = true;
1598
+ }
1599
+ // Validate human approval receipts when present (CPL-US-008: fail-closed on unknown schema)
1600
+ if (payload.human_approval_receipts !== undefined) {
1601
+ const approvalReceipts = payload.human_approval_receipts;
1602
+ if (!Array.isArray(approvalReceipts)) {
1603
+ return {
1604
+ result: { status: 'INVALID', reason: 'human_approval_receipts must be an array', verified_at: now },
1605
+ error: { code: 'SCHEMA_VALIDATION_FAILED', message: 'payload.human_approval_receipts must be an array', field: 'payload.human_approval_receipts' },
1606
+ };
1607
+ }
1608
+ const REQUIRED_HA_FIELDS = [
1609
+ 'receipt_version', 'receipt_id', 'approval_type', 'approver_subject',
1610
+ 'agent_did', 'scope_hash_b64u', 'hash_algorithm', 'timestamp',
1611
+ ];
1612
+ const VALID_APPROVAL_TYPES = ['explicit_approve', 'explicit_deny', 'auto_approve', 'timeout_deny'];
1613
+ for (let i = 0; i < approvalReceipts.length; i++) {
1614
+ const ha = approvalReceipts[i];
1615
+ if (typeof ha !== 'object' || ha === null || Array.isArray(ha)) {
1616
+ return {
1617
+ result: { status: 'INVALID', reason: `human_approval_receipts[${i}] must be an object`, verified_at: now },
1618
+ error: { code: 'SCHEMA_VALIDATION_FAILED', message: `human_approval_receipts[${i}] must be an object`, field: `payload.human_approval_receipts[${i}]` },
1619
+ };
1620
+ }
1621
+ const rec = ha;
1622
+ if (rec.receipt_version !== '1') {
1623
+ return {
1624
+ result: { status: 'INVALID', reason: `human_approval_receipts[${i}]: unknown receipt_version`, verified_at: now },
1625
+ error: { code: 'UNKNOWN_VERSION', message: `human_approval_receipts[${i}].receipt_version must be "1"`, field: `payload.human_approval_receipts[${i}].receipt_version` },
1626
+ };
1627
+ }
1628
+ if (rec.hash_algorithm !== 'SHA-256') {
1629
+ return {
1630
+ result: { status: 'INVALID', reason: `human_approval_receipts[${i}]: unknown hash_algorithm`, verified_at: now },
1631
+ error: { code: 'UNKNOWN_HASH_ALGORITHM', message: `human_approval_receipts[${i}].hash_algorithm must be "SHA-256"`, field: `payload.human_approval_receipts[${i}].hash_algorithm` },
1632
+ };
1633
+ }
1634
+ if (!VALID_APPROVAL_TYPES.includes(rec.approval_type)) {
1635
+ return {
1636
+ result: { status: 'INVALID', reason: `human_approval_receipts[${i}]: unknown approval_type`, verified_at: now },
1637
+ error: { code: 'SCHEMA_VALIDATION_FAILED', message: `human_approval_receipts[${i}].approval_type must be one of: ${VALID_APPROVAL_TYPES.join(', ')}`, field: `payload.human_approval_receipts[${i}].approval_type` },
1638
+ };
1639
+ }
1640
+ for (const field of REQUIRED_HA_FIELDS) {
1641
+ if (rec[field] === undefined || rec[field] === null) {
1642
+ return {
1643
+ result: { status: 'INVALID', reason: `human_approval_receipts[${i}]: missing ${field}`, verified_at: now },
1644
+ error: { code: 'MISSING_REQUIRED_FIELD', message: `human_approval_receipts[${i}].${field} is required`, field: `payload.human_approval_receipts[${i}].${field}` },
1645
+ };
1646
+ }
1647
+ }
1648
+ for (const f of ['receipt_id', 'approver_subject', 'agent_did', 'scope_hash_b64u', 'timestamp']) {
1649
+ if (typeof rec[f] !== 'string' || rec[f].length === 0) {
1650
+ return {
1651
+ result: { status: 'INVALID', reason: `human_approval_receipts[${i}]: invalid ${f}`, verified_at: now },
1652
+ error: { code: 'SCHEMA_VALIDATION_FAILED', message: `human_approval_receipts[${i}].${f} must be a non-empty string`, field: `payload.human_approval_receipts[${i}].${f}` },
1653
+ };
1654
+ }
1655
+ }
1656
+ if (rec.agent_did !== payload.agent_did) {
1657
+ return {
1658
+ result: { status: 'INVALID', reason: `human_approval_receipts[${i}]: agent_did mismatch`, verified_at: now },
1659
+ error: { code: 'PROOF_BUNDLE_AGENT_MISMATCH', message: `human_approval_receipts[${i}].agent_did must equal payload.agent_did`, field: `payload.human_approval_receipts[${i}].agent_did` },
1660
+ };
1661
+ }
1662
+ }
1663
+ componentResults.human_approval_receipts_count = approvalReceipts.length;
1664
+ componentResults.human_approval_receipts_valid = true;
1665
+ }
1666
+ // Validate + verify attestations if present
1667
+ // CVF-US-023: Attestations MUST be signature-verified AND attester_did allowlisted
1668
+ // before they can uplift trust tier.
1669
+ if (payload.attestations !== undefined && payload.attestations.length > 0) {
1670
+ const attestationResults = await Promise.all(payload.attestations.map(async (a) => {
1671
+ if (!validateAttestation(a)) {
1672
+ return {
1673
+ valid: false,
1674
+ signature_valid: false,
1675
+ };
1676
+ }
1677
+ return verifyAttestationReference(a, payload.agent_did, options.allowlistedAttesterDids);
1678
+ }));
1679
+ const signatureVerifiedCount = attestationResults.filter((r) => r.signature_valid).length;
1680
+ const verifiedCount = attestationResults.filter((r) => r.valid).length;
1681
+ componentResults.attestations_count = payload.attestations.length;
1682
+ componentResults.attestations_signature_verified_count = signatureVerifiedCount;
1683
+ componentResults.attestations_verified_count = verifiedCount;
1684
+ // Strict: all attestations must verify to count this component as valid.
1685
+ componentResults.attestations_valid = verifiedCount === payload.attestations.length;
1686
+ }
1687
+ // 16. Compute tiers based on validated components
1688
+ const trustTier = computeTrustTier(componentResults);
1689
+ const proofTier = computeProofTier(componentResults);
1690
+ // 17. Return success with computed tiers
1691
+ return {
1692
+ result: {
1693
+ status: 'VALID',
1694
+ reason: 'Proof bundle verified successfully',
1695
+ verified_at: now,
1696
+ bundle_id: payload.bundle_id,
1697
+ agent_did: payload.agent_did,
1698
+ trust_tier: trustTier,
1699
+ proof_tier: proofTier,
1700
+ model_identity_tier: modelIdentityTier,
1701
+ risk_flags: modelIdentityRiskFlags.size > 0
1702
+ ? [...modelIdentityRiskFlags].sort()
1703
+ : undefined,
1704
+ component_results: componentResults,
1705
+ },
1706
+ };
1707
+ }
1708
+ //# sourceMappingURL=verify-proof-bundle.js.map