@emilia-protocol/gate 0.9.5 → 0.11.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 (81) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/LICENSE +190 -0
  3. package/README.md +95 -16
  4. package/action-control-manifest.js +26 -0
  5. package/action-escrow-custodian.js +395 -0
  6. package/action-escrow-evidence.js +859 -0
  7. package/action-escrow-package.js +1669 -0
  8. package/action-escrow-postgres.js +338 -0
  9. package/action-escrow-state.js +397 -0
  10. package/action-escrow-verifiers.js +332 -0
  11. package/action-escrow.js +2448 -0
  12. package/adapters/_kit.js +47 -5
  13. package/adapters/aws.js +15 -4
  14. package/adapters/github-demo.mjs +25 -22
  15. package/adapters/github.js +1 -1
  16. package/adapters/jira.js +1 -0
  17. package/adapters/linear.js +1 -0
  18. package/adapters/salesforce.js +1 -0
  19. package/adapters/stripe.js +1 -1
  20. package/adapters/supabase.js +26 -4
  21. package/adapters/vercel.js +33 -4
  22. package/aec-execution.js +1 -3
  23. package/breakglass.js +285 -51
  24. package/control-plane.js +341 -0
  25. package/coverage.js +722 -0
  26. package/custody-demo.mjs +13 -3
  27. package/demo.mjs +9 -3
  28. package/deploy/helm/README.md +16 -8
  29. package/deploy/helm/emilia-gate/Chart.yaml +3 -1
  30. package/deploy/helm/emilia-gate/templates/NOTES.txt +3 -2
  31. package/deploy/helm/emilia-gate/templates/deployment.yaml +55 -1
  32. package/deploy/helm/emilia-gate/values.yaml +18 -2
  33. package/deploy/helm/emilia-gate-service/Chart.yaml +11 -0
  34. package/deploy/helm/emilia-gate-service/README.md +81 -0
  35. package/deploy/helm/emilia-gate-service/templates/NOTES.txt +12 -0
  36. package/deploy/helm/emilia-gate-service/templates/_helpers.tpl +83 -0
  37. package/deploy/helm/emilia-gate-service/templates/deployment.yaml +153 -0
  38. package/deploy/helm/emilia-gate-service/templates/migration-job.yaml +73 -0
  39. package/deploy/helm/emilia-gate-service/templates/networkpolicy.yaml +113 -0
  40. package/deploy/helm/emilia-gate-service/templates/pdb.yaml +14 -0
  41. package/deploy/helm/emilia-gate-service/templates/service.yaml +20 -0
  42. package/deploy/helm/emilia-gate-service/templates/serviceaccount.yaml +8 -0
  43. package/deploy/helm/emilia-gate-service/tests/fixtures/001_gate.sql +26 -0
  44. package/deploy/helm/emilia-gate-service/tests/fixtures/002_runtime_access.sql +32 -0
  45. package/deploy/helm/emilia-gate-service/tests/fixtures/gate.config.mjs +29 -0
  46. package/deploy/helm/emilia-gate-service/tests/render-check.sh +145 -0
  47. package/deploy/helm/emilia-gate-service/values.schema.json +145 -0
  48. package/deploy/helm/emilia-gate-service/values.yaml +166 -0
  49. package/deploy/sql/001-runtime.sql +707 -0
  50. package/deploy/terraform/README.md +24 -14
  51. package/deploy/terraform/main.tf +59 -0
  52. package/deploy/terraform/service/README.md +81 -0
  53. package/deploy/terraform/service/main.tf +718 -0
  54. package/deploy/terraform/service/outputs.tf +19 -0
  55. package/deploy/terraform/service/tests/validate.sh +24 -0
  56. package/deploy/terraform/service/tests/validation.tftest.hcl +168 -0
  57. package/deploy/terraform/service/variables.tf +459 -0
  58. package/deploy/terraform/service/versions.tf +10 -0
  59. package/deploy/terraform/variables.tf +95 -2
  60. package/deployment-attestation.js +248 -0
  61. package/eg1-conformance.js +46 -12
  62. package/enterprise.js +6 -2
  63. package/ep-assure.mjs +3 -2
  64. package/evidence-postgres.js +357 -0
  65. package/evidence.js +10 -3
  66. package/execution-binding.js +141 -26
  67. package/index.js +556 -115
  68. package/key-registry.js +51 -19
  69. package/mcp.js +4 -2
  70. package/network-witness.js +500 -0
  71. package/package.json +39 -5
  72. package/reliance-kernel.js +5 -6
  73. package/reliance-packet.js +43 -10
  74. package/reports/assurance-package.js +45 -19
  75. package/reports/reperform.js +78 -18
  76. package/reports/underwriter.js +1 -1
  77. package/settlement.js +300 -0
  78. package/store-postgres.js +14 -0
  79. package/store.js +26 -5
  80. package/strict-json.js +103 -0
  81. package/witness-postgres.js +97 -0
@@ -0,0 +1,1669 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Strict adapter from one exact Action Escrow kernel snapshot to the portable
4
+ * Action Escrow evidence-package profile.
5
+ *
6
+ * This module does not accept package-field overrides. Every package field is
7
+ * either derived from the durable record or occupies one of the four external
8
+ * artifact slots that the kernel intentionally does not own.
9
+ */
10
+ import crypto from 'node:crypto';
11
+
12
+ import {
13
+ ACTION_ESCROW_PROFILE_VERSION,
14
+ ACTION_ESCROW_STATE_VERSION,
15
+ ACTION_ESCROW_STATES,
16
+ ACTION_ESCROW_TRANSITIONS,
17
+ computeActionEscrowReleaseBindingMomentDigest,
18
+ computeActionEscrowResolutionNonce,
19
+ } from './action-escrow.js';
20
+ import { buildActionEscrowEvidencePackage } from './action-escrow-evidence.js';
21
+ import {
22
+ ACTION_ESCROW_STATE_STATEMENT_DOMAIN,
23
+ ACTION_ESCROW_STATE_STATEMENT_VERSION,
24
+ } from './action-escrow-state.js';
25
+ import {
26
+ computeActionEscrowAgreementDigest,
27
+ validateActionEscrowReleaseTemplate,
28
+ } from './action-escrow-verifiers.js';
29
+ import { canonicalize, hashCanonical } from './execution-binding.js';
30
+ import {
31
+ computeDocumentActionBindingDigest,
32
+ computeDocumentSha256,
33
+ computeReleaseActionDigest,
34
+ } from '@emilia-protocol/verify/document-action-binding';
35
+
36
+ const HASH = /^sha256:[0-9a-f]{64}$/;
37
+ const ID = /^[A-Za-z0-9][A-Za-z0-9._:/#@+-]{0,255}$/;
38
+ const BASE64URL = /^[A-Za-z0-9_-]+$/;
39
+ const MAX_RECORD_NODES = 50_000;
40
+ const MAX_RECORD_DEPTH = 64;
41
+ const MAX_STRING_BYTES = 4 * 1024 * 1024;
42
+ const DEFAULT_MAX_DOCUMENT_BYTES = 50 * 1024 * 1024;
43
+
44
+ const INPUT_KEYS = new Set([
45
+ 'kernelRecord',
46
+ 'finalPdfBytes',
47
+ 'documentFileName',
48
+ 'documentExecution',
49
+ 'operatorStateStatement',
50
+ 'verificationProfile',
51
+ ]);
52
+ const OPTION_KEYS = new Set(['now', 'maxDocumentBytes']);
53
+ const RECORD_KEYS = new Set([
54
+ '@version',
55
+ 'escrow_key',
56
+ 'revision',
57
+ 'state',
58
+ 'agreement_digest',
59
+ 'document_action_binding_digest',
60
+ 'milestone_id',
61
+ 'release_action_digest',
62
+ 'parties',
63
+ 'parties_digest',
64
+ 'profile',
65
+ 'profile_digest',
66
+ 'document_action_binding',
67
+ 'agreement_acceptances',
68
+ 'funding',
69
+ 'milestone_evidence',
70
+ 'release_approvals',
71
+ 'release',
72
+ 'dispute',
73
+ 'cancellation',
74
+ 'completion',
75
+ 'pending_amendment',
76
+ 'superseded_bindings',
77
+ 'operations',
78
+ 'history',
79
+ 'created_at',
80
+ 'updated_at',
81
+ ]);
82
+ const PROFILE_KEYS = new Set([
83
+ '@version',
84
+ 'profile_id',
85
+ 'provider_id',
86
+ 'required_acceptance_party_ids',
87
+ 'required_release_approver_party_ids',
88
+ 'prohibit_self_approval',
89
+ ]);
90
+ const PARTY_KEYS = new Set(['party_id', 'role']);
91
+ const CONTAINER_KEYS = new Set(['artifact', 'verification']);
92
+ const ACCEPTANCE_KEYS = new Set(['party_id', 'artifact', 'verification']);
93
+ const RELEASE_APPROVAL_KEYS = new Set(['party_id', 'resolution', 'verification']);
94
+ const CORE_VERIFICATION_KEYS = [
95
+ 'valid',
96
+ 'agreement_digest',
97
+ 'document_action_binding_digest',
98
+ 'milestone_id',
99
+ 'release_action_digest',
100
+ 'parties_digest',
101
+ 'profile_digest',
102
+ ];
103
+ const BINDING_VERIFICATION_KEYS = new Set([
104
+ ...CORE_VERIFICATION_KEYS,
105
+ 'verification_digest',
106
+ 'document_digest',
107
+ 'agreement_id',
108
+ 'binding_id',
109
+ 'release_action_template',
110
+ 'supersedes_document_action_binding_digest',
111
+ ]);
112
+ const ACCEPTANCE_VERIFICATION_KEYS = new Set([
113
+ ...CORE_VERIFICATION_KEYS,
114
+ 'party_id',
115
+ 'principal_key_id',
116
+ 'acceptance_digest',
117
+ ]);
118
+ const FUNDING_VERIFICATION_KEYS = new Set([
119
+ ...CORE_VERIFICATION_KEYS,
120
+ 'authenticated',
121
+ 'provider_id',
122
+ 'statement_type',
123
+ 'status',
124
+ 'provider_transaction_id',
125
+ 'provider_milestone_id',
126
+ 'amount',
127
+ 'currency',
128
+ 'destination_id',
129
+ 'statement_digest',
130
+ ]);
131
+ const MILESTONE_VERIFICATION_KEYS = new Set([
132
+ ...CORE_VERIFICATION_KEYS,
133
+ 'evidence_digest',
134
+ 'submitter_party_id',
135
+ 'observed_at',
136
+ ]);
137
+ const RELEASE_APPROVAL_VERIFICATION_KEYS = new Set([
138
+ ...CORE_VERIFICATION_KEYS,
139
+ 'authorizes_action',
140
+ 'outcome',
141
+ 'party_role',
142
+ 'principal_key_id',
143
+ 'nonce',
144
+ 'issued_at',
145
+ 'expires_at',
146
+ 'resolution_digest',
147
+ 'evidence_digest',
148
+ ]);
149
+ const PROVIDER_VERIFICATION_KEYS = new Set([
150
+ ...CORE_VERIFICATION_KEYS,
151
+ 'authenticated',
152
+ 'provider_id',
153
+ 'provider_idempotency_key',
154
+ 'provider_request_digest',
155
+ 'provider_transaction_id',
156
+ 'provider_milestone_id',
157
+ 'amount',
158
+ 'currency',
159
+ 'destination_id',
160
+ 'statement_type',
161
+ 'status',
162
+ 'statement_digest',
163
+ ]);
164
+ const COMMAND_VERIFICATION_KEYS = new Set([
165
+ ...CORE_VERIFICATION_KEYS,
166
+ 'authorizes_command',
167
+ 'command',
168
+ 'party_id',
169
+ 'details_digest',
170
+ 'command_digest',
171
+ ]);
172
+ const OPERATION_KEYS = new Set([
173
+ 'idempotency_key',
174
+ 'operation',
175
+ 'request_digest',
176
+ 'code',
177
+ 'ok',
178
+ 'outcome',
179
+ 'state',
180
+ 'at',
181
+ ]);
182
+ const HISTORY_KEYS = new Set([
183
+ 'from',
184
+ 'to',
185
+ 'operation',
186
+ 'idempotency_key',
187
+ 'at',
188
+ ]);
189
+ const SUPERSESSION_KEYS = new Set([
190
+ 'document_action_binding_digest',
191
+ 'release_action_digest',
192
+ 'superseded_by_binding_digest',
193
+ 'superseded_at',
194
+ ]);
195
+ const RELEASE_KEYS = new Set([
196
+ 'release_key',
197
+ 'provider_idempotency_key',
198
+ 'operation_idempotency_key',
199
+ 'status',
200
+ 'reserved_at',
201
+ 'reconciled_at',
202
+ 'provider_request',
203
+ 'provider_statement',
204
+ 'provider_verification',
205
+ ]);
206
+ const PROVIDER_REQUEST_KEYS = new Set([
207
+ 'method',
208
+ 'provider_id',
209
+ 'agreement_digest',
210
+ 'document_action_binding_digest',
211
+ 'milestone_id',
212
+ 'release_action_digest',
213
+ 'parties',
214
+ 'parties_digest',
215
+ 'profile',
216
+ 'profile_digest',
217
+ 'agreement_id',
218
+ 'binding_id',
219
+ 'document_digest',
220
+ 'release_action_template',
221
+ 'release_key',
222
+ 'idempotency_key',
223
+ 'request_digest',
224
+ ]);
225
+ const PENDING_AMENDMENT_KEYS = new Set([
226
+ 'from_state',
227
+ 'document_action_binding_digest',
228
+ 'release_action_digest',
229
+ 'document_action_binding',
230
+ 'agreement_acceptances',
231
+ 'proposer_party_id',
232
+ 'proposal_authorization',
233
+ 'proposed_at',
234
+ ]);
235
+ const COMMAND_CONTAINER_KEYS = new Set(['artifact', 'verification']);
236
+ const DISPUTE_KEYS = new Set(['party_id', 'reason', 'authorization', 'opened_at']);
237
+ const CANCELLATION_KEYS = new Set([
238
+ 'party_id',
239
+ 'reason',
240
+ 'authorization',
241
+ 'cancelled_at',
242
+ ]);
243
+ const COMPLETION_KEYS = new Set([
244
+ 'party_id',
245
+ 'meaning',
246
+ 'authorization',
247
+ 'completed_at',
248
+ ]);
249
+ const STATE_STATEMENT_KEYS = new Set([
250
+ 'version',
251
+ 'issuer',
252
+ 'payload',
253
+ 'statement_digest',
254
+ 'signature',
255
+ ]);
256
+ const STATE_ISSUER_KEYS = new Set(['operator_id', 'key_id']);
257
+ const STATE_PAYLOAD_KEYS = new Set([
258
+ 'statement_id',
259
+ 'agreement_id',
260
+ 'binding_digest',
261
+ 'action_digest',
262
+ 'profile_digest',
263
+ 'state',
264
+ 'revision',
265
+ 'amendment_digests',
266
+ 'state_record_digest',
267
+ 'previous_statement_digest',
268
+ 'occurred_at',
269
+ ]);
270
+ const STATE_SIGNATURE_KEYS = new Set(['algorithm', 'signature_b64u']);
271
+ const PROFILE_REFERENCE_KEYS = new Set(['id', 'digest']);
272
+
273
+ const EXECUTED_AGREEMENT_STATES = new Set([
274
+ 'effective',
275
+ 'awaiting_funding',
276
+ 'funded',
277
+ 'milestone_submitted',
278
+ 'release_reserved',
279
+ 'released',
280
+ 'disputed',
281
+ 'amendment_pending',
282
+ 'completed',
283
+ 'release_indeterminate',
284
+ ]);
285
+ const FUNDING_STATES = new Set([
286
+ 'funded',
287
+ 'milestone_submitted',
288
+ 'release_reserved',
289
+ 'released',
290
+ 'disputed',
291
+ 'completed',
292
+ 'release_indeterminate',
293
+ ]);
294
+ const MILESTONE_STATES = new Set([
295
+ 'milestone_submitted',
296
+ 'release_reserved',
297
+ 'released',
298
+ 'disputed',
299
+ 'completed',
300
+ 'release_indeterminate',
301
+ ]);
302
+ const RELEASE_APPROVAL_STATES = new Set([
303
+ 'milestone_submitted',
304
+ 'release_reserved',
305
+ 'released',
306
+ 'disputed',
307
+ 'completed',
308
+ 'release_indeterminate',
309
+ ]);
310
+ const COMPLETE_RELEASE_APPROVAL_STATES = new Set([
311
+ 'release_reserved',
312
+ 'released',
313
+ 'completed',
314
+ 'release_indeterminate',
315
+ ]);
316
+ const REQUIRED_RELEASE_STATES = new Set([
317
+ 'release_reserved',
318
+ 'released',
319
+ 'completed',
320
+ 'release_indeterminate',
321
+ ]);
322
+ const OPTIONAL_RELEASE_STATES = new Set([
323
+ 'milestone_submitted',
324
+ 'disputed',
325
+ ]);
326
+ const KERNEL_OPERATIONS = new Set([
327
+ 'create',
328
+ 'begin_acceptance',
329
+ 'accept_agreement',
330
+ 'request_funding',
331
+ 'record_funding',
332
+ 'submit_milestone',
333
+ 'approve_release',
334
+ 'release',
335
+ 'reconcile_release',
336
+ 'open_dispute',
337
+ 'propose_amendment',
338
+ 'accept_amendment',
339
+ 'cancel',
340
+ 'complete',
341
+ ]);
342
+
343
+ const SECRET_FIELD_NAMES = new Set([
344
+ 'api_key',
345
+ 'api_token',
346
+ 'api_secret',
347
+ 'access_key',
348
+ 'access_key_id',
349
+ 'access_token',
350
+ 'refresh_token',
351
+ 'session_token',
352
+ 'oauth_token',
353
+ 'bearer_token',
354
+ 'client_secret',
355
+ 'client_password',
356
+ 'password',
357
+ 'passwd',
358
+ 'private_key',
359
+ 'private_key_pem',
360
+ 'secret',
361
+ 'secret_key',
362
+ 'secret_access_key',
363
+ 'signing_secret',
364
+ 'webhook_secret',
365
+ 'credential',
366
+ 'credentials',
367
+ 'provider_credentials',
368
+ 'authorization_header',
369
+ 'cookie',
370
+ 'set_cookie',
371
+ 'request_headers',
372
+ 'http_headers',
373
+ 'headers',
374
+ 'connection_string',
375
+ 'database_url',
376
+ ]);
377
+
378
+ function fail(reason) {
379
+ throw new TypeError(`action-escrow package: ${reason}`);
380
+ }
381
+
382
+ function isRecord(value) {
383
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
384
+ const prototype = Object.getPrototypeOf(value);
385
+ return prototype === Object.prototype || prototype === null;
386
+ }
387
+
388
+ function exactKeys(value, allowed, required = allowed) {
389
+ if (!isRecord(value)) return false;
390
+ const keys = Object.keys(value);
391
+ return keys.every((key) => allowed.has(key))
392
+ && [...required].every((key) => Object.hasOwn(value, key));
393
+ }
394
+
395
+ function validString(value, max = 512) {
396
+ return typeof value === 'string'
397
+ && value.length > 0
398
+ && value.length <= max
399
+ && !/[\u0000-\u001f\u007f]/.test(value);
400
+ }
401
+
402
+ function validDigest(value) {
403
+ return typeof value === 'string' && HASH.test(value);
404
+ }
405
+
406
+ function validInstant(value) {
407
+ if (typeof value !== 'string') return false;
408
+ const parsed = Date.parse(value);
409
+ return Number.isFinite(parsed) && new Date(parsed).toISOString() === value;
410
+ }
411
+
412
+ function canonicalCopy(value, label) {
413
+ let nodes = 0;
414
+ let stringBytes = 0;
415
+ const seen = new WeakSet();
416
+
417
+ function copy(current, depth) {
418
+ nodes += 1;
419
+ if (nodes > MAX_RECORD_NODES || depth > MAX_RECORD_DEPTH) {
420
+ fail(`${label} exceeds resource limits`);
421
+ }
422
+ if (current === null || typeof current === 'boolean') return current;
423
+ if (typeof current === 'string') {
424
+ stringBytes += Buffer.byteLength(current, 'utf8');
425
+ if (stringBytes > MAX_STRING_BYTES) fail(`${label} exceeds string limits`);
426
+ return current;
427
+ }
428
+ if (typeof current === 'number') {
429
+ if (!Number.isSafeInteger(current) || Object.is(current, -0)) {
430
+ fail(`${label} contains a non-canonical number`);
431
+ }
432
+ return current;
433
+ }
434
+ if (!isRecord(current) && !Array.isArray(current)) {
435
+ fail(`${label} is not canonical JSON`);
436
+ }
437
+ if (seen.has(current)) fail(`${label} contains an alias or cycle`);
438
+ seen.add(current);
439
+ if (Array.isArray(current)) {
440
+ return current.map((entry) => copy(entry, depth + 1));
441
+ }
442
+ return Object.fromEntries(
443
+ Object.entries(current).map(([key, entry]) => [key, copy(entry, depth + 1)]),
444
+ );
445
+ }
446
+
447
+ return copy(value, 0);
448
+ }
449
+
450
+ function canonicalDigest(value) {
451
+ return `sha256:${hashCanonical(value)}`;
452
+ }
453
+
454
+ function sameCanonical(left, right) {
455
+ try {
456
+ return canonicalize(left) === canonicalize(right);
457
+ } catch {
458
+ return false;
459
+ }
460
+ }
461
+
462
+ function normalizeFieldName(value) {
463
+ return value
464
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
465
+ .replace(/[^A-Za-z0-9]+/g, '_')
466
+ .replace(/^_+|_+$/g, '')
467
+ .toLowerCase();
468
+ }
469
+
470
+ function assertNoSecrets(value, label) {
471
+ const stack = [{ value, path: label }];
472
+ while (stack.length > 0) {
473
+ const current = stack.pop();
474
+ if (typeof current.value === 'string') {
475
+ if (/-----BEGIN (?:ENCRYPTED )?PRIVATE KEY-----/.test(current.value)
476
+ || /^\s*Bearer\s+\S+/i.test(current.value)
477
+ || /^[a-z][a-z0-9+.-]*:\/\/[^/\s]*@/i.test(current.value)) {
478
+ fail(`${current.path} contains provider credentials or secrets`);
479
+ }
480
+ continue;
481
+ }
482
+ if (current.value === null || typeof current.value !== 'object') continue;
483
+ for (const [key, child] of Object.entries(current.value)) {
484
+ const normalized = normalizeFieldName(key);
485
+ if (SECRET_FIELD_NAMES.has(normalized)
486
+ || normalized.endsWith('_client_secret')
487
+ || normalized.endsWith('_private_key')
488
+ || normalized.endsWith('_access_token')
489
+ || normalized.endsWith('_refresh_token')
490
+ || normalized.endsWith('_password')) {
491
+ fail(`${current.path}.${key} contains provider credentials or secrets`);
492
+ }
493
+ stack.push({ value: child, path: `${current.path}.${key}` });
494
+ }
495
+ }
496
+ }
497
+
498
+ function expectedCore(record, overrides = {}) {
499
+ return {
500
+ agreement_digest: record.agreement_digest,
501
+ document_action_binding_digest:
502
+ overrides.document_action_binding_digest ?? record.document_action_binding_digest,
503
+ milestone_id: record.milestone_id,
504
+ release_action_digest: overrides.release_action_digest ?? record.release_action_digest,
505
+ parties_digest: record.parties_digest,
506
+ profile_digest: record.profile_digest,
507
+ };
508
+ }
509
+
510
+ function verificationMatchesCore(value, expected) {
511
+ return isRecord(value)
512
+ && value.valid === true
513
+ && Object.entries(expected).every(([key, entry]) => value[key] === entry);
514
+ }
515
+
516
+ function validateStringArray(value, allowed, label) {
517
+ if (!Array.isArray(value)
518
+ || value.length === 0
519
+ || value.some((entry) => !validString(entry, 256))
520
+ || new Set(value).size !== value.length
521
+ || value.some((entry) => !allowed.has(entry))) {
522
+ fail(`${label} is invalid`);
523
+ }
524
+ }
525
+
526
+ function partyIdentity(party) {
527
+ return `${party.role}\u0000${party.party_id}`;
528
+ }
529
+
530
+ function sameParties(left, right) {
531
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
532
+ const expected = new Set(left.map(partyIdentity));
533
+ return expected.size === left.length && right.every((party) => (
534
+ exactKeys(party, PARTY_KEYS) && expected.has(partyIdentity(party))
535
+ ));
536
+ }
537
+
538
+ function validatePartiesAndProfile(record) {
539
+ if (!Array.isArray(record.parties)
540
+ || record.parties.length < 2
541
+ || record.parties.length > 16) {
542
+ fail('kernel record parties are invalid');
543
+ }
544
+ const ids = new Set();
545
+ for (const party of record.parties) {
546
+ if (!exactKeys(party, PARTY_KEYS)
547
+ || !validString(party.party_id, 256)
548
+ || !validString(party.role, 128)
549
+ || ids.has(party.party_id)) {
550
+ fail('kernel record party is malformed or duplicated');
551
+ }
552
+ ids.add(party.party_id);
553
+ }
554
+ if (record.parties_digest !== canonicalDigest(record.parties)) {
555
+ fail('kernel record parties digest mismatch');
556
+ }
557
+ if (!exactKeys(record.profile, PROFILE_KEYS)
558
+ || record.profile['@version'] !== ACTION_ESCROW_PROFILE_VERSION
559
+ || !validString(record.profile.profile_id, 256)
560
+ || !validString(record.profile.provider_id, 256)
561
+ || typeof record.profile.prohibit_self_approval !== 'boolean') {
562
+ fail('kernel record profile is malformed');
563
+ }
564
+ validateStringArray(
565
+ record.profile.required_acceptance_party_ids,
566
+ ids,
567
+ 'required acceptance party roster',
568
+ );
569
+ validateStringArray(
570
+ record.profile.required_release_approver_party_ids,
571
+ ids,
572
+ 'required release party roster',
573
+ );
574
+ if (record.profile.required_acceptance_party_ids.length !== record.parties.length) {
575
+ fail('required acceptance party roster does not cover the exact parties');
576
+ }
577
+ if (record.profile_digest !== canonicalDigest(record.profile)) {
578
+ fail('kernel record profile digest mismatch');
579
+ }
580
+ }
581
+
582
+ function validateHistory(record) {
583
+ if (!Array.isArray(record.operations)
584
+ || record.operations.length === 0
585
+ || !Array.isArray(record.history)
586
+ || record.history.length === 0) {
587
+ fail('kernel operation or state history is malformed');
588
+ }
589
+ const operationIds = new Set();
590
+ const operationsById = new Map();
591
+ let previousOperationTime = Number.NEGATIVE_INFINITY;
592
+ for (const operation of record.operations) {
593
+ if (!exactKeys(operation, OPERATION_KEYS)
594
+ || !validString(operation.idempotency_key, 512)
595
+ || operationIds.has(operation.idempotency_key)
596
+ || !validString(operation.operation, 128)
597
+ || !KERNEL_OPERATIONS.has(operation.operation)
598
+ || !validDigest(operation.request_digest)
599
+ || !validString(operation.code, 256)
600
+ || typeof operation.ok !== 'boolean'
601
+ || !validString(operation.outcome, 128)
602
+ || !ACTION_ESCROW_STATES.includes(operation.state)
603
+ || !validInstant(operation.at)) {
604
+ fail('kernel operation history is malformed');
605
+ }
606
+ const operationTime = Date.parse(operation.at);
607
+ if (operationTime < previousOperationTime) {
608
+ fail('kernel operation history is not time-monotonic');
609
+ }
610
+ operationIds.add(operation.idempotency_key);
611
+ operationsById.set(operation.idempotency_key, operation);
612
+ previousOperationTime = operationTime;
613
+ }
614
+ const firstOperation = record.operations[0];
615
+ if (firstOperation.operation !== 'create'
616
+ || firstOperation.state !== 'draft'
617
+ || firstOperation.code !== 'escrow_created'
618
+ || firstOperation.ok !== true) {
619
+ fail('kernel operation history has no valid creation root');
620
+ }
621
+
622
+ let previous = null;
623
+ let previousHistoryTime = Number.NEGATIVE_INFINITY;
624
+ const releaseHistoryCounts = new Map();
625
+ for (const [index, entry] of record.history.entries()) {
626
+ if (!exactKeys(entry, HISTORY_KEYS)
627
+ || entry.from !== previous
628
+ || !ACTION_ESCROW_STATES.includes(entry.to)
629
+ || !validString(entry.operation, 128)
630
+ || !validString(entry.idempotency_key, 512)
631
+ || !validInstant(entry.at)) {
632
+ fail('kernel state history is malformed');
633
+ }
634
+ const linkedOperation = operationsById.get(entry.idempotency_key);
635
+ const historyTime = Date.parse(entry.at);
636
+ if (!linkedOperation
637
+ || linkedOperation.operation !== entry.operation
638
+ || historyTime < previousHistoryTime) {
639
+ fail('kernel state history is not bound to its operation log');
640
+ }
641
+ if (entry.operation === 'release') {
642
+ if (historyTime < Date.parse(linkedOperation.at)) {
643
+ fail('kernel release history predates its reservation');
644
+ }
645
+ releaseHistoryCounts.set(
646
+ entry.idempotency_key,
647
+ (releaseHistoryCounts.get(entry.idempotency_key) ?? 0) + 1,
648
+ );
649
+ } else if (entry.at !== linkedOperation.at || entry.to !== linkedOperation.state) {
650
+ fail('kernel state history is inconsistent with its operation');
651
+ }
652
+ if (index === 0) {
653
+ if (entry.from !== null || entry.to !== 'draft' || entry.operation !== 'create') {
654
+ fail('kernel state history has no valid creation root');
655
+ }
656
+ } else if (!ACTION_ESCROW_TRANSITIONS[entry.from]?.includes(entry.to)) {
657
+ fail('kernel state history contains an invalid transition');
658
+ }
659
+ previous = entry.to;
660
+ previousHistoryTime = historyTime;
661
+ }
662
+ if (previous !== record.state) fail('kernel state does not match its history');
663
+ const internalReleaseTransitions = [...releaseHistoryCounts.values()]
664
+ .reduce((total, count) => total + Math.max(0, count - 1), 0);
665
+ if (record.revision !== record.operations.length - 1 + internalReleaseTransitions) {
666
+ fail('kernel revision does not match its complete operation history');
667
+ }
668
+
669
+ const eventTimes = [
670
+ ...record.operations.map((entry) => entry.at),
671
+ ...record.history.map((entry) => entry.at),
672
+ ];
673
+ if (record.created_at !== record.operations[0].at
674
+ || Date.parse(record.created_at) > Date.parse(record.updated_at)
675
+ || eventTimes.some((instant) => Date.parse(instant) > Date.parse(record.updated_at))
676
+ || Math.max(...eventTimes.map((instant) => Date.parse(instant)))
677
+ !== Date.parse(record.updated_at)) {
678
+ fail('kernel record timestamps do not match its history');
679
+ }
680
+ }
681
+
682
+ function validateBindingContainer(container, record, {
683
+ bindingDigest = record.document_action_binding_digest,
684
+ actionDigest = record.release_action_digest,
685
+ supersedesDigest = null,
686
+ documentDigest = null,
687
+ documentLength = null,
688
+ } = {}) {
689
+ if (!exactKeys(container, CONTAINER_KEYS)
690
+ || !isRecord(container.artifact)
691
+ || !isRecord(container.verification)) {
692
+ fail('document-action binding container is malformed');
693
+ }
694
+ const binding = container.artifact;
695
+ const verification = container.verification;
696
+ const computedBindingDigest = computeDocumentActionBindingDigest(binding);
697
+ const computedActionDigest = computeReleaseActionDigest(binding.release_action?.template);
698
+ const bindingDocumentDigest = binding.document?.digest;
699
+ const releaseActionTemplate = validateActionEscrowReleaseTemplate(
700
+ binding.release_action?.template,
701
+ {
702
+ profileDigest: record.profile_digest,
703
+ agreementId: binding.agreement_id,
704
+ agreementDigest: record.agreement_digest,
705
+ milestoneId: record.milestone_id,
706
+ documentDigest: bindingDocumentDigest,
707
+ materialTerms: binding.material_terms,
708
+ },
709
+ );
710
+ const requiredVerification = new Set(BINDING_VERIFICATION_KEYS);
711
+ if (supersedesDigest === null) {
712
+ requiredVerification.delete('supersedes_document_action_binding_digest');
713
+ }
714
+ if (!exactKeys(verification, BINDING_VERIFICATION_KEYS, requiredVerification)
715
+ || !verificationMatchesCore(verification, expectedCore(record, {
716
+ document_action_binding_digest: bindingDigest,
717
+ release_action_digest: actionDigest,
718
+ }))
719
+ || computedBindingDigest !== bindingDigest
720
+ || binding.binding_digest !== bindingDigest
721
+ || verification.verification_digest !== bindingDigest
722
+ || computedActionDigest !== actionDigest
723
+ || binding.release_action?.digest !== actionDigest
724
+ || !validDigest(bindingDocumentDigest)
725
+ || verification.document_digest !== bindingDocumentDigest
726
+ || verification.agreement_id !== binding.agreement_id
727
+ || verification.binding_id !== binding.binding_id
728
+ || computeActionEscrowAgreementDigest(binding.agreement_id) !== record.agreement_digest
729
+ || releaseActionTemplate === null
730
+ || !sameCanonical(
731
+ verification.release_action_template,
732
+ releaseActionTemplate,
733
+ )
734
+ || !sameParties(record.parties, binding.parties)
735
+ || !sameParties(record.parties, binding.required_parties)
736
+ || (binding.supersedes_digest ?? null) !== supersedesDigest
737
+ || (verification.supersedes_document_action_binding_digest ?? null)
738
+ !== supersedesDigest) {
739
+ fail('document-action binding is inconsistent with the kernel record');
740
+ }
741
+ if (documentDigest !== null
742
+ && (bindingDocumentDigest !== documentDigest
743
+ || releaseActionTemplate.document_sha256 !== documentDigest)) {
744
+ fail('final PDF does not match the kernel document binding');
745
+ }
746
+ if (documentLength !== null && binding.document?.byte_length !== documentLength) {
747
+ fail('final PDF byte length does not match the kernel document binding');
748
+ }
749
+ return {
750
+ agreementId: binding.agreement_id,
751
+ binding,
752
+ documentDigest: bindingDocumentDigest,
753
+ };
754
+ }
755
+
756
+ function validateAcceptances(entries, record, core, { requireComplete, requireIncomplete = false } = {}) {
757
+ if (!Array.isArray(entries)) fail('agreement acceptances are malformed');
758
+ const parties = new Map(record.parties.map((party) => [party.party_id, party]));
759
+ const required = new Set(record.profile.required_acceptance_party_ids);
760
+ const seenParties = new Set();
761
+ const seenKeys = new Set();
762
+ const result = [];
763
+ for (const entry of entries) {
764
+ const verification = entry?.verification;
765
+ const party = parties.get(entry?.party_id);
766
+ if (!exactKeys(entry, ACCEPTANCE_KEYS)
767
+ || !party
768
+ || !required.has(entry.party_id)
769
+ || seenParties.has(entry.party_id)
770
+ || !isRecord(entry.artifact)
771
+ || !exactKeys(verification, ACCEPTANCE_VERIFICATION_KEYS)
772
+ || !verificationMatchesCore(verification, core)
773
+ || verification.party_id !== entry.party_id
774
+ || !validString(verification.principal_key_id, 512)
775
+ || seenKeys.has(verification.principal_key_id)
776
+ || verification.acceptance_digest !== canonicalDigest(entry.artifact)) {
777
+ fail('agreement acceptance is malformed or inconsistent');
778
+ }
779
+ seenParties.add(entry.party_id);
780
+ seenKeys.add(verification.principal_key_id);
781
+ result.push({
782
+ party_id: entry.party_id,
783
+ role: party.role,
784
+ evidence: entry.artifact,
785
+ });
786
+ }
787
+ if (requireComplete && seenParties.size !== required.size) {
788
+ fail('required agreement acceptance is missing');
789
+ }
790
+ if (requireIncomplete && seenParties.size >= required.size) {
791
+ fail('agreement acceptance count is inconsistent with the stage');
792
+ }
793
+ return result;
794
+ }
795
+
796
+ function validateFunding(record) {
797
+ const template = record.document_action_binding.verification.release_action_template;
798
+ if (!exactKeys(record.funding, new Set(['statement', 'verification']))
799
+ || !isRecord(record.funding.statement)
800
+ || !exactKeys(record.funding.verification, FUNDING_VERIFICATION_KEYS)
801
+ || !verificationMatchesCore(record.funding.verification, expectedCore(record))
802
+ || record.funding.verification.authenticated !== true
803
+ || record.funding.verification.provider_id !== record.profile.provider_id
804
+ || record.funding.verification.statement_type !== 'funding'
805
+ || record.funding.verification.status !== 'funded'
806
+ || record.funding.verification.provider_transaction_id
807
+ !== template.custodian_transaction_id
808
+ || record.funding.verification.provider_milestone_id
809
+ !== template.custodian_milestone_id
810
+ || record.funding.verification.amount !== template.amount
811
+ || record.funding.verification.currency !== template.currency
812
+ || record.funding.verification.destination_id !== template.destination_id
813
+ || record.funding.verification.statement_digest
814
+ !== canonicalDigest(record.funding.statement)) {
815
+ fail('funding statement is malformed or inconsistent');
816
+ }
817
+ return record.funding.statement;
818
+ }
819
+
820
+ function validateMilestone(record) {
821
+ if (!exactKeys(record.milestone_evidence, CONTAINER_KEYS)
822
+ || !isRecord(record.milestone_evidence.artifact)
823
+ || !exactKeys(
824
+ record.milestone_evidence.verification,
825
+ MILESTONE_VERIFICATION_KEYS,
826
+ )
827
+ || !verificationMatchesCore(
828
+ record.milestone_evidence.verification,
829
+ expectedCore(record),
830
+ )
831
+ || !validDigest(record.milestone_evidence.verification.evidence_digest)
832
+ || !record.parties.some(
833
+ (party) => party.party_id
834
+ === record.milestone_evidence.verification.submitter_party_id,
835
+ )
836
+ || !validInstant(record.milestone_evidence.verification.observed_at)) {
837
+ fail('milestone evidence is malformed or inconsistent');
838
+ }
839
+ const artifact = record.milestone_evidence.artifact;
840
+ const claimedDigest = artifact.evidence_digest
841
+ ?? artifact.payload?.claim?.evidence_manifest_digest;
842
+ if (claimedDigest !== undefined
843
+ && claimedDigest !== record.milestone_evidence.verification.evidence_digest) {
844
+ fail('milestone artifact digest does not match its kernel verification');
845
+ }
846
+ const submission = record.operations.find(
847
+ (operation) => operation.operation === 'submit_milestone',
848
+ );
849
+ if (!submission
850
+ || Date.parse(record.milestone_evidence.verification.observed_at)
851
+ > Date.parse(submission.at)) {
852
+ fail('milestone evidence was not valid at its submission operation');
853
+ }
854
+ return {
855
+ milestone_id: record.milestone_id,
856
+ evidence: artifact,
857
+ resolution: null,
858
+ };
859
+ }
860
+
861
+ function validateReleaseApprovals(record, milestoneVerification) {
862
+ if (!Array.isArray(record.release_approvals)) fail('release approvals are malformed');
863
+ const parties = new Map(record.parties.map((party) => [party.party_id, party]));
864
+ const required = new Set(record.profile.required_release_approver_party_ids);
865
+ const bindingInput = {
866
+ agreement_digest: record.agreement_digest,
867
+ document_action_binding_digest: record.document_action_binding_digest,
868
+ milestone_id: record.milestone_id,
869
+ release_action_digest: record.release_action_digest,
870
+ profile_digest: record.profile_digest,
871
+ evidence_digest: milestoneVerification.evidence_digest,
872
+ release_action_template:
873
+ record.document_action_binding.verification.release_action_template,
874
+ };
875
+ const bindingMomentDigest = computeActionEscrowReleaseBindingMomentDigest(bindingInput);
876
+ const expectedInitiator = milestoneVerification.submitter_party_id;
877
+ if (!validDigest(bindingMomentDigest) || !validString(expectedInitiator, 256)) {
878
+ fail('release approval binding context is malformed');
879
+ }
880
+ const seenParties = new Set();
881
+ const seenKeys = new Set();
882
+ const result = [];
883
+ const approvalOperations = record.operations.filter(
884
+ (operation) => operation.operation === 'approve_release',
885
+ );
886
+ for (const [index, entry] of record.release_approvals.entries()) {
887
+ const party = parties.get(entry?.party_id);
888
+ const verification = entry?.verification;
889
+ const context = entry?.resolution?.signoff?.context;
890
+ if (!exactKeys(entry, RELEASE_APPROVAL_KEYS)
891
+ || !party
892
+ || !required.has(entry.party_id)
893
+ || seenParties.has(entry.party_id)
894
+ || !isRecord(entry.resolution)
895
+ || entry.resolution.profile !== 'EP-RESOLUTION-v1'
896
+ || !isRecord(context)
897
+ || !exactKeys(verification, RELEASE_APPROVAL_VERIFICATION_KEYS)
898
+ || !verificationMatchesCore(verification, expectedCore(record))
899
+ || verification.authorizes_action !== true
900
+ || verification.outcome !== 'approved'
901
+ || verification.party_role !== party.role
902
+ || !validString(verification.principal_key_id, 512)
903
+ || seenKeys.has(verification.principal_key_id)
904
+ || !validString(verification.nonce, 512)
905
+ || !validInstant(verification.issued_at)
906
+ || !validInstant(verification.expires_at)
907
+ || Date.parse(verification.issued_at) < Date.parse(milestoneVerification.observed_at)
908
+ || Date.parse(verification.expires_at) <= Date.parse(verification.issued_at)
909
+ || verification.resolution_digest !== canonicalDigest(entry.resolution)
910
+ || verification.evidence_digest !== milestoneVerification.evidence_digest
911
+ || context.principal !== entry.party_id
912
+ || context.principal_key_id !== verification.principal_key_id
913
+ || context.envelope_hash !== bindingMomentDigest
914
+ || context.action_hash !== record.release_action_digest
915
+ || context.initiator !== expectedInitiator
916
+ || context.nonce !== computeActionEscrowResolutionNonce(bindingInput, entry.party_id)
917
+ || context.nonce !== verification.nonce
918
+ || context.issued_at !== verification.issued_at
919
+ || context.expires_at !== verification.expires_at
920
+ || context.resolution?.outcome !== 'approved'
921
+ || context.resolution?.selected_option !== 0) {
922
+ fail('release approval is malformed or inconsistent');
923
+ }
924
+ const approvalOperation = approvalOperations[index];
925
+ if (!approvalOperation
926
+ || Date.parse(verification.issued_at) > Date.parse(approvalOperation.at)
927
+ || Date.parse(verification.expires_at) <= Date.parse(approvalOperation.at)
928
+ || (
929
+ record.release !== null
930
+ && (
931
+ Date.parse(verification.issued_at) > Date.parse(record.release.reserved_at)
932
+ || Date.parse(verification.expires_at) <= Date.parse(record.release.reserved_at)
933
+ )
934
+ )) {
935
+ fail('release approval was not valid at its operation');
936
+ }
937
+ seenParties.add(entry.party_id);
938
+ seenKeys.add(verification.principal_key_id);
939
+ result.push({
940
+ party_id: entry.party_id,
941
+ role: party.role,
942
+ evidence: entry.resolution,
943
+ });
944
+ }
945
+ return { approvals: result, approvedPartyIds: seenParties };
946
+ }
947
+
948
+ function validateProviderVerification(value, record, release) {
949
+ const template = record.document_action_binding.verification.release_action_template;
950
+ if (!exactKeys(value, PROVIDER_VERIFICATION_KEYS)
951
+ || !verificationMatchesCore(value, expectedCore(record))
952
+ || value.authenticated !== true
953
+ || value.provider_id !== record.profile.provider_id
954
+ || value.provider_idempotency_key !== release.provider_idempotency_key
955
+ || value.provider_request_digest !== release.provider_request.request_digest
956
+ || value.provider_transaction_id !== template.custodian_transaction_id
957
+ || value.provider_milestone_id !== template.custodian_milestone_id
958
+ || value.amount !== template.amount
959
+ || value.currency !== template.currency
960
+ || value.destination_id !== template.destination_id
961
+ || value.statement_type !== 'release'
962
+ || !['released', 'not_released', 'pending'].includes(value.status)
963
+ || !validDigest(value.statement_digest)
964
+ || value.statement_digest !== canonicalDigest(release.provider_statement)) {
965
+ fail('provider release statement is malformed or inconsistent');
966
+ }
967
+ }
968
+
969
+ function validateRelease(record) {
970
+ const release = record.release;
971
+ if (!exactKeys(release, RELEASE_KEYS)
972
+ || !validString(release.release_key, 256)
973
+ || !validString(release.provider_idempotency_key, 256)
974
+ || !validString(release.operation_idempotency_key, 512)
975
+ || !['reserved', 'released', 'not_released', 'indeterminate'].includes(release.status)
976
+ || !validInstant(release.reserved_at)
977
+ || (release.reconciled_at !== null && !validInstant(release.reconciled_at))
978
+ || !exactKeys(release.provider_request, PROVIDER_REQUEST_KEYS)) {
979
+ fail('release record is malformed');
980
+ }
981
+ const expectedReleaseKey = `ep-ae-reservation:${hashCanonical({
982
+ '@version': 'EP-ACTION-ESCROW-RELEASE-KEY-v1',
983
+ agreement_digest: record.agreement_digest,
984
+ document_action_binding_digest: record.document_action_binding_digest,
985
+ milestone_id: record.milestone_id,
986
+ release_action_digest: record.release_action_digest,
987
+ profile_digest: record.profile_digest,
988
+ })}`;
989
+ const expectedProviderKey = `ep-ae-release:${hashCanonical({
990
+ '@version': 'EP-ACTION-ESCROW-PROVIDER-IDEMPOTENCY-v1',
991
+ agreement_digest: record.agreement_digest,
992
+ document_action_binding_digest: record.document_action_binding_digest,
993
+ milestone_id: record.milestone_id,
994
+ release_action_digest: record.release_action_digest,
995
+ profile_digest: record.profile_digest,
996
+ })}`;
997
+ if (release.release_key !== expectedReleaseKey
998
+ || release.provider_idempotency_key !== expectedProviderKey) {
999
+ fail('release reservation key mismatch');
1000
+ }
1001
+
1002
+ const request = release.provider_request;
1003
+ const { request_digest: requestDigest, ...requestScope } = request;
1004
+ if (request.method !== 'POST'
1005
+ || request.provider_id !== record.profile.provider_id
1006
+ || request.agreement_digest !== record.agreement_digest
1007
+ || request.document_action_binding_digest
1008
+ !== record.document_action_binding_digest
1009
+ || request.milestone_id !== record.milestone_id
1010
+ || request.release_action_digest !== record.release_action_digest
1011
+ || !sameCanonical(request.parties, record.parties)
1012
+ || request.parties_digest !== record.parties_digest
1013
+ || !sameCanonical(request.profile, record.profile)
1014
+ || request.profile_digest !== record.profile_digest
1015
+ || request.agreement_id
1016
+ !== record.document_action_binding.verification.agreement_id
1017
+ || request.binding_id !== record.document_action_binding.verification.binding_id
1018
+ || request.document_digest
1019
+ !== record.document_action_binding.verification.document_digest
1020
+ || !sameCanonical(
1021
+ request.release_action_template,
1022
+ record.document_action_binding.verification.release_action_template,
1023
+ )
1024
+ || request.release_key !== release.release_key
1025
+ || request.idempotency_key !== release.provider_idempotency_key
1026
+ || requestDigest !== canonicalDigest({
1027
+ '@version': 'EP-ACTION-ESCROW-PROVIDER-REQUEST-v1',
1028
+ ...requestScope,
1029
+ })) {
1030
+ fail('provider release request is inconsistent with the kernel record');
1031
+ }
1032
+
1033
+ const execution = record.operations.find((entry) => (
1034
+ entry.operation === 'release'
1035
+ && entry.idempotency_key === release.operation_idempotency_key
1036
+ ));
1037
+ if (!execution) fail('release execution record is missing');
1038
+ if (release.reserved_at !== execution.at
1039
+ || (
1040
+ release.reconciled_at !== null
1041
+ && Date.parse(release.reconciled_at) < Date.parse(release.reserved_at)
1042
+ )) {
1043
+ fail('release timestamps are inconsistent with the execution operation');
1044
+ }
1045
+ const expectedExecution = {
1046
+ reserved: {
1047
+ states: new Set(['release_reserved']),
1048
+ operationStates: new Set(['release_reserved']),
1049
+ codes: new Set(['release_reserved']),
1050
+ ok: false,
1051
+ outcomes: new Set(['reserved']),
1052
+ },
1053
+ released: {
1054
+ states: new Set(['released', 'completed']),
1055
+ operationStates: new Set(['released']),
1056
+ codes: new Set(['release_committed', 'release_reconciled_released']),
1057
+ ok: true,
1058
+ outcomes: new Set(['applied']),
1059
+ },
1060
+ not_released: {
1061
+ states: new Set(['milestone_submitted', 'disputed', 'amendment_pending']),
1062
+ operationStates: new Set(['milestone_submitted', 'amendment_pending']),
1063
+ codes: new Set([
1064
+ 'provider_release_not_released',
1065
+ 'release_reconciled_not_released',
1066
+ ]),
1067
+ ok: false,
1068
+ outcomes: new Set(['refused']),
1069
+ },
1070
+ indeterminate: {
1071
+ states: new Set(['release_indeterminate']),
1072
+ operationStates: new Set(['release_indeterminate']),
1073
+ codes: new Set([
1074
+ 'release_effect_indeterminate',
1075
+ 'release_commit_indeterminate',
1076
+ 'release_still_indeterminate',
1077
+ ]),
1078
+ ok: false,
1079
+ outcomes: new Set(['indeterminate']),
1080
+ },
1081
+ }[release.status];
1082
+ if (!expectedExecution.states.has(record.state)
1083
+ || !expectedExecution.operationStates.has(execution.state)
1084
+ || !expectedExecution.codes.has(execution.code)
1085
+ || execution.ok !== expectedExecution.ok
1086
+ || !expectedExecution.outcomes.has(execution.outcome)) {
1087
+ fail('release execution record is inconsistent with the release state');
1088
+ }
1089
+
1090
+ if (release.status === 'reserved') {
1091
+ if (release.reconciled_at !== null
1092
+ || release.provider_statement !== null
1093
+ || release.provider_verification !== null) {
1094
+ fail('reserved release carries premature provider evidence');
1095
+ }
1096
+ } else {
1097
+ if (release.reconciled_at === null) fail('reconciled release is missing its instant');
1098
+ const hasStatement = release.provider_statement !== null;
1099
+ const hasVerification = release.provider_verification !== null;
1100
+ if (hasStatement !== hasVerification) {
1101
+ fail('provider release statement and verification must be paired');
1102
+ }
1103
+ if (release.status !== 'indeterminate' && !hasStatement) {
1104
+ fail('authoritative provider release statement is missing');
1105
+ }
1106
+ if (hasStatement) {
1107
+ if (!isRecord(release.provider_statement)) {
1108
+ fail('provider release statement is malformed');
1109
+ }
1110
+ validateProviderVerification(release.provider_verification, record, release);
1111
+ if (release.status !== 'indeterminate'
1112
+ && release.provider_verification.status !== release.status) {
1113
+ fail('provider release status mismatch');
1114
+ }
1115
+ }
1116
+ }
1117
+
1118
+ return {
1119
+ reservation: {
1120
+ release_key: release.release_key,
1121
+ provider_idempotency_key: release.provider_idempotency_key,
1122
+ reserved_at: release.reserved_at,
1123
+ },
1124
+ provider_request: request,
1125
+ provider_statement: release.provider_statement,
1126
+ execution_record: execution,
1127
+ };
1128
+ }
1129
+
1130
+ function validateCommandAuthorization(container, record, command, partyId, details) {
1131
+ if (!exactKeys(container, COMMAND_CONTAINER_KEYS)
1132
+ || !isRecord(container.artifact)
1133
+ || !exactKeys(container.verification, COMMAND_VERIFICATION_KEYS)
1134
+ || !verificationMatchesCore(container.verification, expectedCore(record))) {
1135
+ fail(`${command} authorization is malformed`);
1136
+ }
1137
+ const detailsDigest = canonicalDigest(details);
1138
+ const commandDigest = canonicalDigest({
1139
+ '@version': 'EP-ACTION-ESCROW-COMMAND-v1',
1140
+ ...expectedCore(record),
1141
+ command,
1142
+ party_id: partyId,
1143
+ details_digest: detailsDigest,
1144
+ });
1145
+ if (container.verification.authorizes_command !== true
1146
+ || container.verification.command !== command
1147
+ || container.verification.party_id !== partyId
1148
+ || container.verification.details_digest !== detailsDigest
1149
+ || container.verification.command_digest !== commandDigest) {
1150
+ fail(`${command} authorization does not match the state change`);
1151
+ }
1152
+ }
1153
+
1154
+ function validateAncillaryState(record) {
1155
+ if (record.state === 'disputed') {
1156
+ if (!exactKeys(record.dispute, DISPUTE_KEYS)
1157
+ || !record.parties.some((party) => party.party_id === record.dispute.party_id)
1158
+ || !validString(record.dispute.reason, 2048)
1159
+ || !validInstant(record.dispute.opened_at)) {
1160
+ fail('dispute state is malformed');
1161
+ }
1162
+ validateCommandAuthorization(
1163
+ record.dispute.authorization,
1164
+ record,
1165
+ 'open_dispute',
1166
+ record.dispute.party_id,
1167
+ { reason: record.dispute.reason },
1168
+ );
1169
+ } else if (record.dispute !== null) {
1170
+ fail('non-disputed stage carries a dispute');
1171
+ }
1172
+
1173
+ if (record.state === 'cancelled') {
1174
+ if (!exactKeys(record.cancellation, CANCELLATION_KEYS)
1175
+ || !record.parties.some(
1176
+ (party) => party.party_id === record.cancellation.party_id,
1177
+ )
1178
+ || (record.cancellation.reason !== null
1179
+ && !validString(record.cancellation.reason, 2048))
1180
+ || !validInstant(record.cancellation.cancelled_at)) {
1181
+ fail('cancellation state is malformed');
1182
+ }
1183
+ validateCommandAuthorization(
1184
+ record.cancellation.authorization,
1185
+ record,
1186
+ 'cancel',
1187
+ record.cancellation.party_id,
1188
+ { reason: record.cancellation.reason },
1189
+ );
1190
+ } else if (record.cancellation !== null) {
1191
+ fail('non-cancelled stage carries a cancellation');
1192
+ }
1193
+
1194
+ if (record.state === 'completed') {
1195
+ if (!exactKeys(record.completion, COMPLETION_KEYS)
1196
+ || !record.parties.some((party) => party.party_id === record.completion.party_id)
1197
+ || record.completion.meaning !== 'administrative_archive_only'
1198
+ || !validInstant(record.completion.completed_at)) {
1199
+ fail('completion state is malformed');
1200
+ }
1201
+ validateCommandAuthorization(
1202
+ record.completion.authorization,
1203
+ record,
1204
+ 'complete',
1205
+ record.completion.party_id,
1206
+ { meaning: 'administrative_archive_only' },
1207
+ );
1208
+ } else if (record.completion !== null) {
1209
+ fail('non-completed stage carries a completion record');
1210
+ }
1211
+ }
1212
+
1213
+ function validateSupersessions(record) {
1214
+ if (!Array.isArray(record.superseded_bindings)) {
1215
+ fail('superseded binding history is malformed');
1216
+ }
1217
+ const amendments = [];
1218
+ const amendmentDigests = [];
1219
+ let previousNext = null;
1220
+ const seen = new Set();
1221
+ for (const entry of record.superseded_bindings) {
1222
+ if (!exactKeys(entry, SUPERSESSION_KEYS)
1223
+ || !validDigest(entry.document_action_binding_digest)
1224
+ || !validDigest(entry.release_action_digest)
1225
+ || !validDigest(entry.superseded_by_binding_digest)
1226
+ || !validInstant(entry.superseded_at)
1227
+ || entry.document_action_binding_digest === entry.superseded_by_binding_digest
1228
+ || seen.has(entry.document_action_binding_digest)
1229
+ || (previousNext !== null
1230
+ && entry.document_action_binding_digest !== previousNext)) {
1231
+ fail('superseded binding history is inconsistent');
1232
+ }
1233
+ seen.add(entry.document_action_binding_digest);
1234
+ previousNext = entry.superseded_by_binding_digest;
1235
+ amendments.push(entry);
1236
+ amendmentDigests.push(canonicalDigest(entry));
1237
+ }
1238
+ if (previousNext !== null && previousNext !== record.document_action_binding_digest) {
1239
+ fail('superseded binding history does not terminate at the current binding');
1240
+ }
1241
+ return {
1242
+ amendments,
1243
+ amendmentDigests,
1244
+ currentSupersedes: amendments.at(-1)?.document_action_binding_digest ?? null,
1245
+ };
1246
+ }
1247
+
1248
+ function validatePendingAmendment(record) {
1249
+ const pendingAllowed = record.state === 'amendment_pending'
1250
+ || record.state === 'release_indeterminate'
1251
+ || record.state === 'cancelled';
1252
+ if (record.pending_amendment === null) {
1253
+ if (record.state === 'amendment_pending') fail('pending amendment is missing');
1254
+ return;
1255
+ }
1256
+ if (!pendingAllowed
1257
+ || !exactKeys(record.pending_amendment, PENDING_AMENDMENT_KEYS)
1258
+ || ![
1259
+ 'effective',
1260
+ 'awaiting_funding',
1261
+ 'funded',
1262
+ 'milestone_submitted',
1263
+ 'disputed',
1264
+ 'release_reserved',
1265
+ 'release_indeterminate',
1266
+ ].includes(record.pending_amendment.from_state)
1267
+ || !validDigest(record.pending_amendment.document_action_binding_digest)
1268
+ || !validDigest(record.pending_amendment.release_action_digest)
1269
+ || record.pending_amendment.document_action_binding_digest
1270
+ === record.document_action_binding_digest
1271
+ || !record.parties.some(
1272
+ (party) => party.party_id === record.pending_amendment.proposer_party_id,
1273
+ )
1274
+ || !validInstant(record.pending_amendment.proposed_at)) {
1275
+ fail('pending amendment is malformed');
1276
+ }
1277
+ if (record.state === 'amendment_pending'
1278
+ && ['release_reserved', 'release_indeterminate'].includes(
1279
+ record.pending_amendment.from_state,
1280
+ )) {
1281
+ fail('pending amendment stage is inconsistent with release reconciliation');
1282
+ }
1283
+ if (record.state === 'release_indeterminate'
1284
+ && !['release_reserved', 'release_indeterminate'].includes(
1285
+ record.pending_amendment.from_state,
1286
+ )) {
1287
+ fail('indeterminate amendment does not originate from an uncertain release');
1288
+ }
1289
+
1290
+ validateBindingContainer(
1291
+ record.pending_amendment.document_action_binding,
1292
+ record,
1293
+ {
1294
+ bindingDigest: record.pending_amendment.document_action_binding_digest,
1295
+ actionDigest: record.pending_amendment.release_action_digest,
1296
+ supersedesDigest: record.document_action_binding_digest,
1297
+ },
1298
+ );
1299
+ const pendingCore = expectedCore(record, {
1300
+ document_action_binding_digest:
1301
+ record.pending_amendment.document_action_binding_digest,
1302
+ release_action_digest: record.pending_amendment.release_action_digest,
1303
+ });
1304
+ validateAcceptances(
1305
+ record.pending_amendment.agreement_acceptances,
1306
+ record,
1307
+ pendingCore,
1308
+ { requireComplete: false, requireIncomplete: true },
1309
+ );
1310
+ validateCommandAuthorization(
1311
+ record.pending_amendment.proposal_authorization,
1312
+ record,
1313
+ 'propose_amendment',
1314
+ record.pending_amendment.proposer_party_id,
1315
+ {
1316
+ next_document_action_binding_digest:
1317
+ record.pending_amendment.document_action_binding_digest,
1318
+ next_release_action_digest: record.pending_amendment.release_action_digest,
1319
+ },
1320
+ );
1321
+ }
1322
+
1323
+ function validateLifecycle(record) {
1324
+ const requiresExecutedAgreement = EXECUTED_AGREEMENT_STATES.has(record.state);
1325
+ const agreementAcceptances = validateAcceptances(
1326
+ record.agreement_acceptances,
1327
+ record,
1328
+ expectedCore(record),
1329
+ {
1330
+ requireComplete: requiresExecutedAgreement,
1331
+ requireIncomplete: record.state === 'awaiting_acceptance',
1332
+ },
1333
+ );
1334
+ if (record.state === 'draft' && agreementAcceptances.length !== 0) {
1335
+ fail('draft stage carries agreement acceptances');
1336
+ }
1337
+
1338
+ const requiresFunding = FUNDING_STATES.has(record.state);
1339
+ if (requiresFunding !== (record.funding !== null)) {
1340
+ fail(requiresFunding
1341
+ ? 'required funding statement is missing'
1342
+ : 'stage carries an unexpected funding statement');
1343
+ }
1344
+ const fundingStatement = record.funding === null ? null : validateFunding(record);
1345
+
1346
+ const requiresMilestone = MILESTONE_STATES.has(record.state);
1347
+ if (requiresMilestone !== (record.milestone_evidence !== null)) {
1348
+ fail(requiresMilestone
1349
+ ? 'required milestone evidence is missing'
1350
+ : 'stage carries unexpected milestone evidence');
1351
+ }
1352
+ const milestone = record.milestone_evidence === null ? null : validateMilestone(record);
1353
+
1354
+ validateLifecycleOperationCoverage(record);
1355
+ if (!RELEASE_APPROVAL_STATES.has(record.state)
1356
+ && record.release_approvals.length !== 0) {
1357
+ fail('stage carries unexpected release approvals');
1358
+ }
1359
+ if (record.release_approvals.length > 0 && milestone === null) {
1360
+ fail('release approval has no milestone evidence');
1361
+ }
1362
+ const releaseApprovalResult = milestone === null
1363
+ ? { approvals: [], approvedPartyIds: new Set() }
1364
+ : validateReleaseApprovals(record, record.milestone_evidence.verification);
1365
+ if (COMPLETE_RELEASE_APPROVAL_STATES.has(record.state)
1366
+ && releaseApprovalResult.approvedPartyIds.size
1367
+ !== record.profile.required_release_approver_party_ids.length) {
1368
+ fail('required release approval is missing');
1369
+ }
1370
+
1371
+ const hasRelease = record.release !== null;
1372
+ if (REQUIRED_RELEASE_STATES.has(record.state) && !hasRelease) {
1373
+ fail('required release artifacts are missing');
1374
+ }
1375
+ if (!REQUIRED_RELEASE_STATES.has(record.state)
1376
+ && !OPTIONAL_RELEASE_STATES.has(record.state)
1377
+ && hasRelease) {
1378
+ fail('stage carries unexpected release artifacts');
1379
+ }
1380
+ const release = hasRelease ? validateRelease(record) : null;
1381
+
1382
+ validateAncillaryState(record);
1383
+ validatePendingAmendment(record);
1384
+
1385
+ return {
1386
+ requiresExecutedAgreement,
1387
+ agreementAcceptances,
1388
+ fundingStatement,
1389
+ milestones: milestone === null ? [] : [milestone],
1390
+ releaseApprovals: releaseApprovalResult.approvals,
1391
+ release,
1392
+ };
1393
+ }
1394
+
1395
+ function validateLifecycleOperationCoverage(record) {
1396
+ const count = (name) => record.operations
1397
+ .filter((operation) => operation.operation === name).length;
1398
+ const requireExact = (name, expected) => {
1399
+ if (count(name) !== expected) {
1400
+ fail(`kernel ${name} operation coverage does not match its artifacts`);
1401
+ }
1402
+ };
1403
+ const acceptanceCount = record.profile.required_acceptance_party_ids.length;
1404
+ const expectedInitialAcceptances = record.superseded_bindings.length > 0
1405
+ ? acceptanceCount
1406
+ : record.agreement_acceptances.length;
1407
+ const expectedAmendmentAcceptances =
1408
+ record.superseded_bindings.length * acceptanceCount
1409
+ + (Array.isArray(record.pending_amendment?.agreement_acceptances)
1410
+ ? record.pending_amendment.agreement_acceptances.length
1411
+ : 0);
1412
+
1413
+ requireExact('create', 1);
1414
+ requireExact(
1415
+ 'begin_acceptance',
1416
+ record.history.some((entry) => entry.to === 'awaiting_acceptance') ? 1 : 0,
1417
+ );
1418
+ requireExact('accept_agreement', expectedInitialAcceptances);
1419
+ requireExact(
1420
+ 'request_funding',
1421
+ record.history.some((entry) => entry.to === 'awaiting_funding') ? 1 : 0,
1422
+ );
1423
+ requireExact('record_funding', record.funding === null ? 0 : 1);
1424
+ requireExact('submit_milestone', record.milestone_evidence === null ? 0 : 1);
1425
+ requireExact('approve_release', record.release_approvals.length);
1426
+ requireExact(
1427
+ 'propose_amendment',
1428
+ record.superseded_bindings.length + (record.pending_amendment === null ? 0 : 1),
1429
+ );
1430
+ requireExact('accept_amendment', expectedAmendmentAcceptances);
1431
+ requireExact('open_dispute', record.dispute === null ? 0 : 1);
1432
+ requireExact('cancel', record.cancellation === null ? 0 : 1);
1433
+ requireExact('complete', record.completion === null ? 0 : 1);
1434
+ if ((record.release === null) !== (count('release') === 0)) {
1435
+ fail('kernel release operation coverage does not match its artifacts');
1436
+ }
1437
+ if (record.release === null && count('reconcile_release') !== 0) {
1438
+ fail('kernel reconciliation operation has no release artifact');
1439
+ }
1440
+ }
1441
+
1442
+ function validateKernelRecord(record) {
1443
+ if (!exactKeys(record, RECORD_KEYS)
1444
+ || record['@version'] !== ACTION_ESCROW_STATE_VERSION
1445
+ || !Number.isSafeInteger(record.revision)
1446
+ || record.revision < 0
1447
+ || !ACTION_ESCROW_STATES.includes(record.state)
1448
+ || !validString(record.escrow_key, 256)
1449
+ || !validDigest(record.agreement_digest)
1450
+ || !validDigest(record.document_action_binding_digest)
1451
+ || !validString(record.milestone_id, 256)
1452
+ || !validDigest(record.release_action_digest)
1453
+ || !validDigest(record.parties_digest)
1454
+ || !validDigest(record.profile_digest)
1455
+ || !validInstant(record.created_at)
1456
+ || !validInstant(record.updated_at)) {
1457
+ fail('kernel record is malformed');
1458
+ }
1459
+ const expectedEscrowKey = `ep-action-escrow:${hashCanonical({
1460
+ '@version': 'EP-ACTION-ESCROW-KEY-v1',
1461
+ agreement_digest: record.agreement_digest,
1462
+ milestone_id: record.milestone_id,
1463
+ })}`;
1464
+ if (record.escrow_key !== expectedEscrowKey) fail('kernel escrow key mismatch');
1465
+ validatePartiesAndProfile(record);
1466
+ validateHistory(record);
1467
+ }
1468
+
1469
+ function validateDocumentExecution(value, expected, required) {
1470
+ if (value === null) {
1471
+ if (required) fail('required document-execution artifact is missing');
1472
+ return null;
1473
+ }
1474
+ if (!isRecord(value)
1475
+ || value.agreement_id !== expected.agreementId
1476
+ || value.binding_digest !== expected.bindingDigest
1477
+ || value.document_digest !== expected.documentDigest
1478
+ || value.authorizes_action !== false
1479
+ || (value.state !== undefined && value.state !== 'executed')) {
1480
+ fail('document-execution artifact does not match the kernel record');
1481
+ }
1482
+ return value;
1483
+ }
1484
+
1485
+ function validateProfileReference(value, record) {
1486
+ if (!exactKeys(value, PROFILE_REFERENCE_KEYS)
1487
+ || value.id !== record.profile.profile_id
1488
+ || value.digest !== record.profile_digest) {
1489
+ fail('verification-profile reference mismatch');
1490
+ }
1491
+ }
1492
+
1493
+ function stateStatementDigest(statement) {
1494
+ const body = {
1495
+ version: statement.version,
1496
+ issuer: statement.issuer,
1497
+ payload: statement.payload,
1498
+ };
1499
+ const bytes = Buffer.from(
1500
+ ACTION_ESCROW_STATE_STATEMENT_DOMAIN + canonicalize(body),
1501
+ 'utf8',
1502
+ );
1503
+ return `sha256:${crypto.createHash('sha256').update(bytes).digest('hex')}`;
1504
+ }
1505
+
1506
+ function validateStateStatement(
1507
+ statement,
1508
+ record,
1509
+ agreementId,
1510
+ amendmentDigests,
1511
+ assembledAt,
1512
+ ) {
1513
+ if (!exactKeys(statement, STATE_STATEMENT_KEYS)
1514
+ || statement.version !== ACTION_ESCROW_STATE_STATEMENT_VERSION
1515
+ || !exactKeys(statement.issuer, STATE_ISSUER_KEYS)
1516
+ || !validString(statement.issuer.operator_id, 256)
1517
+ || !validString(statement.issuer.key_id, 256)
1518
+ || !exactKeys(statement.payload, STATE_PAYLOAD_KEYS)
1519
+ || !validString(statement.payload.statement_id, 256)
1520
+ || statement.payload.agreement_id !== agreementId
1521
+ || statement.payload.binding_digest !== record.document_action_binding_digest
1522
+ || statement.payload.action_digest !== record.release_action_digest
1523
+ || statement.payload.profile_digest !== record.profile_digest
1524
+ || statement.payload.state !== record.state
1525
+ || statement.payload.revision !== record.revision
1526
+ || !Array.isArray(statement.payload.amendment_digests)
1527
+ || statement.payload.amendment_digests.length !== amendmentDigests.length
1528
+ || statement.payload.amendment_digests.some(
1529
+ (digest, index) => digest !== amendmentDigests[index],
1530
+ )
1531
+ || statement.payload.state_record_digest !== canonicalDigest(record)
1532
+ || (statement.payload.previous_statement_digest !== null
1533
+ && !validDigest(statement.payload.previous_statement_digest))
1534
+ || !validInstant(statement.payload.occurred_at)
1535
+ || Date.parse(statement.payload.occurred_at) > assembledAt
1536
+ || !exactKeys(statement.signature, STATE_SIGNATURE_KEYS)
1537
+ || statement.signature.algorithm !== 'Ed25519'
1538
+ || typeof statement.signature.signature_b64u !== 'string'
1539
+ || !BASE64URL.test(statement.signature.signature_b64u)
1540
+ || statement.signature.signature_b64u.length % 4 === 1
1541
+ || !validDigest(statement.statement_digest)
1542
+ || statement.statement_digest !== stateStatementDigest(statement)) {
1543
+ fail('operator state statement mismatch');
1544
+ }
1545
+ }
1546
+
1547
+ function finalPdfBytes(value, maxDocumentBytes) {
1548
+ if (!(Buffer.isBuffer(value) || value instanceof Uint8Array)) {
1549
+ fail('finalPdfBytes must be bytes');
1550
+ }
1551
+ const bytes = Buffer.from(value);
1552
+ if (bytes.length === 0 || bytes.length > maxDocumentBytes) {
1553
+ fail(`finalPdfBytes must be between 1 and ${maxDocumentBytes} bytes`);
1554
+ }
1555
+ if (!bytes.subarray(0, 5).equals(Buffer.from('%PDF-', 'ascii'))
1556
+ || !bytes.subarray(Math.max(0, bytes.length - 1024)).includes(Buffer.from('%%EOF'))) {
1557
+ fail('finalPdfBytes is not a final PDF');
1558
+ }
1559
+ return bytes;
1560
+ }
1561
+
1562
+ function evaluationInstant(value) {
1563
+ const candidate = typeof value === 'function' ? value() : value;
1564
+ const parsed = candidate instanceof Date
1565
+ ? candidate.getTime()
1566
+ : typeof candidate === 'number'
1567
+ ? candidate
1568
+ : Date.parse(candidate);
1569
+ if (!Number.isFinite(parsed)) fail('now must be a valid instant');
1570
+ return parsed;
1571
+ }
1572
+
1573
+ /**
1574
+ * Assemble an EP-ACTION-ESCROW-EVIDENCE-PACKAGE-v1 from one exact durable
1575
+ * kernel record and the external artifacts the kernel deliberately does not
1576
+ * retain. Invalid local assembly input throws before a package is produced.
1577
+ */
1578
+ export function assembleActionEscrowEvidencePackage(input = {}, options = {}) {
1579
+ if (!exactKeys(input, INPUT_KEYS)) {
1580
+ fail('caller overrides are not accepted; use the closed assembler input');
1581
+ }
1582
+ if (!exactKeys(options, OPTION_KEYS, new Set())) {
1583
+ fail('caller overrides are not accepted in assembler options');
1584
+ }
1585
+ const maxDocumentBytes = options.maxDocumentBytes ?? DEFAULT_MAX_DOCUMENT_BYTES;
1586
+ if (!Number.isSafeInteger(maxDocumentBytes) || maxDocumentBytes <= 0) {
1587
+ fail('maxDocumentBytes must be a positive safe integer');
1588
+ }
1589
+ const assembledAt = evaluationInstant(options.now ?? Date.now);
1590
+ const bytes = finalPdfBytes(input.finalPdfBytes, maxDocumentBytes);
1591
+ const record = canonicalCopy(input.kernelRecord, 'kernelRecord');
1592
+ const documentExecution = input.documentExecution === null
1593
+ ? null
1594
+ : canonicalCopy(input.documentExecution, 'documentExecution');
1595
+ const stateStatement = canonicalCopy(
1596
+ input.operatorStateStatement,
1597
+ 'operatorStateStatement',
1598
+ );
1599
+ const verificationProfile = canonicalCopy(
1600
+ input.verificationProfile,
1601
+ 'verificationProfile',
1602
+ );
1603
+
1604
+ assertNoSecrets(record, 'kernelRecord');
1605
+ assertNoSecrets(documentExecution, 'documentExecution');
1606
+ assertNoSecrets(stateStatement, 'operatorStateStatement');
1607
+ assertNoSecrets(verificationProfile, 'verificationProfile');
1608
+
1609
+ validateKernelRecord(record);
1610
+ if (Date.parse(record.updated_at) > assembledAt) {
1611
+ fail('kernel record is from the future');
1612
+ }
1613
+ const supersessions = validateSupersessions(record);
1614
+ const documentDigest = computeDocumentSha256(bytes);
1615
+ const binding = validateBindingContainer(record.document_action_binding, record, {
1616
+ supersedesDigest: supersessions.currentSupersedes,
1617
+ documentDigest,
1618
+ documentLength: bytes.length,
1619
+ });
1620
+ const lifecycle = validateLifecycle(record);
1621
+ const execution = validateDocumentExecution(
1622
+ documentExecution,
1623
+ {
1624
+ agreementId: binding.agreementId,
1625
+ bindingDigest: record.document_action_binding_digest,
1626
+ documentDigest,
1627
+ },
1628
+ lifecycle.requiresExecutedAgreement,
1629
+ );
1630
+ validateProfileReference(verificationProfile, record);
1631
+ validateStateStatement(
1632
+ stateStatement,
1633
+ record,
1634
+ binding.agreementId,
1635
+ supersessions.amendmentDigests,
1636
+ assembledAt,
1637
+ );
1638
+
1639
+ return buildActionEscrowEvidencePackage({
1640
+ agreementId: binding.agreementId,
1641
+ stage: record.state,
1642
+ binding: binding.binding,
1643
+ documentBytes: bytes,
1644
+ documentFileName: input.documentFileName,
1645
+ documentExecution: execution,
1646
+ agreementAcceptances: lifecycle.agreementAcceptances,
1647
+ releaseApprovals: lifecycle.releaseApprovals,
1648
+ fundingStatement: lifecycle.fundingStatement,
1649
+ milestones: lifecycle.milestones,
1650
+ release: lifecycle.release,
1651
+ stateRecord: {
1652
+ snapshot: record,
1653
+ statement: stateStatement,
1654
+ },
1655
+ amendments: supersessions.amendments,
1656
+ verificationProfile,
1657
+ }, {
1658
+ now: assembledAt,
1659
+ maxDocumentBytes,
1660
+ });
1661
+ }
1662
+
1663
+ export const buildActionEscrowEvidencePackageFromKernel =
1664
+ assembleActionEscrowEvidencePackage;
1665
+
1666
+ export default {
1667
+ assembleActionEscrowEvidencePackage,
1668
+ buildActionEscrowEvidencePackageFromKernel,
1669
+ };