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