@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,859 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * EP-ACTION-ESCROW-EVIDENCE-PACKAGE-v1
4
+ *
5
+ * A content-addressed manifest for the artifacts used by Action Escrow. This
6
+ * module does not decide whether a contract is enforceable and does not move
7
+ * money. Verification replays caller-supplied, relying-party-owned component
8
+ * verifiers so a package cannot make an invalid nested artifact trustworthy by
9
+ * merely hashing it.
10
+ */
11
+ import crypto from 'node:crypto';
12
+ import { hashCanonical } from './execution-binding.js';
13
+ import { strictJsonGate } from './strict-json.js';
14
+
15
+ export const ACTION_ESCROW_EVIDENCE_PACKAGE_VERSION = 'EP-ACTION-ESCROW-EVIDENCE-PACKAGE-v1';
16
+ export const ACTION_ESCROW_EVIDENCE_STAGES = Object.freeze([
17
+ 'draft',
18
+ 'awaiting_acceptance',
19
+ 'effective',
20
+ 'awaiting_funding',
21
+ 'funded',
22
+ 'milestone_submitted',
23
+ 'release_reserved',
24
+ 'released',
25
+ 'disputed',
26
+ 'amendment_pending',
27
+ 'cancelled',
28
+ 'completed',
29
+ 'release_indeterminate',
30
+ ]);
31
+
32
+ const HASH = /^sha256:[0-9a-f]{64}$/;
33
+ const TOP_KEYS = new Set([
34
+ 'version',
35
+ 'agreement_id',
36
+ 'stage',
37
+ 'binding',
38
+ 'document',
39
+ 'document_execution',
40
+ 'agreement_acceptances',
41
+ 'release_approvals',
42
+ 'funding_statement',
43
+ 'milestones',
44
+ 'release',
45
+ 'state_record',
46
+ 'amendments',
47
+ 'verification_profile',
48
+ 'assembled_at',
49
+ 'limitations',
50
+ 'package_digest',
51
+ ]);
52
+ const DOCUMENT_KEYS = new Set(['media_type', 'digest', 'byte_length', 'file_name']);
53
+ const PARTY_EVIDENCE_KEYS = new Set(['party_id', 'role', 'evidence']);
54
+ const MILESTONE_KEYS = new Set(['milestone_id', 'evidence', 'resolution']);
55
+ const RELEASE_KEYS = new Set([
56
+ 'reservation',
57
+ 'provider_request',
58
+ 'provider_statement',
59
+ 'execution_record',
60
+ ]);
61
+ const STATE_RECORD_KEYS = new Set(['snapshot', 'statement']);
62
+ const LIMITATIONS = Object.freeze([
63
+ 'The package does not establish contract enforceability, comprehension, voluntariness, workmanship, physical truth, or legal compliance.',
64
+ 'The package does not establish custodian licensing, solvency, or that no payment path existed outside the integrated release boundary.',
65
+ 'A content digest does not upgrade an invalid component; each component must verify under relying-party-pinned trust roots.',
66
+ 'An indeterminate provider effect is not proof of failure and must not be retried before authoritative reconciliation.',
67
+ ]);
68
+ const DEFAULT_MAX_DOCUMENT_BYTES = 50 * 1024 * 1024;
69
+ const DEFAULT_MAX_PACKAGE_BYTES = 4 * 1024 * 1024;
70
+
71
+ function isRecord(value) {
72
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
73
+ }
74
+
75
+ function exactKeys(value, allowed, required = allowed) {
76
+ if (!isRecord(value)) return false;
77
+ const keys = Object.keys(value);
78
+ return keys.every((key) => allowed.has(key))
79
+ && [...required].every((key) => Object.hasOwn(value, key));
80
+ }
81
+
82
+ function toIso(value) {
83
+ const candidate = typeof value === 'function' ? value() : value;
84
+ const date = candidate instanceof Date ? candidate : new Date(candidate ?? 0);
85
+ if (!Number.isFinite(date.getTime())) {
86
+ throw new TypeError('action-escrow evidence: assembled_at must be a valid instant');
87
+ }
88
+ return date.toISOString();
89
+ }
90
+
91
+ function portableJsonCopy(value) {
92
+ let nodes = 0;
93
+ let stringBytes = 0;
94
+ const active = new Set();
95
+
96
+ function copy(current, depth) {
97
+ nodes += 1;
98
+ if (nodes > 50_000 || depth > 64) {
99
+ throw new TypeError('action-escrow evidence: value exceeds resource limits');
100
+ }
101
+ if (current === null || typeof current === 'boolean') return current;
102
+ if (typeof current === 'string') {
103
+ stringBytes += Buffer.byteLength(current, 'utf8');
104
+ if (stringBytes > 4 * 1024 * 1024) {
105
+ throw new TypeError('action-escrow evidence: strings exceed resource limits');
106
+ }
107
+ return current;
108
+ }
109
+ if (typeof current === 'number') {
110
+ if (!Number.isSafeInteger(current) || Object.is(current, -0)) {
111
+ throw new TypeError('action-escrow evidence: numbers must be safe integers');
112
+ }
113
+ return current;
114
+ }
115
+ if (!isRecord(current) && !Array.isArray(current)) {
116
+ throw new TypeError('action-escrow evidence: value is not canonical JSON');
117
+ }
118
+ if (active.has(current)) {
119
+ throw new TypeError('action-escrow evidence: cyclic or aliased value');
120
+ }
121
+ active.add(current);
122
+ try {
123
+ if (Array.isArray(current)) return current.map((entry) => copy(entry, depth + 1));
124
+ const prototype = Object.getPrototypeOf(current);
125
+ if (prototype !== Object.prototype && prototype !== null) {
126
+ throw new TypeError('action-escrow evidence: objects must use a plain prototype');
127
+ }
128
+ return Object.fromEntries(
129
+ Object.entries(current).map(([key, entry]) => [key, copy(entry, depth + 1)]),
130
+ );
131
+ } finally {
132
+ active.delete(current);
133
+ }
134
+ }
135
+
136
+ return copy(value, 0);
137
+ }
138
+
139
+ function deepFreeze(value) {
140
+ if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
141
+ Object.freeze(value);
142
+ for (const child of Object.values(value)) deepFreeze(child);
143
+ return value;
144
+ }
145
+
146
+ function documentBytes(value, maxBytes) {
147
+ if (!(Buffer.isBuffer(value) || value instanceof Uint8Array)) {
148
+ throw new TypeError('action-escrow evidence: documentBytes must be bytes');
149
+ }
150
+ const bytes = Buffer.from(value);
151
+ if (bytes.length === 0 || bytes.length > maxBytes) {
152
+ throw new TypeError(`action-escrow evidence: documentBytes must be between 1 and ${maxBytes} bytes`);
153
+ }
154
+ return bytes;
155
+ }
156
+
157
+ function sha256(bytes) {
158
+ return `sha256:${crypto.createHash('sha256').update(bytes).digest('hex')}`;
159
+ }
160
+
161
+ function validInstant(value) {
162
+ if (typeof value !== 'string') return false;
163
+ const parsed = Date.parse(value);
164
+ return Number.isFinite(parsed) && new Date(parsed).toISOString() === value;
165
+ }
166
+
167
+ function canonicalSha256(value) {
168
+ return `sha256:${hashCanonical(value)}`;
169
+ }
170
+
171
+ function digestScope(pkg) {
172
+ const { package_digest: _digest, ...scope } = pkg;
173
+ return scope;
174
+ }
175
+
176
+ /** Strict raw parser for security-bearing package transport. */
177
+ export function parseActionEscrowEvidencePackage(raw, {
178
+ maxBytes = DEFAULT_MAX_PACKAGE_BYTES,
179
+ } = {}) {
180
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
181
+ return { ok: false, reason: 'invalid_package_limit', value: null };
182
+ }
183
+ if (typeof raw !== 'string') {
184
+ return { ok: false, reason: 'package_must_be_json_text', value: null };
185
+ }
186
+ if (Buffer.byteLength(raw, 'utf8') > maxBytes) {
187
+ return { ok: false, reason: 'package_exceeds_size_limit', value: null };
188
+ }
189
+ const gated = strictJsonGate(raw);
190
+ if (!gated.ok) return { ok: false, reason: gated.reason, value: null };
191
+ try {
192
+ return { ok: true, reason: 'parsed', value: JSON.parse(raw) };
193
+ } catch {
194
+ return { ok: false, reason: 'invalid_json_syntax', value: null };
195
+ }
196
+ }
197
+
198
+ function normalizedRequiredParties(value) {
199
+ if (!Array.isArray(value) || value.length === 0) return null;
200
+ const result = [];
201
+ for (const entry of value) {
202
+ if (typeof entry === 'string' && entry.length > 0) {
203
+ result.push({ party_id: entry, role: null });
204
+ continue;
205
+ }
206
+ if (!isRecord(entry)
207
+ || typeof entry.party_id !== 'string' || entry.party_id.length === 0
208
+ || typeof entry.role !== 'string' || entry.role.length === 0) {
209
+ return null;
210
+ }
211
+ result.push({ party_id: entry.party_id, role: entry.role });
212
+ }
213
+ const identities = result.map((entry) => `${entry.role ?? ''}\u0000${entry.party_id}`);
214
+ return new Set(identities).size === identities.length ? result : null;
215
+ }
216
+
217
+ function stageRequiresExecutedAgreement(stage) {
218
+ return [
219
+ 'effective',
220
+ 'awaiting_funding',
221
+ 'funded',
222
+ 'milestone_submitted',
223
+ 'release_reserved',
224
+ 'released',
225
+ 'disputed',
226
+ 'completed',
227
+ 'release_indeterminate',
228
+ ].includes(stage);
229
+ }
230
+
231
+ function stageRequiresReleaseApprovals(stage) {
232
+ return ['release_reserved', 'released', 'completed', 'release_indeterminate'].includes(stage);
233
+ }
234
+
235
+ function stageRequiresFunding(stage) {
236
+ return [
237
+ 'funded',
238
+ 'milestone_submitted',
239
+ 'release_reserved',
240
+ 'released',
241
+ 'disputed',
242
+ 'completed',
243
+ 'release_indeterminate',
244
+ ].includes(stage);
245
+ }
246
+
247
+ function stageAllowsFunding(stage) {
248
+ return [
249
+ 'funded',
250
+ 'milestone_submitted',
251
+ 'release_reserved',
252
+ 'released',
253
+ 'disputed',
254
+ 'completed',
255
+ 'release_indeterminate',
256
+ ].includes(stage);
257
+ }
258
+
259
+ function stageRequiresMilestone(stage) {
260
+ return [
261
+ 'milestone_submitted',
262
+ 'release_reserved',
263
+ 'released',
264
+ 'disputed',
265
+ 'completed',
266
+ 'release_indeterminate',
267
+ ].includes(stage);
268
+ }
269
+
270
+ function stageAllowsMilestone(stage) {
271
+ return [
272
+ 'milestone_submitted',
273
+ 'release_reserved',
274
+ 'released',
275
+ 'disputed',
276
+ 'completed',
277
+ 'release_indeterminate',
278
+ ].includes(stage);
279
+ }
280
+
281
+ function stageAllowsReleaseApprovals(stage) {
282
+ return [
283
+ 'milestone_submitted',
284
+ 'release_reserved',
285
+ 'released',
286
+ 'disputed',
287
+ 'completed',
288
+ 'release_indeterminate',
289
+ ].includes(stage);
290
+ }
291
+
292
+ function stageRequiresRelease(stage) {
293
+ return ['release_reserved', 'released', 'completed', 'release_indeterminate'].includes(stage);
294
+ }
295
+
296
+ function stageAllowsRelease(stage) {
297
+ return [
298
+ 'milestone_submitted',
299
+ 'release_reserved',
300
+ 'released',
301
+ 'disputed',
302
+ 'amendment_pending',
303
+ 'completed',
304
+ 'release_indeterminate',
305
+ ].includes(stage);
306
+ }
307
+
308
+ /**
309
+ * Build a portable evidence manifest. The final document bytes are hashed but
310
+ * not embedded; transport them beside the JSON manifest.
311
+ */
312
+ export function buildActionEscrowEvidencePackage({
313
+ agreementId,
314
+ stage,
315
+ binding,
316
+ documentBytes: rawDocumentBytes,
317
+ documentFileName = null,
318
+ documentExecution = null,
319
+ agreementAcceptances = [],
320
+ releaseApprovals = [],
321
+ fundingStatement = null,
322
+ milestones = [],
323
+ release = null,
324
+ stateRecord,
325
+ amendments = [],
326
+ verificationProfile,
327
+ } = {}, {
328
+ now = 0,
329
+ maxDocumentBytes = DEFAULT_MAX_DOCUMENT_BYTES,
330
+ } = {}) {
331
+ if (typeof agreementId !== 'string' || agreementId.length === 0) {
332
+ throw new TypeError('action-escrow evidence: agreementId is required');
333
+ }
334
+ if (!ACTION_ESCROW_EVIDENCE_STAGES.includes(stage)) {
335
+ throw new TypeError('action-escrow evidence: stage is not in the closed set');
336
+ }
337
+ if (!Number.isSafeInteger(maxDocumentBytes) || maxDocumentBytes <= 0) {
338
+ throw new TypeError('action-escrow evidence: maxDocumentBytes must be a positive safe integer');
339
+ }
340
+ const bytes = documentBytes(rawDocumentBytes, maxDocumentBytes);
341
+ if (documentFileName !== null
342
+ && (typeof documentFileName !== 'string'
343
+ || documentFileName.length === 0
344
+ || documentFileName.length > 255
345
+ || /[/\\\u0000]/.test(documentFileName))) {
346
+ throw new TypeError('action-escrow evidence: documentFileName is invalid');
347
+ }
348
+
349
+ const body = portableJsonCopy({
350
+ version: ACTION_ESCROW_EVIDENCE_PACKAGE_VERSION,
351
+ agreement_id: agreementId,
352
+ stage,
353
+ binding,
354
+ document: {
355
+ media_type: 'application/pdf',
356
+ digest: sha256(bytes),
357
+ byte_length: bytes.length,
358
+ ...(documentFileName === null ? {} : { file_name: documentFileName }),
359
+ },
360
+ document_execution: documentExecution,
361
+ agreement_acceptances: agreementAcceptances,
362
+ release_approvals: releaseApprovals,
363
+ funding_statement: fundingStatement,
364
+ milestones,
365
+ release: release ?? {
366
+ reservation: null,
367
+ provider_request: null,
368
+ provider_statement: null,
369
+ execution_record: null,
370
+ },
371
+ state_record: stateRecord,
372
+ amendments,
373
+ verification_profile: verificationProfile,
374
+ assembled_at: toIso(now),
375
+ limitations: [...LIMITATIONS],
376
+ });
377
+
378
+ return deepFreeze({
379
+ ...body,
380
+ package_digest: canonicalSha256(body),
381
+ });
382
+ }
383
+
384
+ function resultFailure(reason, checks, details = {}) {
385
+ return {
386
+ valid: false,
387
+ reason,
388
+ checks,
389
+ ...details,
390
+ };
391
+ }
392
+
393
+ async function callVerifier(verifier, value, context) {
394
+ if (typeof verifier !== 'function') return { valid: false, reason: 'verifier_required' };
395
+ try {
396
+ const result = await verifier(value, context);
397
+ return isRecord(result) ? result : { valid: false, reason: 'malformed_verifier_result' };
398
+ } catch {
399
+ return { valid: false, reason: 'verifier_threw' };
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Re-perform every package join using relying-party-owned component verifiers.
405
+ *
406
+ * Component verifiers are configuration, never read from the package. Their
407
+ * returned binding fields are checked again here so a valid artifact for one
408
+ * agreement, document, party, or action cannot be relabeled into another slot.
409
+ */
410
+ export async function verifyActionEscrowEvidencePackage(pkg, {
411
+ documentBytes: rawDocumentBytes,
412
+ verifyBinding,
413
+ verifyProfile,
414
+ verifyDocumentExecution,
415
+ verifyAgreementAcceptance,
416
+ verifyReleaseApproval,
417
+ verifyFunding,
418
+ verifyMilestone,
419
+ verifyRelease,
420
+ verifyAmendment,
421
+ verifyState,
422
+ expectedAgreementId,
423
+ now,
424
+ maxDocumentBytes = DEFAULT_MAX_DOCUMENT_BYTES,
425
+ } = {}) {
426
+ const checks = {
427
+ structure: false,
428
+ package_digest: false,
429
+ time: false,
430
+ document: false,
431
+ binding: false,
432
+ profile: false,
433
+ document_execution: false,
434
+ agreement_acceptances: false,
435
+ amendments: false,
436
+ state: false,
437
+ release_approvals: false,
438
+ funding: false,
439
+ milestones: false,
440
+ release: false,
441
+ };
442
+
443
+ try {
444
+ const requiredTop = new Set(TOP_KEYS);
445
+ checks.structure = exactKeys(pkg, TOP_KEYS, requiredTop)
446
+ && pkg.version === ACTION_ESCROW_EVIDENCE_PACKAGE_VERSION
447
+ && typeof pkg.agreement_id === 'string' && pkg.agreement_id.length > 0
448
+ && ACTION_ESCROW_EVIDENCE_STAGES.includes(pkg.stage)
449
+ && HASH.test(pkg.package_digest)
450
+ && exactKeys(pkg.document, DOCUMENT_KEYS, new Set(['media_type', 'digest', 'byte_length']))
451
+ && pkg.document.media_type === 'application/pdf'
452
+ && HASH.test(pkg.document.digest)
453
+ && Number.isSafeInteger(pkg.document.byte_length) && pkg.document.byte_length > 0
454
+ && (pkg.document_execution === null || isRecord(pkg.document_execution))
455
+ && Array.isArray(pkg.agreement_acceptances)
456
+ && Array.isArray(pkg.release_approvals)
457
+ && Array.isArray(pkg.milestones)
458
+ && exactKeys(pkg.release, RELEASE_KEYS)
459
+ && exactKeys(pkg.state_record, STATE_RECORD_KEYS)
460
+ && Array.isArray(pkg.amendments)
461
+ && Array.isArray(pkg.limitations)
462
+ && pkg.limitations.length === LIMITATIONS.length
463
+ && pkg.limitations.every((entry, index) => entry === LIMITATIONS[index]);
464
+ if (!checks.structure) return resultFailure('malformed_evidence_package', checks);
465
+
466
+ if (expectedAgreementId !== undefined
467
+ && (typeof expectedAgreementId !== 'string' || pkg.agreement_id !== expectedAgreementId)) {
468
+ return resultFailure('agreement_id_mismatch', checks);
469
+ }
470
+
471
+ checks.package_digest = canonicalSha256(digestScope(pkg)) === pkg.package_digest;
472
+ if (!checks.package_digest) return resultFailure('package_digest_mismatch', checks);
473
+
474
+ const assembledAt = Date.parse(pkg.assembled_at);
475
+ const evaluation = now === undefined
476
+ ? null
477
+ : (now instanceof Date ? now.getTime() : typeof now === 'number' ? now : Date.parse(now));
478
+ checks.time = Number.isFinite(assembledAt)
479
+ && (evaluation === null || (Number.isFinite(evaluation) && assembledAt <= evaluation));
480
+ if (!checks.time) return resultFailure('invalid_or_future_assembled_at', checks);
481
+
482
+ if (!Number.isSafeInteger(maxDocumentBytes) || maxDocumentBytes <= 0) {
483
+ return resultFailure('invalid_document_limit', checks);
484
+ }
485
+ const bytes = documentBytes(rawDocumentBytes, maxDocumentBytes);
486
+ checks.document = bytes.length === pkg.document.byte_length && sha256(bytes) === pkg.document.digest;
487
+ if (!checks.document) return resultFailure('document_bytes_mismatch', checks);
488
+
489
+ const bindingResult = await callVerifier(verifyBinding, pkg.binding, {
490
+ expectedAgreementId: pkg.agreement_id,
491
+ expectedDocumentDigest: pkg.document.digest,
492
+ });
493
+ const requiredParties = normalizedRequiredParties(bindingResult.required_parties);
494
+ checks.binding = bindingResult.valid === true
495
+ && bindingResult.agreement_id === pkg.agreement_id
496
+ && bindingResult.document_digest === pkg.document.digest
497
+ && HASH.test(bindingResult.binding_digest)
498
+ && HASH.test(bindingResult.action_digest)
499
+ && (bindingResult.supersedes_digest === null || HASH.test(bindingResult.supersedes_digest))
500
+ && requiredParties !== null;
501
+ if (!checks.binding) {
502
+ return resultFailure('binding_verification_failed', checks, {
503
+ binding_reason: bindingResult.reason ?? null,
504
+ });
505
+ }
506
+
507
+ const profileResult = await callVerifier(verifyProfile, pkg.verification_profile, {
508
+ agreementId: pkg.agreement_id,
509
+ bindingDigest: bindingResult.binding_digest,
510
+ actionDigest: bindingResult.action_digest,
511
+ });
512
+ const requiredReleaseParties = normalizedRequiredParties(
513
+ profileResult.required_release_parties,
514
+ );
515
+ checks.profile = profileResult.valid === true
516
+ && profileResult.agreement_id === pkg.agreement_id
517
+ && profileResult.binding_digest === bindingResult.binding_digest
518
+ && profileResult.action_digest === bindingResult.action_digest
519
+ && HASH.test(profileResult.profile_digest)
520
+ && requiredReleaseParties !== null;
521
+ if (!checks.profile) {
522
+ return resultFailure('verification_profile_failed', checks, {
523
+ profile_reason: profileResult.reason ?? null,
524
+ });
525
+ }
526
+
527
+ if (pkg.document_execution !== null) {
528
+ const executionResult = await callVerifier(
529
+ verifyDocumentExecution,
530
+ pkg.document_execution,
531
+ {
532
+ agreementId: pkg.agreement_id,
533
+ bindingDigest: bindingResult.binding_digest,
534
+ documentDigest: pkg.document.digest,
535
+ },
536
+ );
537
+ checks.document_execution = executionResult.valid === true
538
+ && executionResult.authorizes_action === false
539
+ && executionResult.agreement_id === pkg.agreement_id
540
+ && executionResult.binding_digest === bindingResult.binding_digest
541
+ && executionResult.document_digest === pkg.document.digest
542
+ && executionResult.state === 'executed';
543
+ } else {
544
+ checks.document_execution = !stageRequiresExecutedAgreement(pkg.stage);
545
+ }
546
+ if (!checks.document_execution) {
547
+ return resultFailure('document_execution_verification_failed', checks);
548
+ }
549
+
550
+ const requiredAgreementByIdentity = new Map(
551
+ requiredParties.map((entry) => [`${entry.role ?? ''}\u0000${entry.party_id}`, entry]),
552
+ );
553
+ const seenAgreementParties = new Set();
554
+ const seenAgreementKeys = new Set();
555
+ let agreementAcceptancesValid = true;
556
+ for (const acceptance of pkg.agreement_acceptances) {
557
+ if (!exactKeys(acceptance, PARTY_EVIDENCE_KEYS)
558
+ || typeof acceptance.party_id !== 'string'
559
+ || typeof acceptance.role !== 'string') {
560
+ agreementAcceptancesValid = false;
561
+ break;
562
+ }
563
+ const identity = `${acceptance.role}\u0000${acceptance.party_id}`;
564
+ if (!requiredAgreementByIdentity.has(identity) || seenAgreementParties.has(identity)) {
565
+ agreementAcceptancesValid = false;
566
+ break;
567
+ }
568
+ const result = await callVerifier(
569
+ verifyAgreementAcceptance,
570
+ acceptance.evidence,
571
+ {
572
+ partyId: acceptance.party_id,
573
+ role: acceptance.role,
574
+ agreementId: pkg.agreement_id,
575
+ bindingDigest: bindingResult.binding_digest,
576
+ documentDigest: pkg.document.digest,
577
+ },
578
+ );
579
+ if (result.valid !== true
580
+ || result.accepts_agreement !== true
581
+ || result.authorizes_action !== false
582
+ || result.agreement_id !== pkg.agreement_id
583
+ || result.party_id !== acceptance.party_id
584
+ || result.role !== acceptance.role
585
+ || result.binding_digest !== bindingResult.binding_digest
586
+ || result.document_digest !== pkg.document.digest
587
+ || typeof result.principal_key_id !== 'string'
588
+ || result.principal_key_id.length === 0
589
+ || seenAgreementKeys.has(result.principal_key_id)) {
590
+ agreementAcceptancesValid = false;
591
+ break;
592
+ }
593
+ seenAgreementParties.add(identity);
594
+ seenAgreementKeys.add(result.principal_key_id);
595
+ }
596
+ if (stageRequiresExecutedAgreement(pkg.stage)) {
597
+ agreementAcceptancesValid = agreementAcceptancesValid
598
+ && seenAgreementParties.size === requiredAgreementByIdentity.size;
599
+ }
600
+ checks.agreement_acceptances = agreementAcceptancesValid;
601
+ if (!checks.agreement_acceptances) {
602
+ return resultFailure('agreement_acceptance_verification_failed', checks);
603
+ }
604
+
605
+ const amendmentResults = [];
606
+ let amendmentsValid = true;
607
+ let expectedNextBinding = bindingResult.binding_digest;
608
+ for (let index = pkg.amendments.length - 1; index >= 0; index -= 1) {
609
+ const result = await callVerifier(verifyAmendment, pkg.amendments[index], {
610
+ agreementId: pkg.agreement_id,
611
+ expectedNextBindingDigest: expectedNextBinding,
612
+ profileDigest: profileResult.profile_digest,
613
+ });
614
+ amendmentResults.unshift(result);
615
+ if (result.valid !== true
616
+ || !HASH.test(result.amendment_digest)
617
+ || !HASH.test(result.previous_binding_digest)
618
+ || result.next_binding_digest !== expectedNextBinding) {
619
+ amendmentsValid = false;
620
+ break;
621
+ }
622
+ expectedNextBinding = result.previous_binding_digest;
623
+ }
624
+ if (bindingResult.supersedes_digest === null) {
625
+ amendmentsValid = amendmentsValid && pkg.amendments.length === 0;
626
+ } else {
627
+ const finalAmendment = amendmentResults.at(-1);
628
+ amendmentsValid = amendmentsValid
629
+ && pkg.amendments.length > 0
630
+ && finalAmendment?.previous_binding_digest === bindingResult.supersedes_digest
631
+ && finalAmendment?.next_binding_digest === bindingResult.binding_digest;
632
+ }
633
+ checks.amendments = amendmentsValid;
634
+ if (!checks.amendments) {
635
+ return resultFailure('amendment_chain_verification_failed', checks);
636
+ }
637
+ const amendmentDigests = amendmentResults.map((result) => result.amendment_digest);
638
+
639
+ const stateResult = await callVerifier(verifyState, pkg.state_record, {
640
+ agreementId: pkg.agreement_id,
641
+ bindingDigest: bindingResult.binding_digest,
642
+ actionDigest: bindingResult.action_digest,
643
+ profileDigest: profileResult.profile_digest,
644
+ amendmentDigests,
645
+ stage: pkg.stage,
646
+ });
647
+ checks.state = stateResult.valid === true
648
+ && stateResult.agreement_id === pkg.agreement_id
649
+ && stateResult.binding_digest === bindingResult.binding_digest
650
+ && stateResult.action_digest === bindingResult.action_digest
651
+ && stateResult.profile_digest === profileResult.profile_digest
652
+ && stateResult.state === pkg.stage
653
+ && Number.isSafeInteger(stateResult.revision) && stateResult.revision >= 0
654
+ && Array.isArray(stateResult.amendment_digests)
655
+ && stateResult.amendment_digests.length === amendmentDigests.length
656
+ && stateResult.amendment_digests.every((digest, index) => digest === amendmentDigests[index]);
657
+ if (!checks.state) {
658
+ return resultFailure('state_record_verification_failed', checks, {
659
+ state_reason: stateResult.reason ?? null,
660
+ });
661
+ }
662
+
663
+ if (pkg.funding_statement !== null) {
664
+ if (!stageAllowsFunding(pkg.stage)) {
665
+ return resultFailure('unexpected_funding_statement', checks);
666
+ }
667
+ const fundingResult = await callVerifier(verifyFunding, pkg.funding_statement, {
668
+ agreementId: pkg.agreement_id,
669
+ bindingDigest: bindingResult.binding_digest,
670
+ actionDigest: bindingResult.action_digest,
671
+ });
672
+ checks.funding = fundingResult.valid === true
673
+ && fundingResult.agreement_id === pkg.agreement_id
674
+ && fundingResult.binding_digest === bindingResult.binding_digest
675
+ && fundingResult.action_digest === bindingResult.action_digest
676
+ && fundingResult.state === 'funded';
677
+ if (!checks.funding) {
678
+ return resultFailure('funding_statement_verification_failed', checks, {
679
+ funding_reason: fundingResult.reason ?? null,
680
+ });
681
+ }
682
+ } else {
683
+ checks.funding = !stageRequiresFunding(pkg.stage);
684
+ if (!checks.funding) return resultFailure('funding_statement_missing', checks);
685
+ }
686
+
687
+ let milestonesValid = true;
688
+ const milestoneIds = new Set();
689
+ const milestoneEvidenceDigests = [];
690
+ if (pkg.milestones.length > 0 && !stageAllowsMilestone(pkg.stage)) {
691
+ return resultFailure('unexpected_milestone_evidence', checks);
692
+ }
693
+ for (const milestone of pkg.milestones) {
694
+ if (!exactKeys(milestone, MILESTONE_KEYS)
695
+ || typeof milestone.milestone_id !== 'string' || milestone.milestone_id.length === 0
696
+ || milestoneIds.has(milestone.milestone_id)) {
697
+ milestonesValid = false;
698
+ break;
699
+ }
700
+ milestoneIds.add(milestone.milestone_id);
701
+ const result = await callVerifier(verifyMilestone, milestone, {
702
+ agreementId: pkg.agreement_id,
703
+ bindingDigest: bindingResult.binding_digest,
704
+ actionDigest: bindingResult.action_digest,
705
+ });
706
+ if (result.valid !== true
707
+ || result.agreement_id !== pkg.agreement_id
708
+ || result.milestone_id !== milestone.milestone_id
709
+ || result.binding_digest !== bindingResult.binding_digest
710
+ || result.action_digest !== bindingResult.action_digest
711
+ || !HASH.test(result.evidence_digest)) {
712
+ milestonesValid = false;
713
+ break;
714
+ }
715
+ milestoneEvidenceDigests.push(result.evidence_digest);
716
+ }
717
+ if (stageRequiresMilestone(pkg.stage)) milestonesValid = milestonesValid && milestoneIds.size > 0;
718
+ checks.milestones = milestonesValid;
719
+ if (!checks.milestones) return resultFailure('milestone_verification_failed', checks);
720
+
721
+ const requiredReleaseByIdentity = new Map(
722
+ requiredReleaseParties.map(
723
+ (entry) => [`${entry.role ?? ''}\u0000${entry.party_id}`, entry],
724
+ ),
725
+ );
726
+ const seenReleaseParties = new Set();
727
+ const seenReleaseKeys = new Set();
728
+ let releaseApprovalsValid = true;
729
+ if (pkg.release_approvals.length > 0 && !stageAllowsReleaseApprovals(pkg.stage)) {
730
+ return resultFailure('unexpected_release_approval', checks);
731
+ }
732
+ const releaseExecutionAt = pkg.release.execution_record?.at ?? null;
733
+ for (const [approvalIndex, approval] of pkg.release_approvals.entries()) {
734
+ if (!exactKeys(approval, PARTY_EVIDENCE_KEYS)
735
+ || typeof approval.party_id !== 'string'
736
+ || typeof approval.role !== 'string') {
737
+ releaseApprovalsValid = false;
738
+ break;
739
+ }
740
+ const identity = `${approval.role}\u0000${approval.party_id}`;
741
+ if (!requiredReleaseByIdentity.has(identity) || seenReleaseParties.has(identity)) {
742
+ releaseApprovalsValid = false;
743
+ break;
744
+ }
745
+ const result = await callVerifier(
746
+ verifyReleaseApproval,
747
+ approval.evidence,
748
+ {
749
+ partyId: approval.party_id,
750
+ role: approval.role,
751
+ agreementId: pkg.agreement_id,
752
+ bindingDigest: bindingResult.binding_digest,
753
+ actionDigest: bindingResult.action_digest,
754
+ milestoneEvidenceDigests,
755
+ approvalIndex,
756
+ stateRecord: pkg.state_record,
757
+ },
758
+ );
759
+ if (result.valid !== true
760
+ || result.authorizes_action !== true
761
+ || result.outcome !== 'approved'
762
+ || result.agreement_id !== pkg.agreement_id
763
+ || result.party_id !== approval.party_id
764
+ || result.role !== approval.role
765
+ || result.binding_digest !== bindingResult.binding_digest
766
+ || result.action_digest !== bindingResult.action_digest
767
+ || !Array.isArray(result.milestone_evidence_digests)
768
+ || result.milestone_evidence_digests.length !== milestoneEvidenceDigests.length
769
+ || result.milestone_evidence_digests.some(
770
+ (digest, index) => digest !== milestoneEvidenceDigests[index],
771
+ )
772
+ || typeof result.principal_key_id !== 'string'
773
+ || result.principal_key_id.length === 0
774
+ || !validInstant(result.issued_at)
775
+ || !validInstant(result.expires_at)
776
+ || !validInstant(result.admitted_at)
777
+ || Date.parse(result.issued_at) > Date.parse(result.admitted_at)
778
+ || Date.parse(result.expires_at) <= Date.parse(result.admitted_at)
779
+ || (
780
+ releaseExecutionAt !== null
781
+ && (
782
+ !validInstant(releaseExecutionAt)
783
+ || Date.parse(result.issued_at) > Date.parse(releaseExecutionAt)
784
+ || Date.parse(result.expires_at) <= Date.parse(releaseExecutionAt)
785
+ )
786
+ )
787
+ || seenReleaseKeys.has(result.principal_key_id)) {
788
+ releaseApprovalsValid = false;
789
+ break;
790
+ }
791
+ seenReleaseParties.add(identity);
792
+ seenReleaseKeys.add(result.principal_key_id);
793
+ }
794
+ if (stageRequiresReleaseApprovals(pkg.stage)) {
795
+ releaseApprovalsValid = releaseApprovalsValid
796
+ && seenReleaseParties.size === requiredReleaseByIdentity.size;
797
+ }
798
+ checks.release_approvals = releaseApprovalsValid;
799
+ if (!checks.release_approvals) {
800
+ return resultFailure('release_approval_verification_failed', checks);
801
+ }
802
+
803
+ const hasReleaseArtifacts = Object.values(pkg.release).some((entry) => entry !== null);
804
+ if (hasReleaseArtifacts) {
805
+ if (!stageAllowsRelease(pkg.stage)) {
806
+ return resultFailure('release_artifacts_not_allowed_for_stage', checks);
807
+ }
808
+ const releaseResult = await callVerifier(verifyRelease, pkg.release, {
809
+ agreementId: pkg.agreement_id,
810
+ bindingDigest: bindingResult.binding_digest,
811
+ actionDigest: bindingResult.action_digest,
812
+ stage: pkg.stage,
813
+ });
814
+ const expectedReleaseState = pkg.stage === 'release_indeterminate'
815
+ ? 'indeterminate'
816
+ : pkg.stage === 'release_reserved'
817
+ ? 'reserved'
818
+ : ['released', 'completed'].includes(pkg.stage)
819
+ ? 'released'
820
+ : 'not_released';
821
+ checks.release = releaseResult.valid === true
822
+ && releaseResult.agreement_id === pkg.agreement_id
823
+ && releaseResult.binding_digest === bindingResult.binding_digest
824
+ && releaseResult.action_digest === bindingResult.action_digest
825
+ && releaseResult.state === expectedReleaseState;
826
+ if (!checks.release) {
827
+ return resultFailure('release_verification_failed', checks, {
828
+ release_reason: releaseResult.reason ?? null,
829
+ });
830
+ }
831
+ } else {
832
+ checks.release = !stageRequiresRelease(pkg.stage);
833
+ if (!checks.release) return resultFailure('release_artifact_missing', checks);
834
+ }
835
+
836
+ return {
837
+ valid: true,
838
+ reason: 'verified',
839
+ checks,
840
+ package_digest: pkg.package_digest,
841
+ agreement_id: pkg.agreement_id,
842
+ binding_digest: bindingResult.binding_digest,
843
+ action_digest: bindingResult.action_digest,
844
+ profile_digest: profileResult.profile_digest,
845
+ required_parties: requiredParties,
846
+ required_release_parties: requiredReleaseParties,
847
+ };
848
+ } catch {
849
+ return resultFailure('malformed_evidence_package', checks);
850
+ }
851
+ }
852
+
853
+ export default {
854
+ ACTION_ESCROW_EVIDENCE_PACKAGE_VERSION,
855
+ ACTION_ESCROW_EVIDENCE_STAGES,
856
+ parseActionEscrowEvidencePackage,
857
+ buildActionEscrowEvidencePackage,
858
+ verifyActionEscrowEvidencePackage,
859
+ };