@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,2661 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Pure orchestration kernel for contractor milestone release.
|
|
5
|
+
*
|
|
6
|
+
* The kernel owns no keys, documents, funds, or provider credentials. It joins
|
|
7
|
+
* construction-pinned verification results under a durable expected-revision
|
|
8
|
+
* CAS and refuses every state or external-effect ambiguity.
|
|
9
|
+
*/
|
|
10
|
+
import { canonicalize, hashCanonical } from './execution-binding.js';
|
|
11
|
+
import { ACTION_ESCROW_CONTRACTOR_TEMPLATE_VERSION, validateActionEscrowReleaseTemplate, } from './action-escrow-verifiers.js';
|
|
12
|
+
export const ACTION_ESCROW_STATE_VERSION = 'EP-ACTION-ESCROW-STATE-v1';
|
|
13
|
+
export const ACTION_ESCROW_OUTCOME_VERSION = 'EP-ACTION-ESCROW-OUTCOME-v1';
|
|
14
|
+
export const ACTION_ESCROW_PROFILE_VERSION = 'EP-ACTION-ESCROW-PROFILE-v1';
|
|
15
|
+
export const ACTION_ESCROW_STATES = Object.freeze([
|
|
16
|
+
'draft',
|
|
17
|
+
'awaiting_acceptance',
|
|
18
|
+
'effective',
|
|
19
|
+
'awaiting_funding',
|
|
20
|
+
'funded',
|
|
21
|
+
'milestone_submitted',
|
|
22
|
+
'release_reserved',
|
|
23
|
+
'released',
|
|
24
|
+
'disputed',
|
|
25
|
+
'amendment_pending',
|
|
26
|
+
'cancelled',
|
|
27
|
+
'completed',
|
|
28
|
+
'release_indeterminate',
|
|
29
|
+
]);
|
|
30
|
+
export const ACTION_ESCROW_TRANSITIONS = Object.freeze({
|
|
31
|
+
draft: Object.freeze(['awaiting_acceptance', 'cancelled']),
|
|
32
|
+
awaiting_acceptance: Object.freeze(['effective', 'cancelled']),
|
|
33
|
+
effective: Object.freeze(['awaiting_funding', 'amendment_pending', 'cancelled']),
|
|
34
|
+
awaiting_funding: Object.freeze(['funded']),
|
|
35
|
+
funded: Object.freeze(['milestone_submitted', 'disputed']),
|
|
36
|
+
milestone_submitted: Object.freeze([
|
|
37
|
+
'release_reserved',
|
|
38
|
+
'disputed',
|
|
39
|
+
]),
|
|
40
|
+
release_reserved: Object.freeze(['released', 'release_indeterminate', 'milestone_submitted']),
|
|
41
|
+
released: Object.freeze(['completed']),
|
|
42
|
+
disputed: Object.freeze([]),
|
|
43
|
+
amendment_pending: Object.freeze(['effective', 'cancelled']),
|
|
44
|
+
cancelled: Object.freeze([]),
|
|
45
|
+
completed: Object.freeze([]),
|
|
46
|
+
release_indeterminate: Object.freeze([
|
|
47
|
+
'released',
|
|
48
|
+
'milestone_submitted',
|
|
49
|
+
]),
|
|
50
|
+
});
|
|
51
|
+
const RESOLUTION_VERSION = 'EP-RESOLUTION-v1';
|
|
52
|
+
const DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
|
|
53
|
+
const MAX_CAS_ATTEMPTS = 4;
|
|
54
|
+
const MAX_OPERATIONS = 256;
|
|
55
|
+
const MAX_HISTORY = 512;
|
|
56
|
+
const MAX_SUPERSEDED_BINDINGS = 64;
|
|
57
|
+
const MAX_STATE_BYTES = 4 * 1024 * 1024;
|
|
58
|
+
const DEFAULT_PROVIDER_TIMEOUT_MS = 30_000;
|
|
59
|
+
const STORE_CAPABILITIES = Object.freeze([
|
|
60
|
+
'durable',
|
|
61
|
+
'atomicExpectedRevisionCas',
|
|
62
|
+
'linearizableReads',
|
|
63
|
+
'monotonicRevisions',
|
|
64
|
+
'nonExpiring',
|
|
65
|
+
]);
|
|
66
|
+
const COMMON_INPUT_KEYS = Object.freeze([
|
|
67
|
+
'agreement_digest',
|
|
68
|
+
'document_action_binding_digest',
|
|
69
|
+
'milestone_id',
|
|
70
|
+
'release_action_digest',
|
|
71
|
+
'parties',
|
|
72
|
+
'profile',
|
|
73
|
+
'idempotency_key',
|
|
74
|
+
]);
|
|
75
|
+
const RECORD_KEYS = new Set([
|
|
76
|
+
'@version',
|
|
77
|
+
'escrow_key',
|
|
78
|
+
'revision',
|
|
79
|
+
'state',
|
|
80
|
+
'agreement_digest',
|
|
81
|
+
'document_action_binding_digest',
|
|
82
|
+
'milestone_id',
|
|
83
|
+
'release_action_digest',
|
|
84
|
+
'parties',
|
|
85
|
+
'parties_digest',
|
|
86
|
+
'profile',
|
|
87
|
+
'profile_digest',
|
|
88
|
+
'document_action_binding',
|
|
89
|
+
'agreement_acceptances',
|
|
90
|
+
'funding',
|
|
91
|
+
'milestone_evidence',
|
|
92
|
+
'release_approvals',
|
|
93
|
+
'release',
|
|
94
|
+
'dispute',
|
|
95
|
+
'cancellation',
|
|
96
|
+
'completion',
|
|
97
|
+
'pending_amendment',
|
|
98
|
+
'superseded_bindings',
|
|
99
|
+
'operations',
|
|
100
|
+
'history',
|
|
101
|
+
'created_at',
|
|
102
|
+
'updated_at',
|
|
103
|
+
]);
|
|
104
|
+
const CONTAINER_KEYS = new Set(['artifact', 'verification']);
|
|
105
|
+
const FUNDING_CONTAINER_KEYS = new Set(['statement', 'verification']);
|
|
106
|
+
const ACCEPTANCE_KEYS = new Set(['party_id', 'artifact', 'verification']);
|
|
107
|
+
const RELEASE_APPROVAL_KEYS = new Set(['party_id', 'resolution', 'verification']);
|
|
108
|
+
const CORE_VERIFICATION_KEYS = new Set([
|
|
109
|
+
'valid',
|
|
110
|
+
'agreement_digest',
|
|
111
|
+
'document_action_binding_digest',
|
|
112
|
+
'milestone_id',
|
|
113
|
+
'release_action_digest',
|
|
114
|
+
'parties_digest',
|
|
115
|
+
'profile_digest',
|
|
116
|
+
]);
|
|
117
|
+
const ACCEPTANCE_VERIFICATION_KEYS = new Set([
|
|
118
|
+
...CORE_VERIFICATION_KEYS,
|
|
119
|
+
'party_id',
|
|
120
|
+
'principal_key_id',
|
|
121
|
+
'acceptance_digest',
|
|
122
|
+
]);
|
|
123
|
+
const FUNDING_VERIFICATION_KEYS = new Set([
|
|
124
|
+
...CORE_VERIFICATION_KEYS,
|
|
125
|
+
'authenticated',
|
|
126
|
+
'provider_id',
|
|
127
|
+
'statement_type',
|
|
128
|
+
'status',
|
|
129
|
+
'provider_transaction_id',
|
|
130
|
+
'provider_milestone_id',
|
|
131
|
+
'amount',
|
|
132
|
+
'currency',
|
|
133
|
+
'destination_id',
|
|
134
|
+
'statement_digest',
|
|
135
|
+
]);
|
|
136
|
+
const MILESTONE_VERIFICATION_KEYS = new Set([
|
|
137
|
+
...CORE_VERIFICATION_KEYS,
|
|
138
|
+
'evidence_digest',
|
|
139
|
+
'submitter_party_id',
|
|
140
|
+
'observed_at',
|
|
141
|
+
]);
|
|
142
|
+
const RELEASE_APPROVAL_VERIFICATION_KEYS = new Set([
|
|
143
|
+
...CORE_VERIFICATION_KEYS,
|
|
144
|
+
'authorizes_action',
|
|
145
|
+
'outcome',
|
|
146
|
+
'party_role',
|
|
147
|
+
'principal_key_id',
|
|
148
|
+
'nonce',
|
|
149
|
+
'issued_at',
|
|
150
|
+
'expires_at',
|
|
151
|
+
'resolution_digest',
|
|
152
|
+
'evidence_digest',
|
|
153
|
+
]);
|
|
154
|
+
const PROVIDER_VERIFICATION_KEYS = new Set([
|
|
155
|
+
...CORE_VERIFICATION_KEYS,
|
|
156
|
+
'authenticated',
|
|
157
|
+
'provider_id',
|
|
158
|
+
'provider_idempotency_key',
|
|
159
|
+
'provider_request_digest',
|
|
160
|
+
'provider_transaction_id',
|
|
161
|
+
'provider_milestone_id',
|
|
162
|
+
'amount',
|
|
163
|
+
'currency',
|
|
164
|
+
'destination_id',
|
|
165
|
+
'statement_type',
|
|
166
|
+
'status',
|
|
167
|
+
'statement_digest',
|
|
168
|
+
]);
|
|
169
|
+
const OPERATION_KEYS = new Set([
|
|
170
|
+
'idempotency_key',
|
|
171
|
+
'operation',
|
|
172
|
+
'request_digest',
|
|
173
|
+
'code',
|
|
174
|
+
'ok',
|
|
175
|
+
'outcome',
|
|
176
|
+
'state',
|
|
177
|
+
'at',
|
|
178
|
+
]);
|
|
179
|
+
const HISTORY_KEYS = new Set(['from', 'to', 'operation', 'idempotency_key', 'at']);
|
|
180
|
+
const KERNEL_OPERATIONS = new Set([
|
|
181
|
+
'create',
|
|
182
|
+
'begin_acceptance',
|
|
183
|
+
'accept_agreement',
|
|
184
|
+
'request_funding',
|
|
185
|
+
'record_funding',
|
|
186
|
+
'submit_milestone',
|
|
187
|
+
'approve_release',
|
|
188
|
+
'release',
|
|
189
|
+
'reconcile_release',
|
|
190
|
+
'open_dispute',
|
|
191
|
+
'propose_amendment',
|
|
192
|
+
'accept_amendment',
|
|
193
|
+
'cancel',
|
|
194
|
+
'complete',
|
|
195
|
+
]);
|
|
196
|
+
function isPlainObject(value) {
|
|
197
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
198
|
+
return false;
|
|
199
|
+
const prototype = Object.getPrototypeOf(value);
|
|
200
|
+
return prototype === Object.prototype || prototype === null;
|
|
201
|
+
}
|
|
202
|
+
function exactKeys(value, allowed, required = allowed) {
|
|
203
|
+
if (!isPlainObject(value))
|
|
204
|
+
return false;
|
|
205
|
+
const keys = Object.keys(value);
|
|
206
|
+
return keys.every((key) => allowed.has(key))
|
|
207
|
+
&& [...required].every((key) => Object.hasOwn(value, key));
|
|
208
|
+
}
|
|
209
|
+
function validString(value, max = 512) {
|
|
210
|
+
return typeof value === 'string'
|
|
211
|
+
&& value.length > 0
|
|
212
|
+
&& value.length <= max
|
|
213
|
+
&& !/[\u0000-\u001f\u007f]/.test(value);
|
|
214
|
+
}
|
|
215
|
+
function validDigest(value) {
|
|
216
|
+
return typeof value === 'string' && DIGEST_RE.test(value);
|
|
217
|
+
}
|
|
218
|
+
function validInstant(value) {
|
|
219
|
+
if (!validString(value, 64))
|
|
220
|
+
return false;
|
|
221
|
+
const parsed = Date.parse(value);
|
|
222
|
+
return Number.isFinite(parsed) && new Date(parsed).toISOString() === value;
|
|
223
|
+
}
|
|
224
|
+
function canonicalSnapshot(value) {
|
|
225
|
+
return JSON.parse(canonicalize(value));
|
|
226
|
+
}
|
|
227
|
+
function canonicalDigest(value) {
|
|
228
|
+
return `sha256:${hashCanonical(value)}`;
|
|
229
|
+
}
|
|
230
|
+
function resolutionBindingInput(record) {
|
|
231
|
+
return {
|
|
232
|
+
agreement_digest: record.agreement_digest,
|
|
233
|
+
document_action_binding_digest: record.document_action_binding_digest,
|
|
234
|
+
milestone_id: record.milestone_id,
|
|
235
|
+
release_action_digest: record.release_action_digest,
|
|
236
|
+
profile_digest: record.profile_digest,
|
|
237
|
+
evidence_digest: record.milestone_evidence?.verification?.evidence_digest,
|
|
238
|
+
release_action_template: record.document_action_binding?.verification?.release_action_template,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Build the exact human-facing envelope signed by an Action Escrow approver.
|
|
243
|
+
*
|
|
244
|
+
* The release action digest remains the normative machine-action binding. This
|
|
245
|
+
* envelope binds the document, evidence, and material release fields that the
|
|
246
|
+
* approver reviews before selecting the approval option.
|
|
247
|
+
*/
|
|
248
|
+
export function createActionEscrowReleaseBindingMoment(input) {
|
|
249
|
+
try {
|
|
250
|
+
if (!isPlainObject(input)
|
|
251
|
+
|| !validDigest(input.agreement_digest)
|
|
252
|
+
|| !validDigest(input.document_action_binding_digest)
|
|
253
|
+
|| !validString(input.milestone_id, 256)
|
|
254
|
+
|| !validDigest(input.release_action_digest)
|
|
255
|
+
|| !validDigest(input.profile_digest)
|
|
256
|
+
|| !validDigest(input.evidence_digest)
|
|
257
|
+
|| !isPlainObject(input.release_action_template)) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
const template = input.release_action_template;
|
|
261
|
+
if (template.action_type !== 'escrow.milestone.release'
|
|
262
|
+
|| !validString(template.amount, 128)
|
|
263
|
+
|| !validString(template.currency, 16)
|
|
264
|
+
|| !validString(template.payee_id, 512)
|
|
265
|
+
|| !validString(template.destination_id, 512)
|
|
266
|
+
|| !validDigest(template.document_sha256)
|
|
267
|
+
|| !validDigest(template.material_terms_sha256)
|
|
268
|
+
|| template.completion_evidence_sha256 !== input.evidence_digest
|
|
269
|
+
|| !Number.isSafeInteger(template.amendment_version)
|
|
270
|
+
|| template.amendment_version < 1) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
return deepFreeze(canonicalSnapshot({
|
|
274
|
+
synopsis: `Authorize one ${template.amount} ${template.currency} milestone release.`,
|
|
275
|
+
findings: [
|
|
276
|
+
`Agreement digest: ${input.agreement_digest}`,
|
|
277
|
+
`Document-action binding digest: ${input.document_action_binding_digest}`,
|
|
278
|
+
`Release action digest: ${input.release_action_digest}`,
|
|
279
|
+
`Milestone evidence digest: ${input.evidence_digest}`,
|
|
280
|
+
`Milestone: ${input.milestone_id}`,
|
|
281
|
+
`Payee: ${template.payee_id}`,
|
|
282
|
+
`Destination: ${template.destination_id}`,
|
|
283
|
+
`Amendment version: ${template.amendment_version}`,
|
|
284
|
+
],
|
|
285
|
+
recommendations: [
|
|
286
|
+
'Verify the exact document, amount, destination, and completion evidence before approving.',
|
|
287
|
+
],
|
|
288
|
+
offer: 'Decline if any material field is unexpected; amendments require a new binding and fresh approvals.',
|
|
289
|
+
question: {
|
|
290
|
+
stem: `Authorize release of ${template.amount} ${template.currency} for milestone ${input.milestone_id} to ${template.payee_id} at ${template.destination_id}?`,
|
|
291
|
+
options: [
|
|
292
|
+
{
|
|
293
|
+
label: 'Approve exact release',
|
|
294
|
+
reasoning: 'Authorize only the action identified by the signed action digest.',
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
label: 'Decline release',
|
|
298
|
+
reasoning: 'Do not authorize this action or any custodian effect.',
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
recommended_idx: 1,
|
|
302
|
+
hatches: {
|
|
303
|
+
free_text: false,
|
|
304
|
+
dialogue: false,
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
meta: {
|
|
308
|
+
decision_class: 'escrow.milestone.release',
|
|
309
|
+
calibration_note: 'No approval recommendation; verify every material field.',
|
|
310
|
+
},
|
|
311
|
+
}));
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
export function computeActionEscrowReleaseBindingMomentDigest(input) {
|
|
318
|
+
const bindingMoment = createActionEscrowReleaseBindingMoment(input);
|
|
319
|
+
return bindingMoment === null ? null : canonicalDigest(bindingMoment);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Return the relying-party-pinned nonce for one party and one exact release.
|
|
323
|
+
*
|
|
324
|
+
* It is stable while both parties approve, changes with any material action or
|
|
325
|
+
* evidence change, and is consumed by the durable approval CAS.
|
|
326
|
+
*/
|
|
327
|
+
export function computeActionEscrowResolutionNonce(input, partyId) {
|
|
328
|
+
if (!validString(partyId, 256))
|
|
329
|
+
return null;
|
|
330
|
+
const bindingMomentDigest = computeActionEscrowReleaseBindingMomentDigest(input);
|
|
331
|
+
if (bindingMomentDigest === null)
|
|
332
|
+
return null;
|
|
333
|
+
try {
|
|
334
|
+
return `ep-ae-resolution:${hashCanonical({
|
|
335
|
+
'@version': 'EP-ACTION-ESCROW-RESOLUTION-NONCE-v1',
|
|
336
|
+
party_id: partyId,
|
|
337
|
+
binding_moment_digest: bindingMomentDigest,
|
|
338
|
+
release_action_digest: input.release_action_digest,
|
|
339
|
+
evidence_digest: input.evidence_digest,
|
|
340
|
+
})}`;
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function deepFreeze(value) {
|
|
347
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value))
|
|
348
|
+
return value;
|
|
349
|
+
Object.freeze(value);
|
|
350
|
+
for (const child of Object.values(value))
|
|
351
|
+
deepFreeze(child);
|
|
352
|
+
return value;
|
|
353
|
+
}
|
|
354
|
+
function sameStringSet(left, right) {
|
|
355
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length)
|
|
356
|
+
return false;
|
|
357
|
+
const values = new Set(left);
|
|
358
|
+
return values.size === left.length && right.every((value) => values.has(value));
|
|
359
|
+
}
|
|
360
|
+
function validateParties(parties) {
|
|
361
|
+
if (!Array.isArray(parties) || parties.length < 2 || parties.length > 16) {
|
|
362
|
+
return 'parties_invalid';
|
|
363
|
+
}
|
|
364
|
+
const ids = [];
|
|
365
|
+
for (const party of parties) {
|
|
366
|
+
if (!exactKeys(party, new Set(['party_id', 'role']))
|
|
367
|
+
|| !validString(party.party_id, 256)
|
|
368
|
+
|| !validString(party.role, 128)) {
|
|
369
|
+
return 'party_invalid';
|
|
370
|
+
}
|
|
371
|
+
ids.push(party.party_id);
|
|
372
|
+
}
|
|
373
|
+
return new Set(ids).size === ids.length ? null : 'party_duplicate';
|
|
374
|
+
}
|
|
375
|
+
function validateProfileShape(profile) {
|
|
376
|
+
if (!exactKeys(profile, new Set([
|
|
377
|
+
'@version',
|
|
378
|
+
'profile_id',
|
|
379
|
+
'provider_id',
|
|
380
|
+
'required_acceptance_party_ids',
|
|
381
|
+
'required_release_approver_party_ids',
|
|
382
|
+
'prohibit_self_approval',
|
|
383
|
+
]))) {
|
|
384
|
+
return 'profile_shape_invalid';
|
|
385
|
+
}
|
|
386
|
+
if (profile['@version'] !== ACTION_ESCROW_PROFILE_VERSION
|
|
387
|
+
|| !validString(profile.profile_id, 256)
|
|
388
|
+
|| !validString(profile.provider_id, 256)) {
|
|
389
|
+
return 'profile_identity_invalid';
|
|
390
|
+
}
|
|
391
|
+
for (const field of [
|
|
392
|
+
'required_acceptance_party_ids',
|
|
393
|
+
'required_release_approver_party_ids',
|
|
394
|
+
]) {
|
|
395
|
+
const roster = profile[field];
|
|
396
|
+
if (!Array.isArray(roster) || roster.length === 0 || roster.length > 16
|
|
397
|
+
|| roster.some((partyId) => !validString(partyId, 256))
|
|
398
|
+
|| new Set(roster).size !== roster.length) {
|
|
399
|
+
return `${field}_invalid`;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (profile.prohibit_self_approval !== true
|
|
403
|
+
&& profile.prohibit_self_approval !== false) {
|
|
404
|
+
return 'profile_self_approval_policy_invalid';
|
|
405
|
+
}
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
function validateProfileForParties(profile, parties) {
|
|
409
|
+
const shapeError = validateProfileShape(profile);
|
|
410
|
+
if (shapeError)
|
|
411
|
+
return shapeError;
|
|
412
|
+
const partyIds = parties.map((party) => party.party_id);
|
|
413
|
+
if (!sameStringSet(profile.required_acceptance_party_ids, partyIds)) {
|
|
414
|
+
return 'mutual_acceptance_roster_invalid';
|
|
415
|
+
}
|
|
416
|
+
if (!profile.required_release_approver_party_ids.every((partyId) => partyIds.includes(partyId))) {
|
|
417
|
+
return 'release_approval_roster_invalid';
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
function escrowKey(context) {
|
|
422
|
+
return `ep-action-escrow:${hashCanonical({
|
|
423
|
+
'@version': 'EP-ACTION-ESCROW-KEY-v1',
|
|
424
|
+
agreement_digest: context.agreement_digest,
|
|
425
|
+
milestone_id: context.milestone_id,
|
|
426
|
+
})}`;
|
|
427
|
+
}
|
|
428
|
+
function releaseReservationKey(context) {
|
|
429
|
+
return `ep-ae-reservation:${hashCanonical({
|
|
430
|
+
'@version': 'EP-ACTION-ESCROW-RELEASE-KEY-v1',
|
|
431
|
+
agreement_digest: context.agreement_digest,
|
|
432
|
+
document_action_binding_digest: context.document_action_binding_digest,
|
|
433
|
+
milestone_id: context.milestone_id,
|
|
434
|
+
release_action_digest: context.release_action_digest,
|
|
435
|
+
profile_digest: context.profile_digest,
|
|
436
|
+
})}`;
|
|
437
|
+
}
|
|
438
|
+
function providerIdempotencyKey(context) {
|
|
439
|
+
return `ep-ae-release:${hashCanonical({
|
|
440
|
+
'@version': 'EP-ACTION-ESCROW-PROVIDER-IDEMPOTENCY-v1',
|
|
441
|
+
agreement_digest: context.agreement_digest,
|
|
442
|
+
document_action_binding_digest: context.document_action_binding_digest,
|
|
443
|
+
milestone_id: context.milestone_id,
|
|
444
|
+
release_action_digest: context.release_action_digest,
|
|
445
|
+
profile_digest: context.profile_digest,
|
|
446
|
+
})}`;
|
|
447
|
+
}
|
|
448
|
+
function expectedBindings(context) {
|
|
449
|
+
return {
|
|
450
|
+
agreement_digest: context.agreement_digest,
|
|
451
|
+
document_action_binding_digest: context.document_action_binding_digest,
|
|
452
|
+
milestone_id: context.milestone_id,
|
|
453
|
+
release_action_digest: context.release_action_digest,
|
|
454
|
+
parties_digest: context.parties_digest,
|
|
455
|
+
profile_digest: context.profile_digest,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function normalizedInput(operation, input, extraAllowed = [], extraRequired = extraAllowed) {
|
|
459
|
+
try {
|
|
460
|
+
const snapshot = canonicalSnapshot(input);
|
|
461
|
+
const allowed = new Set([...COMMON_INPUT_KEYS, ...extraAllowed]);
|
|
462
|
+
const required = new Set([...COMMON_INPUT_KEYS, ...extraRequired]);
|
|
463
|
+
if (!exactKeys(snapshot, allowed, required)) {
|
|
464
|
+
return { error: 'invalid_operation_input' };
|
|
465
|
+
}
|
|
466
|
+
if (!validDigest(snapshot.agreement_digest)
|
|
467
|
+
|| !validDigest(snapshot.document_action_binding_digest)
|
|
468
|
+
|| !validDigest(snapshot.release_action_digest)
|
|
469
|
+
|| !validString(snapshot.milestone_id, 256)
|
|
470
|
+
|| !validString(snapshot.idempotency_key, 256)) {
|
|
471
|
+
return { error: 'invalid_operation_binding' };
|
|
472
|
+
}
|
|
473
|
+
const partiesError = validateParties(snapshot.parties);
|
|
474
|
+
if (partiesError)
|
|
475
|
+
return { error: partiesError };
|
|
476
|
+
const profileError = validateProfileForParties(snapshot.profile, snapshot.parties);
|
|
477
|
+
if (profileError)
|
|
478
|
+
return { error: profileError };
|
|
479
|
+
const context = {
|
|
480
|
+
agreement_digest: snapshot.agreement_digest,
|
|
481
|
+
document_action_binding_digest: snapshot.document_action_binding_digest,
|
|
482
|
+
milestone_id: snapshot.milestone_id,
|
|
483
|
+
release_action_digest: snapshot.release_action_digest,
|
|
484
|
+
parties: snapshot.parties,
|
|
485
|
+
parties_digest: canonicalDigest(snapshot.parties),
|
|
486
|
+
profile: snapshot.profile,
|
|
487
|
+
profile_digest: canonicalDigest(snapshot.profile),
|
|
488
|
+
};
|
|
489
|
+
return {
|
|
490
|
+
snapshot,
|
|
491
|
+
context,
|
|
492
|
+
escrowKey: escrowKey(context),
|
|
493
|
+
requestDigest: canonicalDigest({ operation, input: snapshot }),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
return { error: 'invalid_operation_input' };
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function outcome({ ok = false, type = 'refused', code, operation = null, record = null, details = null, }) {
|
|
501
|
+
try {
|
|
502
|
+
return deepFreeze(canonicalSnapshot({
|
|
503
|
+
'@version': ACTION_ESCROW_OUTCOME_VERSION,
|
|
504
|
+
ok,
|
|
505
|
+
outcome: type,
|
|
506
|
+
code,
|
|
507
|
+
operation,
|
|
508
|
+
state: record?.state ?? null,
|
|
509
|
+
revision: record?.revision ?? null,
|
|
510
|
+
escrow_key: record?.escrow_key ?? null,
|
|
511
|
+
details,
|
|
512
|
+
record,
|
|
513
|
+
}));
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
return Object.freeze({
|
|
517
|
+
'@version': ACTION_ESCROW_OUTCOME_VERSION,
|
|
518
|
+
ok: false,
|
|
519
|
+
outcome: 'refused',
|
|
520
|
+
code: 'closed_outcome_encoding_failed',
|
|
521
|
+
operation,
|
|
522
|
+
state: null,
|
|
523
|
+
revision: null,
|
|
524
|
+
escrow_key: null,
|
|
525
|
+
details: null,
|
|
526
|
+
record: null,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function operationBindingMatches(record, normalized, mode = 'active') {
|
|
531
|
+
const binding = mode === 'pending'
|
|
532
|
+
? record.pending_amendment
|
|
533
|
+
: record;
|
|
534
|
+
if (!binding)
|
|
535
|
+
return false;
|
|
536
|
+
return record.agreement_digest === normalized.context.agreement_digest
|
|
537
|
+
&& record.milestone_id === normalized.context.milestone_id
|
|
538
|
+
&& binding.document_action_binding_digest
|
|
539
|
+
=== normalized.context.document_action_binding_digest
|
|
540
|
+
&& binding.release_action_digest === normalized.context.release_action_digest
|
|
541
|
+
&& record.parties_digest === normalized.context.parties_digest
|
|
542
|
+
&& record.profile_digest === normalized.context.profile_digest;
|
|
543
|
+
}
|
|
544
|
+
function boundVerificationMatches(result, expected) {
|
|
545
|
+
return isPlainObject(result)
|
|
546
|
+
&& result.valid === true
|
|
547
|
+
&& result.agreement_digest === expected.agreement_digest
|
|
548
|
+
&& result.document_action_binding_digest === expected.document_action_binding_digest
|
|
549
|
+
&& result.milestone_id === expected.milestone_id
|
|
550
|
+
&& result.release_action_digest === expected.release_action_digest
|
|
551
|
+
&& result.parties_digest === expected.parties_digest
|
|
552
|
+
&& result.profile_digest === expected.profile_digest;
|
|
553
|
+
}
|
|
554
|
+
function boundVerificationSummary(result, expected, extras = {}) {
|
|
555
|
+
return {
|
|
556
|
+
valid: true,
|
|
557
|
+
...expectedBindings(expected),
|
|
558
|
+
...extras,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
function bindingVerificationDetails(result, expected) {
|
|
562
|
+
try {
|
|
563
|
+
if (!boundVerificationMatches(result, expectedBindings(expected))
|
|
564
|
+
|| !validDigest(result.verification_digest)
|
|
565
|
+
|| !validDigest(result.document_digest)
|
|
566
|
+
|| !validString(result.agreement_id, 256)
|
|
567
|
+
|| !validString(result.binding_id, 256)
|
|
568
|
+
|| !isPlainObject(result.release_action_template)) {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
// Passed through an explicitly Record<string, any>-typed local (rather than
|
|
572
|
+
// as an inline object literal) so this call isn't subject to TypeScript's
|
|
573
|
+
// excess-property check against validateActionEscrowReleaseTemplate's
|
|
574
|
+
// options type, which (in action-escrow-verifiers.ts, out of scope here)
|
|
575
|
+
// is currently under-typed relative to the properties it actually reads.
|
|
576
|
+
const releaseActionTemplateOptions = {
|
|
577
|
+
profileDigest: expected.profile_digest,
|
|
578
|
+
agreementId: result.agreement_id,
|
|
579
|
+
agreementDigest: expected.agreement_digest,
|
|
580
|
+
milestoneId: expected.milestone_id,
|
|
581
|
+
documentDigest: result.document_digest,
|
|
582
|
+
contractorProjectSource: result.release_action_template.action_escrow_template_profile
|
|
583
|
+
=== ACTION_ESCROW_CONTRACTOR_TEMPLATE_VERSION,
|
|
584
|
+
};
|
|
585
|
+
const releaseActionTemplate = validateActionEscrowReleaseTemplate(result.release_action_template, releaseActionTemplateOptions);
|
|
586
|
+
if (!releaseActionTemplate)
|
|
587
|
+
return null;
|
|
588
|
+
return {
|
|
589
|
+
verification_digest: result.verification_digest,
|
|
590
|
+
document_digest: result.document_digest,
|
|
591
|
+
agreement_id: result.agreement_id,
|
|
592
|
+
binding_id: result.binding_id,
|
|
593
|
+
release_action_template: releaseActionTemplate,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function storedBindingVerificationValid(container, expected) {
|
|
601
|
+
if (!isPlainObject(container)
|
|
602
|
+
|| !isPlainObject(container.artifact)
|
|
603
|
+
|| !isPlainObject(container.verification)
|
|
604
|
+
|| container.verification.valid !== true) {
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
return bindingVerificationDetails(container.verification, expected) !== null;
|
|
608
|
+
}
|
|
609
|
+
function coreVerificationValid(value, record, allowedKeys, overrides = {}) {
|
|
610
|
+
const expected = {
|
|
611
|
+
agreement_digest: record.agreement_digest,
|
|
612
|
+
document_action_binding_digest: record.document_action_binding_digest,
|
|
613
|
+
milestone_id: record.milestone_id,
|
|
614
|
+
release_action_digest: record.release_action_digest,
|
|
615
|
+
parties_digest: record.parties_digest,
|
|
616
|
+
profile_digest: record.profile_digest,
|
|
617
|
+
...overrides,
|
|
618
|
+
};
|
|
619
|
+
return exactKeys(value, allowedKeys, CORE_VERIFICATION_KEYS)
|
|
620
|
+
&& value.valid === true
|
|
621
|
+
&& Object.entries(expected).every(([key, entry]) => value[key] === entry);
|
|
622
|
+
}
|
|
623
|
+
function storedAcceptancesValid(entries, record, { documentActionBindingDigest = record.document_action_binding_digest, releaseActionDigest = record.release_action_digest, } = {}) {
|
|
624
|
+
if (!Array.isArray(entries) || entries.length > record.profile.required_acceptance_party_ids.length) {
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
const required = new Set(record.profile.required_acceptance_party_ids);
|
|
628
|
+
const parties = new Set(record.parties.map((party) => party.party_id));
|
|
629
|
+
const seenParties = new Set();
|
|
630
|
+
const seenKeys = new Set();
|
|
631
|
+
for (const entry of entries) {
|
|
632
|
+
const verification = entry?.verification;
|
|
633
|
+
if (!exactKeys(entry, ACCEPTANCE_KEYS)
|
|
634
|
+
|| !isPlainObject(entry.artifact)
|
|
635
|
+
|| !isPlainObject(verification)
|
|
636
|
+
|| !coreVerificationValid(verification, record, ACCEPTANCE_VERIFICATION_KEYS, {
|
|
637
|
+
document_action_binding_digest: documentActionBindingDigest,
|
|
638
|
+
release_action_digest: releaseActionDigest,
|
|
639
|
+
})
|
|
640
|
+
|| !parties.has(entry.party_id)
|
|
641
|
+
|| !required.has(entry.party_id)
|
|
642
|
+
|| seenParties.has(entry.party_id)
|
|
643
|
+
|| verification.party_id !== entry.party_id
|
|
644
|
+
|| !validString(verification.principal_key_id, 512)
|
|
645
|
+
|| seenKeys.has(verification.principal_key_id)
|
|
646
|
+
|| !validDigest(verification.acceptance_digest)) {
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
seenParties.add(entry.party_id);
|
|
650
|
+
seenKeys.add(verification.principal_key_id);
|
|
651
|
+
}
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
function storedFundingValid(record) {
|
|
655
|
+
if (record.funding === null)
|
|
656
|
+
return true;
|
|
657
|
+
const template = record.document_action_binding.verification.release_action_template;
|
|
658
|
+
const verification = record.funding?.verification;
|
|
659
|
+
return exactKeys(record.funding, FUNDING_CONTAINER_KEYS)
|
|
660
|
+
&& isPlainObject(record.funding.statement)
|
|
661
|
+
&& coreVerificationValid(verification, record, FUNDING_VERIFICATION_KEYS)
|
|
662
|
+
&& verification.authenticated === true
|
|
663
|
+
&& verification.provider_id === record.profile.provider_id
|
|
664
|
+
&& verification.statement_type === 'funding'
|
|
665
|
+
&& verification.status === 'funded'
|
|
666
|
+
&& verification.provider_transaction_id === template.custodian_transaction_id
|
|
667
|
+
&& verification.provider_milestone_id === template.custodian_milestone_id
|
|
668
|
+
&& verification.amount === template.amount
|
|
669
|
+
&& verification.currency === template.currency
|
|
670
|
+
&& verification.destination_id === template.destination_id
|
|
671
|
+
&& validDigest(verification.statement_digest);
|
|
672
|
+
}
|
|
673
|
+
function storedMilestoneEvidenceValid(record) {
|
|
674
|
+
if (record.milestone_evidence === null)
|
|
675
|
+
return true;
|
|
676
|
+
const verification = record.milestone_evidence?.verification;
|
|
677
|
+
return exactKeys(record.milestone_evidence, CONTAINER_KEYS)
|
|
678
|
+
&& isPlainObject(record.milestone_evidence.artifact)
|
|
679
|
+
&& coreVerificationValid(verification, record, MILESTONE_VERIFICATION_KEYS)
|
|
680
|
+
&& validDigest(verification.evidence_digest)
|
|
681
|
+
&& record.parties.some((party) => (party.party_id === verification.submitter_party_id))
|
|
682
|
+
&& validInstant(verification.observed_at);
|
|
683
|
+
}
|
|
684
|
+
function storedReleaseApprovalsValid(record) {
|
|
685
|
+
if (!Array.isArray(record.release_approvals)
|
|
686
|
+
|| record.release_approvals.length > record.profile.required_release_approver_party_ids.length
|
|
687
|
+
|| (record.release_approvals.length > 0 && record.milestone_evidence === null)) {
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
const required = new Set(record.profile.required_release_approver_party_ids);
|
|
691
|
+
const parties = new Map(record.parties.map((party) => [party.party_id, party]));
|
|
692
|
+
const seenParties = new Set();
|
|
693
|
+
const seenKeys = new Set();
|
|
694
|
+
const evidenceDigest = record.milestone_evidence?.verification?.evidence_digest;
|
|
695
|
+
const bindingInput = resolutionBindingInput(record);
|
|
696
|
+
const bindingMomentDigest = computeActionEscrowReleaseBindingMomentDigest(bindingInput);
|
|
697
|
+
if (record.release_approvals.length > 0
|
|
698
|
+
&& (!validDigest(evidenceDigest) || !validDigest(bindingMomentDigest))) {
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
for (const entry of record.release_approvals) {
|
|
702
|
+
const party = parties.get(entry?.party_id);
|
|
703
|
+
const verification = entry?.verification;
|
|
704
|
+
const context = entry?.resolution?.signoff?.context;
|
|
705
|
+
if (!exactKeys(entry, RELEASE_APPROVAL_KEYS)
|
|
706
|
+
|| !party
|
|
707
|
+
|| !required.has(entry.party_id)
|
|
708
|
+
|| seenParties.has(entry.party_id)
|
|
709
|
+
|| !isPlainObject(entry.resolution)
|
|
710
|
+
|| entry.resolution.profile !== RESOLUTION_VERSION
|
|
711
|
+
|| !isPlainObject(context)
|
|
712
|
+
|| !isPlainObject(context.resolution)
|
|
713
|
+
|| !coreVerificationValid(verification, record, RELEASE_APPROVAL_VERIFICATION_KEYS)
|
|
714
|
+
|| verification.authorizes_action !== true
|
|
715
|
+
|| verification.outcome !== 'approved'
|
|
716
|
+
|| verification.party_role !== party.role
|
|
717
|
+
|| !validString(verification.principal_key_id, 512)
|
|
718
|
+
|| seenKeys.has(verification.principal_key_id)
|
|
719
|
+
|| !validString(verification.nonce, 512)
|
|
720
|
+
|| !validInstant(verification.issued_at)
|
|
721
|
+
|| !validInstant(verification.expires_at)
|
|
722
|
+
|| verification.resolution_digest !== canonicalDigest(entry.resolution)
|
|
723
|
+
|| verification.evidence_digest !== evidenceDigest
|
|
724
|
+
|| context.principal !== entry.party_id
|
|
725
|
+
|| context.principal_key_id !== verification.principal_key_id
|
|
726
|
+
|| context.envelope_hash !== bindingMomentDigest
|
|
727
|
+
|| context.action_hash !== record.release_action_digest
|
|
728
|
+
|| context.initiator !== record.milestone_evidence.verification.submitter_party_id
|
|
729
|
+
|| context.nonce !== computeActionEscrowResolutionNonce(bindingInput, entry.party_id)
|
|
730
|
+
|| context.nonce !== verification.nonce
|
|
731
|
+
|| context.issued_at !== verification.issued_at
|
|
732
|
+
|| context.expires_at !== verification.expires_at
|
|
733
|
+
|| context.resolution.outcome !== 'approved'
|
|
734
|
+
|| context.resolution.selected_option !== 0) {
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
seenParties.add(entry.party_id);
|
|
738
|
+
seenKeys.add(verification.principal_key_id);
|
|
739
|
+
}
|
|
740
|
+
return true;
|
|
741
|
+
}
|
|
742
|
+
function storedHistoryValid(record) {
|
|
743
|
+
if (!Array.isArray(record.operations)
|
|
744
|
+
|| record.operations.length === 0
|
|
745
|
+
|| !Array.isArray(record.history)
|
|
746
|
+
|| record.history.length === 0
|
|
747
|
+
|| record.operations.length > MAX_OPERATIONS
|
|
748
|
+
|| record.history.length > MAX_HISTORY) {
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
const operations = new Map();
|
|
752
|
+
for (const entry of record.operations) {
|
|
753
|
+
if (!exactKeys(entry, OPERATION_KEYS)
|
|
754
|
+
|| !validString(entry.idempotency_key, 512)
|
|
755
|
+
|| operations.has(entry.idempotency_key)
|
|
756
|
+
|| !validString(entry.operation, 128)
|
|
757
|
+
|| !KERNEL_OPERATIONS.has(entry.operation)
|
|
758
|
+
|| !validDigest(entry.request_digest)
|
|
759
|
+
|| !validString(entry.code, 256)
|
|
760
|
+
|| typeof entry.ok !== 'boolean'
|
|
761
|
+
|| !validString(entry.outcome, 128)
|
|
762
|
+
|| !ACTION_ESCROW_STATES.includes(entry.state)
|
|
763
|
+
|| !validInstant(entry.at)) {
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
operations.set(entry.idempotency_key, entry);
|
|
767
|
+
}
|
|
768
|
+
let previousState = null;
|
|
769
|
+
for (const [index, entry] of record.history.entries()) {
|
|
770
|
+
const operation = operations.get(entry?.idempotency_key);
|
|
771
|
+
if (!exactKeys(entry, HISTORY_KEYS)
|
|
772
|
+
|| !ACTION_ESCROW_STATES.includes(entry.to)
|
|
773
|
+
|| (entry.from !== null && !ACTION_ESCROW_STATES.includes(entry.from))
|
|
774
|
+
|| !validString(entry.operation, 128)
|
|
775
|
+
|| !validString(entry.idempotency_key, 512)
|
|
776
|
+
|| !validInstant(entry.at)
|
|
777
|
+
|| !operation
|
|
778
|
+
|| operation.operation !== entry.operation
|
|
779
|
+
|| entry.from !== previousState
|
|
780
|
+
|| (index > 0 && !ACTION_ESCROW_TRANSITIONS[entry.from]?.includes(entry.to))) {
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
if (index === 0 && (entry.from !== null || entry.to !== 'draft' || entry.operation !== 'create')) {
|
|
784
|
+
return false;
|
|
785
|
+
}
|
|
786
|
+
previousState = entry.to;
|
|
787
|
+
}
|
|
788
|
+
return previousState === record.state;
|
|
789
|
+
}
|
|
790
|
+
function recordShapeValid(record, revision) {
|
|
791
|
+
if (!exactKeys(record, RECORD_KEYS)
|
|
792
|
+
|| record['@version'] !== ACTION_ESCROW_STATE_VERSION
|
|
793
|
+
|| record.revision !== revision
|
|
794
|
+
|| !Number.isSafeInteger(record.revision)
|
|
795
|
+
|| record.revision < 0
|
|
796
|
+
|| !ACTION_ESCROW_STATES.includes(record.state)
|
|
797
|
+
|| !validString(record.escrow_key, 256)
|
|
798
|
+
|| !validDigest(record.agreement_digest)
|
|
799
|
+
|| !validDigest(record.document_action_binding_digest)
|
|
800
|
+
|| !validDigest(record.release_action_digest)
|
|
801
|
+
|| !validString(record.milestone_id, 256)
|
|
802
|
+
|| !validDigest(record.parties_digest)
|
|
803
|
+
|| !validDigest(record.profile_digest)
|
|
804
|
+
|| validateParties(record.parties)
|
|
805
|
+
|| validateProfileForParties(record.profile, record.parties)
|
|
806
|
+
|| record.parties_digest !== canonicalDigest(record.parties)
|
|
807
|
+
|| record.profile_digest !== canonicalDigest(record.profile)
|
|
808
|
+
|| record.escrow_key !== escrowKey(record)
|
|
809
|
+
|| !storedBindingVerificationValid(record.document_action_binding, record)
|
|
810
|
+
|| !Array.isArray(record.agreement_acceptances)
|
|
811
|
+
|| !Array.isArray(record.release_approvals)
|
|
812
|
+
|| !Array.isArray(record.superseded_bindings)
|
|
813
|
+
|| !Array.isArray(record.operations)
|
|
814
|
+
|| !Array.isArray(record.history)
|
|
815
|
+
|| record.operations.length > MAX_OPERATIONS
|
|
816
|
+
|| record.history.length > MAX_HISTORY
|
|
817
|
+
|| record.superseded_bindings.length > MAX_SUPERSEDED_BINDINGS
|
|
818
|
+
|| !validInstant(record.created_at)
|
|
819
|
+
|| !validInstant(record.updated_at)
|
|
820
|
+
|| !storedHistoryValid(record)
|
|
821
|
+
|| !storedAcceptancesValid(record.agreement_acceptances, record)
|
|
822
|
+
|| !storedFundingValid(record)
|
|
823
|
+
|| !storedMilestoneEvidenceValid(record)
|
|
824
|
+
|| !storedReleaseApprovalsValid(record)) {
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
827
|
+
if (['release_reserved', 'released', 'release_indeterminate'].includes(record.state)
|
|
828
|
+
&& !isPlainObject(record.release)) {
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
831
|
+
return true;
|
|
832
|
+
}
|
|
833
|
+
function existingOperation(record, normalized, operation) {
|
|
834
|
+
const found = record.operations.find((entry) => entry.idempotency_key === normalized.snapshot.idempotency_key);
|
|
835
|
+
if (!found)
|
|
836
|
+
return null;
|
|
837
|
+
if (found.operation !== operation || found.request_digest !== normalized.requestDigest) {
|
|
838
|
+
return { conflict: true };
|
|
839
|
+
}
|
|
840
|
+
return { conflict: false, entry: found };
|
|
841
|
+
}
|
|
842
|
+
function idempotentResult(record, normalized, operation) {
|
|
843
|
+
const existing = existingOperation(record, normalized, operation);
|
|
844
|
+
if (!existing)
|
|
845
|
+
return null;
|
|
846
|
+
if (existing.conflict) {
|
|
847
|
+
return outcome({
|
|
848
|
+
code: 'idempotency_key_conflict',
|
|
849
|
+
operation,
|
|
850
|
+
record,
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
if (operation === 'release'
|
|
854
|
+
&& ['release_reserved', 'release_indeterminate'].includes(record.state)) {
|
|
855
|
+
return outcome({
|
|
856
|
+
code: 'release_reconciliation_required',
|
|
857
|
+
operation,
|
|
858
|
+
record,
|
|
859
|
+
type: 'indeterminate',
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
return outcome({
|
|
863
|
+
ok: foundOperationOk(existing.entry),
|
|
864
|
+
type: foundOperationOk(existing.entry)
|
|
865
|
+
? 'idempotent'
|
|
866
|
+
: existing.entry.outcome ?? 'refused',
|
|
867
|
+
code: existing.entry.code,
|
|
868
|
+
operation,
|
|
869
|
+
record,
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
function foundOperationOk(entry) {
|
|
873
|
+
return entry.ok === undefined ? true : entry.ok === true;
|
|
874
|
+
}
|
|
875
|
+
function appendOperation(next, record, normalized, operation, code, at, result = {}) {
|
|
876
|
+
if (record.operations.length >= MAX_OPERATIONS) {
|
|
877
|
+
return 'operation_history_limit_reached';
|
|
878
|
+
}
|
|
879
|
+
next.operations.push({
|
|
880
|
+
idempotency_key: normalized.snapshot.idempotency_key,
|
|
881
|
+
operation,
|
|
882
|
+
request_digest: normalized.requestDigest,
|
|
883
|
+
code,
|
|
884
|
+
ok: result.ok ?? true,
|
|
885
|
+
outcome: result.type ?? 'applied',
|
|
886
|
+
state: next.state,
|
|
887
|
+
at,
|
|
888
|
+
});
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* @param {Record<string, any>} record
|
|
893
|
+
* @param {Record<string, any>} normalized
|
|
894
|
+
* @param {string} operation
|
|
895
|
+
* @param {string | undefined} code
|
|
896
|
+
* @param {string} at
|
|
897
|
+
* @param {(next: Record<string, any>) => void} mutate
|
|
898
|
+
* @param {Record<string, any>} [result]
|
|
899
|
+
*/
|
|
900
|
+
function finalizeMutation(record, normalized, operation, code, at, mutate, result = {}) {
|
|
901
|
+
const next = canonicalSnapshot(record);
|
|
902
|
+
const fromState = next.state;
|
|
903
|
+
mutate(next);
|
|
904
|
+
next.revision = record.revision + 1;
|
|
905
|
+
next.updated_at = at;
|
|
906
|
+
const operationError = appendOperation(next, record, normalized, operation, code, at, result);
|
|
907
|
+
if (operationError)
|
|
908
|
+
return { error: operationError };
|
|
909
|
+
if (fromState !== next.state) {
|
|
910
|
+
if (next.history.length >= MAX_HISTORY)
|
|
911
|
+
return { error: 'state_history_limit_reached' };
|
|
912
|
+
next.history.push({
|
|
913
|
+
from: fromState,
|
|
914
|
+
to: next.state,
|
|
915
|
+
operation,
|
|
916
|
+
idempotency_key: normalized.snapshot.idempotency_key,
|
|
917
|
+
at,
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
return { next };
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* @param {Record<string, any>} record
|
|
924
|
+
*/
|
|
925
|
+
function providerExpected(record) {
|
|
926
|
+
const template = record.document_action_binding.verification.release_action_template;
|
|
927
|
+
return {
|
|
928
|
+
...expectedBindings(record),
|
|
929
|
+
parties: record.parties,
|
|
930
|
+
profile: record.profile,
|
|
931
|
+
provider_id: record.profile.provider_id,
|
|
932
|
+
provider_idempotency_key: record.release?.provider_idempotency_key ?? null,
|
|
933
|
+
provider_request_digest: record.release?.provider_request?.request_digest ?? null,
|
|
934
|
+
provider_transaction_id: template.custodian_transaction_id,
|
|
935
|
+
provider_milestone_id: template.custodian_milestone_id,
|
|
936
|
+
amount: template.amount,
|
|
937
|
+
currency: template.currency,
|
|
938
|
+
destination_id: template.destination_id,
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Create a fail-closed Action Escrow kernel.
|
|
943
|
+
*
|
|
944
|
+
* Store contract:
|
|
945
|
+
* durable === true
|
|
946
|
+
* atomicExpectedRevisionCas === true
|
|
947
|
+
* linearizableReads === true
|
|
948
|
+
* monotonicRevisions === true
|
|
949
|
+
* nonExpiring === true
|
|
950
|
+
* read(key) -> null | { revision, value: canonicalJsonText }
|
|
951
|
+
* compareAndSwap(key, expectedRevision|null, nextValue)
|
|
952
|
+
* -> { applied, revision }
|
|
953
|
+
*/
|
|
954
|
+
export function createActionEscrowKernel(options = {}) {
|
|
955
|
+
let configurationError = null;
|
|
956
|
+
let readStore = null;
|
|
957
|
+
let compareAndSwap = null;
|
|
958
|
+
let releaseProvider = null;
|
|
959
|
+
let getProviderRelease = null;
|
|
960
|
+
let verifyDocumentActionBinding = null;
|
|
961
|
+
let verifyAgreementAcceptance = null;
|
|
962
|
+
let verifyMilestoneEvidence = null;
|
|
963
|
+
let verifyResolutionReceipt = null;
|
|
964
|
+
let verifyProviderStatement = null;
|
|
965
|
+
let verifyStateCommand = null;
|
|
966
|
+
let resolveProfile = null;
|
|
967
|
+
let pinnedProfiles = null;
|
|
968
|
+
let now = null;
|
|
969
|
+
let providerTimeoutMs = DEFAULT_PROVIDER_TIMEOUT_MS;
|
|
970
|
+
try {
|
|
971
|
+
if (!isPlainObject(options))
|
|
972
|
+
throw new Error('invalid options');
|
|
973
|
+
const store = options.store;
|
|
974
|
+
if (!STORE_CAPABILITIES.every((capability) => store?.[capability] === true)
|
|
975
|
+
|| typeof store?.read !== 'function'
|
|
976
|
+
|| typeof store?.compareAndSwap !== 'function') {
|
|
977
|
+
configurationError = 'durable_cas_store_required';
|
|
978
|
+
}
|
|
979
|
+
else {
|
|
980
|
+
readStore = store.read.bind(store);
|
|
981
|
+
compareAndSwap = store.compareAndSwap.bind(store);
|
|
982
|
+
}
|
|
983
|
+
const provider = options.provider;
|
|
984
|
+
if (!configurationError
|
|
985
|
+
&& (typeof provider?.release !== 'function'
|
|
986
|
+
|| typeof provider?.getRelease !== 'function')) {
|
|
987
|
+
configurationError = 'release_provider_required';
|
|
988
|
+
}
|
|
989
|
+
else if (!configurationError) {
|
|
990
|
+
// The `if` above only takes this `else` when both provider?.release and
|
|
991
|
+
// provider?.getRelease are functions, which is impossible if provider
|
|
992
|
+
// itself were undefined (optional chaining would yield undefined, not
|
|
993
|
+
// 'function', for typeof).
|
|
994
|
+
releaseProvider = provider.release.bind(provider);
|
|
995
|
+
getProviderRelease = provider.getRelease.bind(provider);
|
|
996
|
+
}
|
|
997
|
+
const verifierEntries = [
|
|
998
|
+
['verifyDocumentActionBinding', options.verifyDocumentActionBinding],
|
|
999
|
+
['verifyAgreementAcceptance', options.verifyAgreementAcceptance],
|
|
1000
|
+
['verifyMilestoneEvidence', options.verifyMilestoneEvidence],
|
|
1001
|
+
['verifyResolutionReceipt', options.verifyResolutionReceipt],
|
|
1002
|
+
['verifyProviderStatement', options.verifyProviderStatement],
|
|
1003
|
+
['verifyStateCommand', options.verifyStateCommand],
|
|
1004
|
+
];
|
|
1005
|
+
if (!configurationError && verifierEntries.some(([, verifier]) => typeof verifier !== 'function')) {
|
|
1006
|
+
configurationError = 'pinned_verifiers_required';
|
|
1007
|
+
}
|
|
1008
|
+
else if (!configurationError) {
|
|
1009
|
+
verifyDocumentActionBinding = options.verifyDocumentActionBinding;
|
|
1010
|
+
verifyAgreementAcceptance = options.verifyAgreementAcceptance;
|
|
1011
|
+
verifyMilestoneEvidence = options.verifyMilestoneEvidence;
|
|
1012
|
+
verifyResolutionReceipt = options.verifyResolutionReceipt;
|
|
1013
|
+
verifyProviderStatement = options.verifyProviderStatement;
|
|
1014
|
+
verifyStateCommand = options.verifyStateCommand;
|
|
1015
|
+
}
|
|
1016
|
+
if (!configurationError && options.profilesById !== undefined) {
|
|
1017
|
+
const snapshot = canonicalSnapshot(options.profilesById);
|
|
1018
|
+
if (!isPlainObject(snapshot) || Object.keys(snapshot).length === 0) {
|
|
1019
|
+
configurationError = 'pinned_profile_configuration_invalid';
|
|
1020
|
+
}
|
|
1021
|
+
else {
|
|
1022
|
+
for (const [profileId, profile] of Object.entries(snapshot)) {
|
|
1023
|
+
if (profileId !== profile.profile_id || validateProfileShape(profile)) {
|
|
1024
|
+
configurationError = 'pinned_profile_configuration_invalid';
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
if (!configurationError)
|
|
1029
|
+
pinnedProfiles = deepFreeze(snapshot);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
else if (!configurationError && typeof options.resolveProfile === 'function') {
|
|
1033
|
+
resolveProfile = options.resolveProfile;
|
|
1034
|
+
}
|
|
1035
|
+
else if (!configurationError) {
|
|
1036
|
+
configurationError = 'pinned_profile_resolver_required';
|
|
1037
|
+
}
|
|
1038
|
+
if (!configurationError && options.now !== undefined && typeof options.now !== 'function') {
|
|
1039
|
+
configurationError = 'clock_required';
|
|
1040
|
+
}
|
|
1041
|
+
now = typeof options.now === 'function' ? options.now : Date.now;
|
|
1042
|
+
if (options.providerTimeoutMs !== undefined) {
|
|
1043
|
+
if (!Number.isSafeInteger(options.providerTimeoutMs)
|
|
1044
|
+
|| options.providerTimeoutMs <= 0
|
|
1045
|
+
|| options.providerTimeoutMs > 300_000) {
|
|
1046
|
+
configurationError ??= 'provider_timeout_invalid';
|
|
1047
|
+
}
|
|
1048
|
+
else {
|
|
1049
|
+
providerTimeoutMs = options.providerTimeoutMs;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
catch {
|
|
1054
|
+
configurationError ??= 'invalid_kernel_configuration';
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* @param {string} operation
|
|
1058
|
+
* @param {() => Promise<any>} task
|
|
1059
|
+
*/
|
|
1060
|
+
async function safe(operation, task) {
|
|
1061
|
+
try {
|
|
1062
|
+
return await task();
|
|
1063
|
+
}
|
|
1064
|
+
catch {
|
|
1065
|
+
return outcome({
|
|
1066
|
+
code: 'kernel_internal_refusal',
|
|
1067
|
+
operation,
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* @param {string} operation
|
|
1073
|
+
*/
|
|
1074
|
+
function configurationRefusal(operation) {
|
|
1075
|
+
return configurationError
|
|
1076
|
+
? outcome({ code: configurationError, operation })
|
|
1077
|
+
: null;
|
|
1078
|
+
}
|
|
1079
|
+
function instant() {
|
|
1080
|
+
try {
|
|
1081
|
+
// now is always assigned a real function during construction (either
|
|
1082
|
+
// options.now or the Date.now fallback) unless construction threw
|
|
1083
|
+
// first, in which case configurationError is set and callers refuse
|
|
1084
|
+
// via configurationRefusal() before reaching here.
|
|
1085
|
+
const candidate = now();
|
|
1086
|
+
const date = candidate instanceof Date ? candidate : new Date(candidate);
|
|
1087
|
+
return Number.isFinite(date.getTime()) ? date.toISOString() : null;
|
|
1088
|
+
}
|
|
1089
|
+
catch {
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
function operationInstant(record = null) {
|
|
1094
|
+
const at = instant();
|
|
1095
|
+
if (!at)
|
|
1096
|
+
return { error: 'invalid_clock' };
|
|
1097
|
+
if (record !== null && Date.parse(at) < Date.parse(record.updated_at)) {
|
|
1098
|
+
return { error: 'clock_regression' };
|
|
1099
|
+
}
|
|
1100
|
+
return { at };
|
|
1101
|
+
}
|
|
1102
|
+
async function withProviderTimeout(task) {
|
|
1103
|
+
let timer;
|
|
1104
|
+
try {
|
|
1105
|
+
return await Promise.race([
|
|
1106
|
+
Promise.resolve().then(task),
|
|
1107
|
+
new Promise((_, reject) => {
|
|
1108
|
+
timer = setTimeout(() => reject(new Error('provider_timeout')), providerTimeoutMs);
|
|
1109
|
+
}),
|
|
1110
|
+
]);
|
|
1111
|
+
}
|
|
1112
|
+
finally {
|
|
1113
|
+
clearTimeout(timer);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
async function readRecord(key) {
|
|
1117
|
+
let envelope;
|
|
1118
|
+
try {
|
|
1119
|
+
// readStore is always assigned once configurationError is null (store
|
|
1120
|
+
// validation above either sets configurationError or assigns
|
|
1121
|
+
// readStore); callers refuse via configurationRefusal() otherwise.
|
|
1122
|
+
envelope = await readStore(key);
|
|
1123
|
+
}
|
|
1124
|
+
catch {
|
|
1125
|
+
return { error: 'store_unavailable' };
|
|
1126
|
+
}
|
|
1127
|
+
if (envelope === null)
|
|
1128
|
+
return { record: null };
|
|
1129
|
+
try {
|
|
1130
|
+
if (!exactKeys(envelope, new Set(['revision', 'value']))
|
|
1131
|
+
|| !Number.isSafeInteger(envelope.revision)
|
|
1132
|
+
|| envelope.revision < 0
|
|
1133
|
+
|| typeof envelope.value !== 'string'
|
|
1134
|
+
|| Buffer.byteLength(envelope.value, 'utf8') > MAX_STATE_BYTES) {
|
|
1135
|
+
return { error: 'store_read_invalid' };
|
|
1136
|
+
}
|
|
1137
|
+
const record = JSON.parse(envelope.value);
|
|
1138
|
+
if (canonicalize(record) !== envelope.value
|
|
1139
|
+
|| !recordShapeValid(record, envelope.revision)) {
|
|
1140
|
+
return { error: 'store_record_invalid' };
|
|
1141
|
+
}
|
|
1142
|
+
return { record: deepFreeze(record) };
|
|
1143
|
+
}
|
|
1144
|
+
catch {
|
|
1145
|
+
return { error: 'store_record_invalid' };
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
async function writeRecord(key, expectedRevision, next) {
|
|
1149
|
+
let value;
|
|
1150
|
+
try {
|
|
1151
|
+
value = canonicalize(next);
|
|
1152
|
+
}
|
|
1153
|
+
catch {
|
|
1154
|
+
return { error: 'state_encoding_failed' };
|
|
1155
|
+
}
|
|
1156
|
+
let acknowledgement;
|
|
1157
|
+
try {
|
|
1158
|
+
// compareAndSwap is always assigned once configurationError is null,
|
|
1159
|
+
// for the same reason readStore is (see readRecord above).
|
|
1160
|
+
acknowledgement = await compareAndSwap(key, expectedRevision, value);
|
|
1161
|
+
}
|
|
1162
|
+
catch {
|
|
1163
|
+
return { error: 'store_unavailable' };
|
|
1164
|
+
}
|
|
1165
|
+
try {
|
|
1166
|
+
const expectedNext = expectedRevision === null ? 0 : expectedRevision + 1;
|
|
1167
|
+
if (!exactKeys(acknowledgement, new Set(['applied', 'revision']))
|
|
1168
|
+
|| (acknowledgement.applied !== true && acknowledgement.applied !== false)
|
|
1169
|
+
|| (acknowledgement.revision !== null
|
|
1170
|
+
&& (!Number.isSafeInteger(acknowledgement.revision)
|
|
1171
|
+
|| acknowledgement.revision < 0))) {
|
|
1172
|
+
return { error: 'store_acknowledgement_invalid' };
|
|
1173
|
+
}
|
|
1174
|
+
if (acknowledgement.applied === true) {
|
|
1175
|
+
if (acknowledgement.revision !== expectedNext || next.revision !== expectedNext) {
|
|
1176
|
+
return { error: 'store_revision_invalid' };
|
|
1177
|
+
}
|
|
1178
|
+
return { applied: true };
|
|
1179
|
+
}
|
|
1180
|
+
return { applied: false };
|
|
1181
|
+
}
|
|
1182
|
+
catch {
|
|
1183
|
+
return { error: 'store_acknowledgement_invalid' };
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
async function invokeVerifier(verifier, artifact, expected) {
|
|
1187
|
+
try {
|
|
1188
|
+
const artifactCopy = deepFreeze(canonicalSnapshot(artifact));
|
|
1189
|
+
const expectedCopy = deepFreeze(canonicalSnapshot(expected));
|
|
1190
|
+
const result = canonicalSnapshot(await verifier(artifactCopy, expectedCopy));
|
|
1191
|
+
if (!isPlainObject(result))
|
|
1192
|
+
return { error: 'verifier_result_invalid' };
|
|
1193
|
+
return { artifact: artifactCopy, result };
|
|
1194
|
+
}
|
|
1195
|
+
catch {
|
|
1196
|
+
return { error: 'verifier_failed' };
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
async function verifyStoredReleaseInputs(record, at) {
|
|
1200
|
+
const expectedCore = {
|
|
1201
|
+
...expectedBindings(record),
|
|
1202
|
+
parties: record.parties,
|
|
1203
|
+
profile: record.profile,
|
|
1204
|
+
};
|
|
1205
|
+
const lastSupersession = record.superseded_bindings.at(-1);
|
|
1206
|
+
const expectedSupersedes = lastSupersession?.superseded_by_binding_digest
|
|
1207
|
+
=== record.document_action_binding_digest
|
|
1208
|
+
? lastSupersession.document_action_binding_digest
|
|
1209
|
+
: undefined;
|
|
1210
|
+
const bindingExpected = expectedSupersedes === undefined
|
|
1211
|
+
? expectedCore
|
|
1212
|
+
: {
|
|
1213
|
+
...expectedCore,
|
|
1214
|
+
supersedes_document_action_binding_digest: expectedSupersedes,
|
|
1215
|
+
};
|
|
1216
|
+
const binding = await invokeVerifier(verifyDocumentActionBinding, record.document_action_binding.artifact, bindingExpected);
|
|
1217
|
+
const bindingDetails = binding.error
|
|
1218
|
+
? null
|
|
1219
|
+
: bindingVerificationDetails(binding.result, bindingExpected);
|
|
1220
|
+
const storedBindingDetails = bindingVerificationDetails(record.document_action_binding.verification, record);
|
|
1221
|
+
if (!bindingDetails
|
|
1222
|
+
|| !storedBindingDetails
|
|
1223
|
+
|| canonicalize(bindingDetails) !== canonicalize(storedBindingDetails)
|
|
1224
|
+
|| (binding.result.supersedes_document_action_binding_digest ?? null)
|
|
1225
|
+
!== (record.document_action_binding.verification
|
|
1226
|
+
.supersedes_document_action_binding_digest ?? null)) {
|
|
1227
|
+
return { code: binding.error ?? 'document_action_binding_stale' };
|
|
1228
|
+
}
|
|
1229
|
+
const template = record.document_action_binding.verification.release_action_template;
|
|
1230
|
+
const fundingExpected = {
|
|
1231
|
+
...expectedCore,
|
|
1232
|
+
provider_id: record.profile.provider_id,
|
|
1233
|
+
statement_type: 'funding',
|
|
1234
|
+
expected_status: 'funded',
|
|
1235
|
+
provider_transaction_id: template.custodian_transaction_id,
|
|
1236
|
+
provider_milestone_id: template.custodian_milestone_id,
|
|
1237
|
+
amount: template.amount,
|
|
1238
|
+
currency: template.currency,
|
|
1239
|
+
destination_id: template.destination_id,
|
|
1240
|
+
};
|
|
1241
|
+
const funding = await invokeVerifier(verifyProviderStatement, record.funding.statement, fundingExpected);
|
|
1242
|
+
if (funding.error
|
|
1243
|
+
|| !boundVerificationMatches(funding.result, expectedBindings(fundingExpected))
|
|
1244
|
+
|| funding.result.authenticated !== true
|
|
1245
|
+
|| funding.result.provider_id !== fundingExpected.provider_id
|
|
1246
|
+
|| funding.result.statement_type !== 'funding'
|
|
1247
|
+
|| funding.result.status !== 'funded'
|
|
1248
|
+
|| funding.result.provider_transaction_id !== fundingExpected.provider_transaction_id
|
|
1249
|
+
|| funding.result.provider_milestone_id !== fundingExpected.provider_milestone_id
|
|
1250
|
+
|| funding.result.amount !== fundingExpected.amount
|
|
1251
|
+
|| funding.result.currency !== fundingExpected.currency
|
|
1252
|
+
|| funding.result.destination_id !== fundingExpected.destination_id
|
|
1253
|
+
|| funding.result.statement_digest !== record.funding.verification.statement_digest) {
|
|
1254
|
+
return { code: funding.error ?? 'funding_statement_stale' };
|
|
1255
|
+
}
|
|
1256
|
+
const milestone = await invokeVerifier(verifyMilestoneEvidence, record.milestone_evidence.artifact, expectedCore);
|
|
1257
|
+
const expectedEvidenceDigest = template.completion_evidence_sha256;
|
|
1258
|
+
if (milestone.error
|
|
1259
|
+
|| !boundVerificationMatches(milestone.result, expectedBindings(expectedCore))
|
|
1260
|
+
|| !validDigest(milestone.result.evidence_digest)
|
|
1261
|
+
|| milestone.result.evidence_digest !== expectedEvidenceDigest
|
|
1262
|
+
|| milestone.result.evidence_digest
|
|
1263
|
+
!== record.milestone_evidence.verification.evidence_digest
|
|
1264
|
+
|| !record.parties.some((party) => (party.party_id === milestone.result.submitter_party_id))
|
|
1265
|
+
|| !validInstant(milestone.result.observed_at)
|
|
1266
|
+
|| Date.parse(milestone.result.observed_at) > Date.parse(at)) {
|
|
1267
|
+
return { code: milestone.error ?? 'milestone_evidence_stale' };
|
|
1268
|
+
}
|
|
1269
|
+
const bindingInput = resolutionBindingInput(record);
|
|
1270
|
+
const bindingMoment = createActionEscrowReleaseBindingMoment(bindingInput);
|
|
1271
|
+
const bindingMomentDigest = bindingMoment === null
|
|
1272
|
+
? null
|
|
1273
|
+
: canonicalDigest(bindingMoment);
|
|
1274
|
+
const expectedInitiator = record.milestone_evidence.verification.submitter_party_id;
|
|
1275
|
+
if (bindingMoment === null
|
|
1276
|
+
|| bindingMomentDigest === null
|
|
1277
|
+
|| !validString(expectedInitiator, 256)) {
|
|
1278
|
+
return { code: 'release_approval_context_invalid' };
|
|
1279
|
+
}
|
|
1280
|
+
for (const entry of record.release_approvals) {
|
|
1281
|
+
const context = entry.resolution.signoff.context;
|
|
1282
|
+
const expectedNonce = computeActionEscrowResolutionNonce(bindingInput, entry.party_id);
|
|
1283
|
+
const expected = {
|
|
1284
|
+
...expectedCore,
|
|
1285
|
+
party_id: entry.party_id,
|
|
1286
|
+
evidence_digest: record.milestone_evidence.verification.evidence_digest,
|
|
1287
|
+
binding_moment: bindingMoment,
|
|
1288
|
+
binding_moment_digest: bindingMomentDigest,
|
|
1289
|
+
expected_selected_option: 0,
|
|
1290
|
+
expected_initiator: expectedInitiator,
|
|
1291
|
+
expected_nonce: expectedNonce,
|
|
1292
|
+
evaluation_time: at,
|
|
1293
|
+
};
|
|
1294
|
+
const approval = await invokeVerifier(verifyResolutionReceipt, entry.resolution, expected);
|
|
1295
|
+
if (approval.error
|
|
1296
|
+
|| approval.result.valid !== true
|
|
1297
|
+
|| approval.result.authorizes_action !== true
|
|
1298
|
+
|| approval.result.outcome !== 'approved'
|
|
1299
|
+
|| !boundVerificationMatches(approval.result, expectedBindings(expected))
|
|
1300
|
+
|| approval.result.party_id !== entry.party_id
|
|
1301
|
+
|| approval.result.party_role
|
|
1302
|
+
!== record.parties.find((party) => party.party_id === entry.party_id)?.role
|
|
1303
|
+
|| approval.result.principal_key_id !== context.principal_key_id
|
|
1304
|
+
|| approval.result.nonce !== context.nonce
|
|
1305
|
+
|| approval.result.issued_at !== context.issued_at
|
|
1306
|
+
|| approval.result.expires_at !== context.expires_at
|
|
1307
|
+
|| approval.result.evidence_digest !== expected.evidence_digest
|
|
1308
|
+
|| approval.result.principal_key_id !== entry.verification.principal_key_id
|
|
1309
|
+
|| approval.result.nonce !== entry.verification.nonce
|
|
1310
|
+
|| approval.result.issued_at !== entry.verification.issued_at
|
|
1311
|
+
|| approval.result.expires_at !== entry.verification.expires_at) {
|
|
1312
|
+
return {
|
|
1313
|
+
code: approval.error ?? 'release_approval_stale',
|
|
1314
|
+
details: { party_id: entry.party_id },
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
return null;
|
|
1319
|
+
}
|
|
1320
|
+
async function verifyCommandAuthorization(artifact, record, command, partyId, details) {
|
|
1321
|
+
if (!record.parties.some((party) => party.party_id === partyId)) {
|
|
1322
|
+
return { error: 'command_party_invalid' };
|
|
1323
|
+
}
|
|
1324
|
+
const detailsDigest = canonicalDigest(details);
|
|
1325
|
+
const expected = {
|
|
1326
|
+
...expectedBindings(record),
|
|
1327
|
+
profile: record.profile,
|
|
1328
|
+
parties: record.parties,
|
|
1329
|
+
command,
|
|
1330
|
+
party_id: partyId,
|
|
1331
|
+
details_digest: detailsDigest,
|
|
1332
|
+
};
|
|
1333
|
+
expected.command_digest = canonicalDigest({
|
|
1334
|
+
'@version': 'EP-ACTION-ESCROW-COMMAND-v1',
|
|
1335
|
+
...expectedBindings(record),
|
|
1336
|
+
command,
|
|
1337
|
+
party_id: partyId,
|
|
1338
|
+
details_digest: detailsDigest,
|
|
1339
|
+
});
|
|
1340
|
+
const verified = await invokeVerifier(verifyStateCommand, artifact, expected);
|
|
1341
|
+
if (verified.error
|
|
1342
|
+
|| verified.result.valid !== true
|
|
1343
|
+
|| verified.result.authorizes_command !== true
|
|
1344
|
+
|| verified.result.command !== command
|
|
1345
|
+
|| verified.result.party_id !== partyId
|
|
1346
|
+
|| verified.result.details_digest !== detailsDigest
|
|
1347
|
+
|| verified.result.command_digest !== expected.command_digest
|
|
1348
|
+
|| !boundVerificationMatches(verified.result, expectedBindings(record))) {
|
|
1349
|
+
return { error: verified.error ?? 'command_authorization_refused' };
|
|
1350
|
+
}
|
|
1351
|
+
return {
|
|
1352
|
+
artifact: verified.artifact,
|
|
1353
|
+
verification: {
|
|
1354
|
+
valid: true,
|
|
1355
|
+
authorizes_command: true,
|
|
1356
|
+
...expectedBindings(record),
|
|
1357
|
+
command,
|
|
1358
|
+
party_id: partyId,
|
|
1359
|
+
details_digest: detailsDigest,
|
|
1360
|
+
command_digest: expected.command_digest,
|
|
1361
|
+
},
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
async function selectedProfile(normalized) {
|
|
1365
|
+
try {
|
|
1366
|
+
let selected;
|
|
1367
|
+
if (pinnedProfiles) {
|
|
1368
|
+
if (!Object.hasOwn(pinnedProfiles, normalized.snapshot.profile.profile_id)) {
|
|
1369
|
+
return { error: 'profile_not_pinned' };
|
|
1370
|
+
}
|
|
1371
|
+
selected = pinnedProfiles[normalized.snapshot.profile.profile_id];
|
|
1372
|
+
}
|
|
1373
|
+
else {
|
|
1374
|
+
// Construction guarantees exactly one of pinnedProfiles/resolveProfile
|
|
1375
|
+
// is set once configurationError is null (see the profilesById /
|
|
1376
|
+
// resolveProfile branch above); pinnedProfiles is falsy here, so
|
|
1377
|
+
// resolveProfile must be the one that was assigned.
|
|
1378
|
+
selected = canonicalSnapshot(await resolveProfile(normalized.snapshot.profile.profile_id, deepFreeze(canonicalSnapshot({
|
|
1379
|
+
agreement_digest: normalized.context.agreement_digest,
|
|
1380
|
+
milestone_id: normalized.context.milestone_id,
|
|
1381
|
+
parties: normalized.context.parties,
|
|
1382
|
+
}))));
|
|
1383
|
+
}
|
|
1384
|
+
if (validateProfileForParties(selected, normalized.context.parties)
|
|
1385
|
+
|| canonicalDigest(selected) !== normalized.context.profile_digest) {
|
|
1386
|
+
return { error: 'profile_not_pinned' };
|
|
1387
|
+
}
|
|
1388
|
+
return { profile: deepFreeze(canonicalSnapshot(selected)) };
|
|
1389
|
+
}
|
|
1390
|
+
catch {
|
|
1391
|
+
return { error: 'profile_resolution_failed' };
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
async function create(input = {}) {
|
|
1395
|
+
const operation = 'create';
|
|
1396
|
+
return safe(operation, async () => {
|
|
1397
|
+
const normalized = normalizedInput(operation, input, ['document_action_binding']);
|
|
1398
|
+
if (normalized.error)
|
|
1399
|
+
return outcome({ code: normalized.error, operation });
|
|
1400
|
+
const config = configurationRefusal(operation);
|
|
1401
|
+
if (config)
|
|
1402
|
+
return config;
|
|
1403
|
+
const pinned = await selectedProfile(normalized);
|
|
1404
|
+
if (pinned.error)
|
|
1405
|
+
return outcome({ code: pinned.error, operation });
|
|
1406
|
+
for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
|
|
1407
|
+
const loaded = await readRecord(normalized.escrowKey);
|
|
1408
|
+
if (loaded.error)
|
|
1409
|
+
return outcome({ code: loaded.error, operation });
|
|
1410
|
+
if (loaded.record) {
|
|
1411
|
+
if (!operationBindingMatches(loaded.record, normalized)) {
|
|
1412
|
+
return outcome({
|
|
1413
|
+
code: 'operation_binding_mismatch',
|
|
1414
|
+
operation,
|
|
1415
|
+
record: loaded.record,
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
return idempotentResult(loaded.record, normalized, operation)
|
|
1419
|
+
?? outcome({ code: 'escrow_already_exists', operation, record: loaded.record });
|
|
1420
|
+
}
|
|
1421
|
+
const at = instant();
|
|
1422
|
+
if (!at)
|
|
1423
|
+
return outcome({ code: 'invalid_clock', operation });
|
|
1424
|
+
const expected = {
|
|
1425
|
+
...normalized.context,
|
|
1426
|
+
profile: pinned.profile,
|
|
1427
|
+
};
|
|
1428
|
+
const verification = await invokeVerifier(verifyDocumentActionBinding, normalized.snapshot.document_action_binding, expected);
|
|
1429
|
+
const bindingDetails = verification.error
|
|
1430
|
+
? null
|
|
1431
|
+
: bindingVerificationDetails(verification.result, expected);
|
|
1432
|
+
if (!bindingDetails) {
|
|
1433
|
+
return outcome({
|
|
1434
|
+
code: verification.error ?? 'document_action_binding_invalid',
|
|
1435
|
+
operation,
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
// normalizedInput() only omits `context` on its early `{ error }`
|
|
1439
|
+
// returns, all of which are handled above via the `normalized.error`
|
|
1440
|
+
// checks; once past those, `context` is always populated.
|
|
1441
|
+
const normalizedContext = normalized.context;
|
|
1442
|
+
const record = {
|
|
1443
|
+
'@version': ACTION_ESCROW_STATE_VERSION,
|
|
1444
|
+
escrow_key: normalized.escrowKey,
|
|
1445
|
+
revision: 0,
|
|
1446
|
+
state: 'draft',
|
|
1447
|
+
agreement_digest: normalizedContext.agreement_digest,
|
|
1448
|
+
document_action_binding_digest: normalizedContext.document_action_binding_digest,
|
|
1449
|
+
milestone_id: normalizedContext.milestone_id,
|
|
1450
|
+
release_action_digest: normalizedContext.release_action_digest,
|
|
1451
|
+
parties: normalizedContext.parties,
|
|
1452
|
+
parties_digest: normalizedContext.parties_digest,
|
|
1453
|
+
profile: pinned.profile,
|
|
1454
|
+
profile_digest: normalizedContext.profile_digest,
|
|
1455
|
+
document_action_binding: {
|
|
1456
|
+
artifact: verification.artifact,
|
|
1457
|
+
verification: boundVerificationSummary(verification.result, expected, bindingDetails),
|
|
1458
|
+
},
|
|
1459
|
+
agreement_acceptances: [],
|
|
1460
|
+
funding: null,
|
|
1461
|
+
milestone_evidence: null,
|
|
1462
|
+
release_approvals: [],
|
|
1463
|
+
release: null,
|
|
1464
|
+
dispute: null,
|
|
1465
|
+
cancellation: null,
|
|
1466
|
+
completion: null,
|
|
1467
|
+
pending_amendment: null,
|
|
1468
|
+
superseded_bindings: [],
|
|
1469
|
+
operations: [{
|
|
1470
|
+
idempotency_key: normalized.snapshot.idempotency_key,
|
|
1471
|
+
operation,
|
|
1472
|
+
request_digest: normalized.requestDigest,
|
|
1473
|
+
code: 'escrow_created',
|
|
1474
|
+
ok: true,
|
|
1475
|
+
outcome: 'applied',
|
|
1476
|
+
state: 'draft',
|
|
1477
|
+
at,
|
|
1478
|
+
}],
|
|
1479
|
+
history: [{
|
|
1480
|
+
from: null,
|
|
1481
|
+
to: 'draft',
|
|
1482
|
+
operation,
|
|
1483
|
+
idempotency_key: normalized.snapshot.idempotency_key,
|
|
1484
|
+
at,
|
|
1485
|
+
}],
|
|
1486
|
+
created_at: at,
|
|
1487
|
+
updated_at: at,
|
|
1488
|
+
};
|
|
1489
|
+
const written = await writeRecord(normalized.escrowKey, null, record);
|
|
1490
|
+
if (written.error)
|
|
1491
|
+
return outcome({ code: written.error, operation });
|
|
1492
|
+
if (written.applied) {
|
|
1493
|
+
return outcome({
|
|
1494
|
+
ok: true,
|
|
1495
|
+
type: 'applied',
|
|
1496
|
+
code: 'escrow_created',
|
|
1497
|
+
operation,
|
|
1498
|
+
record,
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
return outcome({ code: 'store_conflict', operation });
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
async function mutate(operation, input, { extraAllowed = [], extraRequired = extraAllowed, bindingMode = 'active', transform, }) {
|
|
1506
|
+
return safe(operation, async () => {
|
|
1507
|
+
const normalized = normalizedInput(operation, input, extraAllowed, extraRequired);
|
|
1508
|
+
if (normalized.error)
|
|
1509
|
+
return outcome({ code: normalized.error, operation });
|
|
1510
|
+
const config = configurationRefusal(operation);
|
|
1511
|
+
if (config)
|
|
1512
|
+
return config;
|
|
1513
|
+
for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
|
|
1514
|
+
const loaded = await readRecord(normalized.escrowKey);
|
|
1515
|
+
if (loaded.error)
|
|
1516
|
+
return outcome({ code: loaded.error, operation });
|
|
1517
|
+
if (!loaded.record)
|
|
1518
|
+
return outcome({ code: 'escrow_not_found', operation });
|
|
1519
|
+
const record = loaded.record;
|
|
1520
|
+
if (!operationBindingMatches(record, normalized, bindingMode)) {
|
|
1521
|
+
return outcome({
|
|
1522
|
+
code: 'operation_binding_mismatch',
|
|
1523
|
+
operation,
|
|
1524
|
+
record,
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
const repeated = idempotentResult(record, normalized, operation);
|
|
1528
|
+
if (repeated)
|
|
1529
|
+
return repeated;
|
|
1530
|
+
const operationTime = operationInstant(record);
|
|
1531
|
+
if (operationTime.error) {
|
|
1532
|
+
return outcome({ code: operationTime.error, operation, record });
|
|
1533
|
+
}
|
|
1534
|
+
// operationInstant() only omits `at` on its `{ error }` return,
|
|
1535
|
+
// already handled above, so `at` is guaranteed present here.
|
|
1536
|
+
const { at } = operationTime;
|
|
1537
|
+
const draft = canonicalSnapshot(record);
|
|
1538
|
+
const decision = await transform(draft, normalized, at);
|
|
1539
|
+
if (decision?.refusal) {
|
|
1540
|
+
return outcome({
|
|
1541
|
+
code: decision.refusal,
|
|
1542
|
+
operation,
|
|
1543
|
+
record,
|
|
1544
|
+
details: decision.details ?? null,
|
|
1545
|
+
type: decision.type ?? 'refused',
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
const code = decision?.code ?? `${operation}_applied`;
|
|
1549
|
+
const finalized = finalizeMutation(record, normalized, operation, code, at, (next) => {
|
|
1550
|
+
Object.assign(next, draft);
|
|
1551
|
+
}, {
|
|
1552
|
+
ok: decision?.ok ?? true,
|
|
1553
|
+
type: decision?.type ?? 'applied',
|
|
1554
|
+
});
|
|
1555
|
+
if (finalized.error) {
|
|
1556
|
+
return outcome({ code: finalized.error, operation, record });
|
|
1557
|
+
}
|
|
1558
|
+
const written = await writeRecord(normalized.escrowKey, record.revision, finalized.next);
|
|
1559
|
+
if (written.error)
|
|
1560
|
+
return outcome({ code: written.error, operation, record });
|
|
1561
|
+
if (written.applied) {
|
|
1562
|
+
return outcome({
|
|
1563
|
+
ok: decision?.ok ?? true,
|
|
1564
|
+
type: decision?.type ?? 'applied',
|
|
1565
|
+
code,
|
|
1566
|
+
operation,
|
|
1567
|
+
record: finalized.next,
|
|
1568
|
+
details: decision?.details ?? null,
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
return outcome({ code: 'store_conflict', operation });
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
async function beginAcceptance(input = {}) {
|
|
1576
|
+
return mutate('begin_acceptance', input, {
|
|
1577
|
+
transform(draft) {
|
|
1578
|
+
if (draft.state !== 'draft') {
|
|
1579
|
+
return { refusal: 'invalid_state_transition' };
|
|
1580
|
+
}
|
|
1581
|
+
draft.state = 'awaiting_acceptance';
|
|
1582
|
+
return { code: 'acceptance_requested' };
|
|
1583
|
+
},
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
async function acceptAgreement(input = {}) {
|
|
1587
|
+
return mutate('accept_agreement', input, {
|
|
1588
|
+
extraAllowed: ['party_id', 'agreement_acceptance'],
|
|
1589
|
+
async transform(draft, normalized, at) {
|
|
1590
|
+
if (draft.state !== 'awaiting_acceptance') {
|
|
1591
|
+
return { refusal: 'invalid_state_transition' };
|
|
1592
|
+
}
|
|
1593
|
+
const partyId = normalized.snapshot.party_id;
|
|
1594
|
+
if (!draft.profile.required_acceptance_party_ids.includes(partyId)) {
|
|
1595
|
+
return { refusal: 'acceptance_party_not_required' };
|
|
1596
|
+
}
|
|
1597
|
+
if (draft.agreement_acceptances.some((entry) => entry.party_id === partyId)) {
|
|
1598
|
+
return { refusal: 'agreement_already_accepted' };
|
|
1599
|
+
}
|
|
1600
|
+
const expected = {
|
|
1601
|
+
...normalized.context,
|
|
1602
|
+
party_id: partyId,
|
|
1603
|
+
};
|
|
1604
|
+
const verified = await invokeVerifier(verifyAgreementAcceptance, normalized.snapshot.agreement_acceptance, expected);
|
|
1605
|
+
if (verified.error
|
|
1606
|
+
|| !boundVerificationMatches(verified.result, expectedBindings(expected))
|
|
1607
|
+
|| verified.result.party_id !== partyId
|
|
1608
|
+
|| !validString(verified.result.principal_key_id, 512)
|
|
1609
|
+
|| draft.agreement_acceptances.some((entry) => entry.verification?.principal_key_id
|
|
1610
|
+
=== verified.result.principal_key_id)
|
|
1611
|
+
|| !validDigest(verified.result.acceptance_digest)) {
|
|
1612
|
+
return { refusal: verified.error ?? 'agreement_acceptance_invalid' };
|
|
1613
|
+
}
|
|
1614
|
+
draft.agreement_acceptances.push({
|
|
1615
|
+
party_id: partyId,
|
|
1616
|
+
artifact: verified.artifact,
|
|
1617
|
+
verification: boundVerificationSummary(verified.result, expected, {
|
|
1618
|
+
party_id: partyId,
|
|
1619
|
+
principal_key_id: verified.result.principal_key_id,
|
|
1620
|
+
acceptance_digest: verified.result.acceptance_digest,
|
|
1621
|
+
}),
|
|
1622
|
+
});
|
|
1623
|
+
const accepted = new Set(draft.agreement_acceptances.map((entry) => entry.party_id));
|
|
1624
|
+
if (draft.profile.required_acceptance_party_ids.every((id) => accepted.has(id))) {
|
|
1625
|
+
draft.state = 'effective';
|
|
1626
|
+
return { code: 'agreement_effective' };
|
|
1627
|
+
}
|
|
1628
|
+
return { code: 'agreement_acceptance_recorded' };
|
|
1629
|
+
},
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
async function requestFunding(input = {}) {
|
|
1633
|
+
return mutate('request_funding', input, {
|
|
1634
|
+
transform(draft) {
|
|
1635
|
+
if (draft.state !== 'effective') {
|
|
1636
|
+
return { refusal: 'invalid_state_transition' };
|
|
1637
|
+
}
|
|
1638
|
+
draft.state = 'awaiting_funding';
|
|
1639
|
+
return { code: 'funding_requested' };
|
|
1640
|
+
},
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
async function recordFunding(input = {}) {
|
|
1644
|
+
return mutate('record_funding', input, {
|
|
1645
|
+
extraAllowed: ['provider_statement'],
|
|
1646
|
+
async transform(draft, normalized, at) {
|
|
1647
|
+
if (draft.state !== 'awaiting_funding') {
|
|
1648
|
+
return { refusal: 'invalid_state_transition' };
|
|
1649
|
+
}
|
|
1650
|
+
const template = draft.document_action_binding.verification.release_action_template;
|
|
1651
|
+
const expected = {
|
|
1652
|
+
...normalized.context,
|
|
1653
|
+
provider_id: draft.profile.provider_id,
|
|
1654
|
+
statement_type: 'funding',
|
|
1655
|
+
expected_status: 'funded',
|
|
1656
|
+
provider_transaction_id: template.custodian_transaction_id,
|
|
1657
|
+
provider_milestone_id: template.custodian_milestone_id,
|
|
1658
|
+
amount: template.amount,
|
|
1659
|
+
currency: template.currency,
|
|
1660
|
+
destination_id: template.destination_id,
|
|
1661
|
+
};
|
|
1662
|
+
const verified = await invokeVerifier(verifyProviderStatement, normalized.snapshot.provider_statement, expected);
|
|
1663
|
+
if (verified.error
|
|
1664
|
+
|| !boundVerificationMatches(verified.result, expectedBindings(expected))
|
|
1665
|
+
|| verified.result.authenticated !== true
|
|
1666
|
+
|| verified.result.provider_id !== draft.profile.provider_id
|
|
1667
|
+
|| verified.result.statement_type !== 'funding'
|
|
1668
|
+
|| verified.result.status !== 'funded'
|
|
1669
|
+
|| verified.result.provider_transaction_id !== expected.provider_transaction_id
|
|
1670
|
+
|| verified.result.provider_milestone_id !== expected.provider_milestone_id
|
|
1671
|
+
|| verified.result.amount !== expected.amount
|
|
1672
|
+
|| verified.result.currency !== expected.currency
|
|
1673
|
+
|| verified.result.destination_id !== expected.destination_id
|
|
1674
|
+
|| !validDigest(verified.result.statement_digest)) {
|
|
1675
|
+
return { refusal: verified.error ?? 'funding_statement_invalid' };
|
|
1676
|
+
}
|
|
1677
|
+
draft.funding = {
|
|
1678
|
+
statement: verified.artifact,
|
|
1679
|
+
verification: boundVerificationSummary(verified.result, expected, {
|
|
1680
|
+
authenticated: true,
|
|
1681
|
+
provider_id: draft.profile.provider_id,
|
|
1682
|
+
statement_type: 'funding',
|
|
1683
|
+
status: 'funded',
|
|
1684
|
+
provider_transaction_id: expected.provider_transaction_id,
|
|
1685
|
+
provider_milestone_id: expected.provider_milestone_id,
|
|
1686
|
+
amount: expected.amount,
|
|
1687
|
+
currency: expected.currency,
|
|
1688
|
+
destination_id: expected.destination_id,
|
|
1689
|
+
statement_digest: verified.result.statement_digest,
|
|
1690
|
+
}),
|
|
1691
|
+
};
|
|
1692
|
+
draft.state = 'funded';
|
|
1693
|
+
return { code: 'funding_confirmed' };
|
|
1694
|
+
},
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
async function submitMilestone(input = {}) {
|
|
1698
|
+
return mutate('submit_milestone', input, {
|
|
1699
|
+
extraAllowed: ['milestone_evidence'],
|
|
1700
|
+
async transform(draft, normalized, at) {
|
|
1701
|
+
if (draft.state !== 'funded') {
|
|
1702
|
+
return { refusal: 'invalid_state_transition' };
|
|
1703
|
+
}
|
|
1704
|
+
const expected = normalized.context;
|
|
1705
|
+
const verified = await invokeVerifier(verifyMilestoneEvidence, normalized.snapshot.milestone_evidence, expected);
|
|
1706
|
+
const partyIds = draft.parties.map((party) => party.party_id);
|
|
1707
|
+
const expectedEvidenceDigest = draft.document_action_binding
|
|
1708
|
+
.verification.release_action_template.completion_evidence_sha256;
|
|
1709
|
+
if (verified.error
|
|
1710
|
+
|| !boundVerificationMatches(verified.result, expectedBindings(expected))
|
|
1711
|
+
|| !validDigest(verified.result.evidence_digest)
|
|
1712
|
+
|| verified.result.evidence_digest !== expectedEvidenceDigest
|
|
1713
|
+
|| !partyIds.includes(verified.result.submitter_party_id)
|
|
1714
|
+
|| !validInstant(verified.result.observed_at)
|
|
1715
|
+
|| Date.parse(verified.result.observed_at) > Date.parse(at)) {
|
|
1716
|
+
return { refusal: verified.error ?? 'milestone_evidence_invalid' };
|
|
1717
|
+
}
|
|
1718
|
+
draft.milestone_evidence = {
|
|
1719
|
+
artifact: verified.artifact,
|
|
1720
|
+
verification: boundVerificationSummary(verified.result, expected, {
|
|
1721
|
+
evidence_digest: verified.result.evidence_digest,
|
|
1722
|
+
submitter_party_id: verified.result.submitter_party_id,
|
|
1723
|
+
observed_at: verified.result.observed_at,
|
|
1724
|
+
}),
|
|
1725
|
+
};
|
|
1726
|
+
draft.release_approvals = [];
|
|
1727
|
+
draft.state = 'milestone_submitted';
|
|
1728
|
+
return { code: 'milestone_evidence_recorded' };
|
|
1729
|
+
},
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
async function approveRelease(input = {}) {
|
|
1733
|
+
return mutate('approve_release', input, {
|
|
1734
|
+
extraAllowed: ['party_id', 'resolution'],
|
|
1735
|
+
async transform(draft, normalized, at) {
|
|
1736
|
+
if (draft.state !== 'milestone_submitted') {
|
|
1737
|
+
return { refusal: 'invalid_state_transition' };
|
|
1738
|
+
}
|
|
1739
|
+
const partyId = normalized.snapshot.party_id;
|
|
1740
|
+
if (!draft.profile.required_release_approver_party_ids.includes(partyId)) {
|
|
1741
|
+
return { refusal: 'release_approval_party_not_required' };
|
|
1742
|
+
}
|
|
1743
|
+
const party = draft.parties.find((entry) => entry.party_id === partyId);
|
|
1744
|
+
if (draft.profile.prohibit_self_approval
|
|
1745
|
+
&& partyId === draft.milestone_evidence?.verification?.submitter_party_id) {
|
|
1746
|
+
return { refusal: 'self_approval_refused' };
|
|
1747
|
+
}
|
|
1748
|
+
if (draft.release_approvals.some((entry) => entry.party_id === partyId)) {
|
|
1749
|
+
return { refusal: 'release_approval_already_recorded' };
|
|
1750
|
+
}
|
|
1751
|
+
const artifact = normalized.snapshot.resolution;
|
|
1752
|
+
const bindingInput = resolutionBindingInput(draft);
|
|
1753
|
+
const bindingMoment = createActionEscrowReleaseBindingMoment(bindingInput);
|
|
1754
|
+
const bindingMomentDigest = bindingMoment === null
|
|
1755
|
+
? null
|
|
1756
|
+
: canonicalDigest(bindingMoment);
|
|
1757
|
+
const expectedNonce = computeActionEscrowResolutionNonce(bindingInput, partyId);
|
|
1758
|
+
const expectedInitiator = draft.milestone_evidence?.verification?.submitter_party_id;
|
|
1759
|
+
if (!isPlainObject(artifact)
|
|
1760
|
+
|| artifact.profile !== RESOLUTION_VERSION
|
|
1761
|
+
|| !isPlainObject(artifact.signoff)
|
|
1762
|
+
|| !isPlainObject(artifact.signoff.context)
|
|
1763
|
+
|| bindingMoment === null
|
|
1764
|
+
|| bindingMomentDigest === null
|
|
1765
|
+
|| expectedNonce === null
|
|
1766
|
+
|| !validString(expectedInitiator, 256)) {
|
|
1767
|
+
return { refusal: 'resolution_profile_invalid' };
|
|
1768
|
+
}
|
|
1769
|
+
const resolutionContext = artifact.signoff.context;
|
|
1770
|
+
if (resolutionContext.principal !== partyId) {
|
|
1771
|
+
return { refusal: 'resolution_party_mismatch' };
|
|
1772
|
+
}
|
|
1773
|
+
if (resolutionContext.envelope_hash !== bindingMomentDigest) {
|
|
1774
|
+
return { refusal: 'resolution_binding_mismatch' };
|
|
1775
|
+
}
|
|
1776
|
+
if (resolutionContext.action_hash !== draft.release_action_digest) {
|
|
1777
|
+
return { refusal: 'resolution_action_mismatch' };
|
|
1778
|
+
}
|
|
1779
|
+
if (resolutionContext.initiator !== expectedInitiator) {
|
|
1780
|
+
return { refusal: 'resolution_initiator_mismatch' };
|
|
1781
|
+
}
|
|
1782
|
+
if (resolutionContext.nonce !== expectedNonce) {
|
|
1783
|
+
return { refusal: 'resolution_nonce_mismatch' };
|
|
1784
|
+
}
|
|
1785
|
+
if (resolutionContext.resolution?.outcome !== 'approved') {
|
|
1786
|
+
return { refusal: 'resolution_not_approved' };
|
|
1787
|
+
}
|
|
1788
|
+
if (!validString(resolutionContext.principal_key_id, 512)
|
|
1789
|
+
|| !validString(resolutionContext.nonce, 512)
|
|
1790
|
+
|| !validInstant(resolutionContext.issued_at)
|
|
1791
|
+
|| !validInstant(resolutionContext.expires_at)
|
|
1792
|
+
|| Date.parse(resolutionContext.issued_at)
|
|
1793
|
+
< Date.parse(draft.milestone_evidence.verification.observed_at)
|
|
1794
|
+
|| Date.parse(resolutionContext.issued_at) > Date.parse(at)
|
|
1795
|
+
|| Date.parse(resolutionContext.expires_at) <= Date.parse(at)
|
|
1796
|
+
|| Date.parse(resolutionContext.expires_at)
|
|
1797
|
+
<= Date.parse(resolutionContext.issued_at)) {
|
|
1798
|
+
return { refusal: 'resolution_freshness_invalid' };
|
|
1799
|
+
}
|
|
1800
|
+
if (draft.release_approvals.some((entry) => entry.verification?.principal_key_id
|
|
1801
|
+
=== resolutionContext.principal_key_id)) {
|
|
1802
|
+
return { refusal: 'resolution_key_already_counted' };
|
|
1803
|
+
}
|
|
1804
|
+
const expected = {
|
|
1805
|
+
...normalized.context,
|
|
1806
|
+
party_id: partyId,
|
|
1807
|
+
evidence_digest: draft.milestone_evidence.verification.evidence_digest,
|
|
1808
|
+
binding_moment: bindingMoment,
|
|
1809
|
+
binding_moment_digest: bindingMomentDigest,
|
|
1810
|
+
expected_selected_option: 0,
|
|
1811
|
+
expected_initiator: expectedInitiator,
|
|
1812
|
+
expected_nonce: expectedNonce,
|
|
1813
|
+
evaluation_time: at,
|
|
1814
|
+
};
|
|
1815
|
+
const verified = await invokeVerifier(verifyResolutionReceipt, artifact, expected);
|
|
1816
|
+
if (verified.error
|
|
1817
|
+
|| verified.result.valid !== true
|
|
1818
|
+
|| verified.result.authorizes_action !== true
|
|
1819
|
+
|| verified.result.outcome !== 'approved'
|
|
1820
|
+
|| !boundVerificationMatches(verified.result, expectedBindings(expected))
|
|
1821
|
+
|| verified.result.party_id !== partyId
|
|
1822
|
+
|| verified.result.party_role !== party.role
|
|
1823
|
+
|| verified.result.principal_key_id !== resolutionContext.principal_key_id
|
|
1824
|
+
|| verified.result.nonce !== resolutionContext.nonce
|
|
1825
|
+
|| verified.result.issued_at !== resolutionContext.issued_at
|
|
1826
|
+
|| verified.result.expires_at !== resolutionContext.expires_at
|
|
1827
|
+
|| verified.result.evidence_digest !== expected.evidence_digest) {
|
|
1828
|
+
return { refusal: verified.error ?? 'resolution_verification_refused' };
|
|
1829
|
+
}
|
|
1830
|
+
draft.release_approvals.push({
|
|
1831
|
+
party_id: partyId,
|
|
1832
|
+
resolution: verified.artifact,
|
|
1833
|
+
verification: {
|
|
1834
|
+
valid: true,
|
|
1835
|
+
authorizes_action: true,
|
|
1836
|
+
outcome: 'approved',
|
|
1837
|
+
party_role: party.role,
|
|
1838
|
+
principal_key_id: resolutionContext.principal_key_id,
|
|
1839
|
+
nonce: resolutionContext.nonce,
|
|
1840
|
+
issued_at: resolutionContext.issued_at,
|
|
1841
|
+
expires_at: resolutionContext.expires_at,
|
|
1842
|
+
resolution_digest: canonicalDigest(verified.artifact),
|
|
1843
|
+
agreement_digest: draft.agreement_digest,
|
|
1844
|
+
document_action_binding_digest: draft.document_action_binding_digest,
|
|
1845
|
+
milestone_id: draft.milestone_id,
|
|
1846
|
+
release_action_digest: draft.release_action_digest,
|
|
1847
|
+
parties_digest: draft.parties_digest,
|
|
1848
|
+
profile_digest: draft.profile_digest,
|
|
1849
|
+
evidence_digest: draft.milestone_evidence.verification.evidence_digest,
|
|
1850
|
+
},
|
|
1851
|
+
});
|
|
1852
|
+
return { code: 'release_approval_recorded' };
|
|
1853
|
+
},
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
async function releasePreconditions(record, at) {
|
|
1857
|
+
if (record.funding?.verification?.status !== 'funded') {
|
|
1858
|
+
return { code: 'funding_not_verified' };
|
|
1859
|
+
}
|
|
1860
|
+
if (!validDigest(record.milestone_evidence?.verification?.evidence_digest)) {
|
|
1861
|
+
return { code: 'milestone_evidence_not_verified' };
|
|
1862
|
+
}
|
|
1863
|
+
const approved = new Set(record.release_approvals
|
|
1864
|
+
.filter((entry) => entry.verification?.authorizes_action === true
|
|
1865
|
+
&& entry.verification?.document_action_binding_digest
|
|
1866
|
+
=== record.document_action_binding_digest
|
|
1867
|
+
&& entry.verification?.release_action_digest === record.release_action_digest
|
|
1868
|
+
&& entry.verification?.evidence_digest
|
|
1869
|
+
=== record.milestone_evidence.verification.evidence_digest)
|
|
1870
|
+
.map((entry) => entry.party_id));
|
|
1871
|
+
const missing = record.profile.required_release_approver_party_ids
|
|
1872
|
+
.filter((partyId) => !approved.has(partyId));
|
|
1873
|
+
if (missing.length > 0) {
|
|
1874
|
+
return { code: 'release_approval_missing', details: { missing_party_ids: missing } };
|
|
1875
|
+
}
|
|
1876
|
+
const evaluationTime = Date.parse(at);
|
|
1877
|
+
const stale = record.release_approvals
|
|
1878
|
+
.filter((entry) => (!validInstant(entry.verification?.issued_at)
|
|
1879
|
+
|| !validInstant(entry.verification?.expires_at)
|
|
1880
|
+
|| Date.parse(entry.verification.issued_at) > evaluationTime
|
|
1881
|
+
|| Date.parse(entry.verification.expires_at) <= evaluationTime))
|
|
1882
|
+
.map((entry) => entry.party_id);
|
|
1883
|
+
if (stale.length > 0) {
|
|
1884
|
+
return { code: 'release_approval_expired', details: { party_ids: stale } };
|
|
1885
|
+
}
|
|
1886
|
+
return verifyStoredReleaseInputs(record, at);
|
|
1887
|
+
}
|
|
1888
|
+
function providerRequestFor(record) {
|
|
1889
|
+
const bindingVerification = record.document_action_binding.verification;
|
|
1890
|
+
const request = {
|
|
1891
|
+
method: 'POST',
|
|
1892
|
+
provider_id: record.profile.provider_id,
|
|
1893
|
+
agreement_digest: record.agreement_digest,
|
|
1894
|
+
document_action_binding_digest: record.document_action_binding_digest,
|
|
1895
|
+
milestone_id: record.milestone_id,
|
|
1896
|
+
release_action_digest: record.release_action_digest,
|
|
1897
|
+
parties: record.parties,
|
|
1898
|
+
parties_digest: record.parties_digest,
|
|
1899
|
+
profile: record.profile,
|
|
1900
|
+
profile_digest: record.profile_digest,
|
|
1901
|
+
agreement_id: bindingVerification.agreement_id,
|
|
1902
|
+
binding_id: bindingVerification.binding_id,
|
|
1903
|
+
document_digest: bindingVerification.document_digest,
|
|
1904
|
+
release_action_template: bindingVerification.release_action_template,
|
|
1905
|
+
release_key: releaseReservationKey(record),
|
|
1906
|
+
idempotency_key: providerIdempotencyKey(record),
|
|
1907
|
+
};
|
|
1908
|
+
return {
|
|
1909
|
+
...request,
|
|
1910
|
+
request_digest: canonicalDigest({
|
|
1911
|
+
'@version': 'EP-ACTION-ESCROW-PROVIDER-REQUEST-v1',
|
|
1912
|
+
...request,
|
|
1913
|
+
}),
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
async function authoritativeProviderRelease(record) {
|
|
1917
|
+
const request = {
|
|
1918
|
+
...record.release.provider_request,
|
|
1919
|
+
method: 'GET',
|
|
1920
|
+
};
|
|
1921
|
+
let response;
|
|
1922
|
+
try {
|
|
1923
|
+
// getProviderRelease is always assigned once configurationError is
|
|
1924
|
+
// null (see the provider validation above); callers refuse via
|
|
1925
|
+
// configurationRefusal() otherwise.
|
|
1926
|
+
response = canonicalSnapshot(await withProviderTimeout(() => getProviderRelease(deepFreeze(canonicalSnapshot(request)))));
|
|
1927
|
+
}
|
|
1928
|
+
catch {
|
|
1929
|
+
return { error: 'provider_reconciliation_failed' };
|
|
1930
|
+
}
|
|
1931
|
+
if (!exactKeys(response, new Set(['authenticated', 'statement']))
|
|
1932
|
+
|| response.authenticated !== true) {
|
|
1933
|
+
return { error: 'provider_reconciliation_unauthenticated' };
|
|
1934
|
+
}
|
|
1935
|
+
const expected = providerExpected(record);
|
|
1936
|
+
const verified = await invokeVerifier(verifyProviderStatement, response.statement, expected);
|
|
1937
|
+
if (verified.error
|
|
1938
|
+
|| !boundVerificationMatches(verified.result, expectedBindings(expected))
|
|
1939
|
+
|| verified.result.authenticated !== true
|
|
1940
|
+
|| verified.result.provider_id !== expected.provider_id
|
|
1941
|
+
|| verified.result.statement_type !== 'release'
|
|
1942
|
+
|| !['released', 'not_released', 'pending'].includes(verified.result.status)
|
|
1943
|
+
|| !validDigest(verified.result.statement_digest)
|
|
1944
|
+
|| verified.result.provider_idempotency_key !== expected.provider_idempotency_key
|
|
1945
|
+
|| verified.result.provider_request_digest !== expected.provider_request_digest
|
|
1946
|
+
|| verified.result.provider_transaction_id !== expected.provider_transaction_id
|
|
1947
|
+
|| verified.result.provider_milestone_id !== expected.provider_milestone_id
|
|
1948
|
+
|| verified.result.amount !== expected.amount
|
|
1949
|
+
|| verified.result.currency !== expected.currency
|
|
1950
|
+
|| verified.result.destination_id !== expected.destination_id) {
|
|
1951
|
+
return { error: verified.error ?? 'provider_release_statement_invalid' };
|
|
1952
|
+
}
|
|
1953
|
+
return {
|
|
1954
|
+
artifact: verified.artifact,
|
|
1955
|
+
verification: boundVerificationSummary(verified.result, expected, {
|
|
1956
|
+
authenticated: true,
|
|
1957
|
+
provider_id: expected.provider_id,
|
|
1958
|
+
provider_idempotency_key: expected.provider_idempotency_key,
|
|
1959
|
+
provider_request_digest: expected.provider_request_digest,
|
|
1960
|
+
provider_transaction_id: expected.provider_transaction_id,
|
|
1961
|
+
provider_milestone_id: expected.provider_milestone_id,
|
|
1962
|
+
amount: expected.amount,
|
|
1963
|
+
currency: expected.currency,
|
|
1964
|
+
destination_id: expected.destination_id,
|
|
1965
|
+
statement_type: 'release',
|
|
1966
|
+
status: verified.result.status,
|
|
1967
|
+
statement_digest: verified.result.statement_digest,
|
|
1968
|
+
}),
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
function updateReleaseOperation(next, idempotencyKey, code, state) {
|
|
1972
|
+
const entry = next.operations.find((operation) => operation.operation === 'release'
|
|
1973
|
+
&& operation.idempotency_key === idempotencyKey);
|
|
1974
|
+
if (entry) {
|
|
1975
|
+
entry.code = code;
|
|
1976
|
+
entry.ok = state === 'released';
|
|
1977
|
+
entry.outcome = state === 'released'
|
|
1978
|
+
? 'applied'
|
|
1979
|
+
: state === 'release_indeterminate'
|
|
1980
|
+
? 'indeterminate'
|
|
1981
|
+
: 'refused';
|
|
1982
|
+
entry.state = state;
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
function internalReleaseTransition(record, targetState, code, at, providerResult = null) {
|
|
1986
|
+
const next = canonicalSnapshot(record);
|
|
1987
|
+
const from = next.state;
|
|
1988
|
+
next.state = targetState;
|
|
1989
|
+
next.revision = record.revision + 1;
|
|
1990
|
+
next.updated_at = at;
|
|
1991
|
+
next.release.status = targetState === 'released'
|
|
1992
|
+
? 'released'
|
|
1993
|
+
: targetState === 'milestone_submitted'
|
|
1994
|
+
? 'not_released'
|
|
1995
|
+
: 'indeterminate';
|
|
1996
|
+
if (providerResult) {
|
|
1997
|
+
next.release.reconciled_at = at;
|
|
1998
|
+
next.release.provider_statement = providerResult.artifact;
|
|
1999
|
+
next.release.provider_verification = providerResult.verification;
|
|
2000
|
+
}
|
|
2001
|
+
updateReleaseOperation(next, next.release.operation_idempotency_key, code, targetState);
|
|
2002
|
+
if (from !== targetState) {
|
|
2003
|
+
if (next.history.length >= MAX_HISTORY)
|
|
2004
|
+
return { error: 'state_history_limit_reached' };
|
|
2005
|
+
next.history.push({
|
|
2006
|
+
from,
|
|
2007
|
+
to: targetState,
|
|
2008
|
+
operation: 'release',
|
|
2009
|
+
idempotency_key: next.release.operation_idempotency_key,
|
|
2010
|
+
at,
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
return { next };
|
|
2014
|
+
}
|
|
2015
|
+
/**
|
|
2016
|
+
* @param {Record<string, any>} record
|
|
2017
|
+
*/
|
|
2018
|
+
async function readLatestRelease(record) {
|
|
2019
|
+
const loaded = await readRecord(record.escrow_key);
|
|
2020
|
+
return loaded.error ? { error: loaded.error } : { record: loaded.record };
|
|
2021
|
+
}
|
|
2022
|
+
/**
|
|
2023
|
+
* @param {Record<string, any>} record
|
|
2024
|
+
* @param {string} operation
|
|
2025
|
+
* @param {string | undefined} code
|
|
2026
|
+
* @param {{ artifact: any, verification: any } | null} providerResult
|
|
2027
|
+
*/
|
|
2028
|
+
async function freezeIndeterminate(record, operation, code, providerResult = null) {
|
|
2029
|
+
let current = record;
|
|
2030
|
+
for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
|
|
2031
|
+
if (current?.state === 'released') {
|
|
2032
|
+
return outcome({
|
|
2033
|
+
ok: true,
|
|
2034
|
+
type: 'reconciled',
|
|
2035
|
+
code: 'release_committed',
|
|
2036
|
+
operation,
|
|
2037
|
+
record: current,
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
if (current?.state === 'release_indeterminate') {
|
|
2041
|
+
return outcome({
|
|
2042
|
+
type: 'indeterminate',
|
|
2043
|
+
code,
|
|
2044
|
+
operation,
|
|
2045
|
+
record: current,
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
if (current?.state !== 'release_reserved') {
|
|
2049
|
+
return outcome({
|
|
2050
|
+
type: 'indeterminate',
|
|
2051
|
+
code,
|
|
2052
|
+
operation,
|
|
2053
|
+
record: current,
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
const operationTime = operationInstant(current);
|
|
2057
|
+
if (operationTime.error) {
|
|
2058
|
+
return outcome({
|
|
2059
|
+
type: 'indeterminate',
|
|
2060
|
+
code: operationTime.error,
|
|
2061
|
+
operation,
|
|
2062
|
+
record: current,
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
const { at } = operationTime;
|
|
2066
|
+
const transition = internalReleaseTransition(current, 'release_indeterminate', code, at, providerResult);
|
|
2067
|
+
if (transition.error) {
|
|
2068
|
+
return outcome({
|
|
2069
|
+
type: 'indeterminate',
|
|
2070
|
+
code: transition.error,
|
|
2071
|
+
operation,
|
|
2072
|
+
record: current,
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2075
|
+
const written = await writeRecord(current.escrow_key, current.revision, transition.next);
|
|
2076
|
+
if (written.applied) {
|
|
2077
|
+
return outcome({
|
|
2078
|
+
type: 'indeterminate',
|
|
2079
|
+
code,
|
|
2080
|
+
operation,
|
|
2081
|
+
record: transition.next,
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
2084
|
+
const latest = await readLatestRelease(current);
|
|
2085
|
+
if (latest.error) {
|
|
2086
|
+
return outcome({
|
|
2087
|
+
type: 'indeterminate',
|
|
2088
|
+
code,
|
|
2089
|
+
operation,
|
|
2090
|
+
record: current,
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
current = latest.record;
|
|
2094
|
+
}
|
|
2095
|
+
return outcome({
|
|
2096
|
+
type: 'indeterminate',
|
|
2097
|
+
code,
|
|
2098
|
+
operation,
|
|
2099
|
+
record: current,
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
/**
|
|
2103
|
+
* @param {Record<string, any>} record
|
|
2104
|
+
* @param {{ artifact: any, verification: any }} providerResult
|
|
2105
|
+
* @param {string} operation
|
|
2106
|
+
*/
|
|
2107
|
+
async function commitReleaseResult(record, providerResult, operation) {
|
|
2108
|
+
const status = providerResult.verification.status;
|
|
2109
|
+
const operationTime = operationInstant(record);
|
|
2110
|
+
if (operationTime.error) {
|
|
2111
|
+
return freezeIndeterminate(record, operation, operationTime.error, providerResult);
|
|
2112
|
+
}
|
|
2113
|
+
const { at } = operationTime;
|
|
2114
|
+
const targetState = status === 'released'
|
|
2115
|
+
? 'released'
|
|
2116
|
+
: status === 'not_released'
|
|
2117
|
+
? 'milestone_submitted'
|
|
2118
|
+
: 'release_indeterminate';
|
|
2119
|
+
const code = status === 'released'
|
|
2120
|
+
? 'release_committed'
|
|
2121
|
+
: status === 'not_released'
|
|
2122
|
+
? 'provider_release_not_released'
|
|
2123
|
+
: 'release_effect_indeterminate';
|
|
2124
|
+
const transition = internalReleaseTransition(record, targetState, code, at, providerResult);
|
|
2125
|
+
if (transition.error) {
|
|
2126
|
+
return freezeIndeterminate(record, operation, 'release_commit_indeterminate', providerResult);
|
|
2127
|
+
}
|
|
2128
|
+
const written = await writeRecord(record.escrow_key, record.revision, transition.next);
|
|
2129
|
+
if (written.applied) {
|
|
2130
|
+
return outcome({
|
|
2131
|
+
ok: status === 'released',
|
|
2132
|
+
type: status === 'released' ? 'applied' : status === 'pending' ? 'indeterminate' : 'refused',
|
|
2133
|
+
code,
|
|
2134
|
+
operation,
|
|
2135
|
+
record: transition.next,
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
const latest = await readLatestRelease(record);
|
|
2139
|
+
if (!latest.error
|
|
2140
|
+
&& latest.record?.state === 'released'
|
|
2141
|
+
&& latest.record.release?.release_key === record.release.release_key) {
|
|
2142
|
+
return outcome({
|
|
2143
|
+
ok: true,
|
|
2144
|
+
type: 'idempotent',
|
|
2145
|
+
code: 'release_committed',
|
|
2146
|
+
operation,
|
|
2147
|
+
record: latest.record,
|
|
2148
|
+
});
|
|
2149
|
+
}
|
|
2150
|
+
return freezeIndeterminate(latest.record ?? record, operation, 'release_commit_indeterminate', providerResult);
|
|
2151
|
+
}
|
|
2152
|
+
async function release(input = {}) {
|
|
2153
|
+
const operation = 'release';
|
|
2154
|
+
return safe(operation, async () => {
|
|
2155
|
+
const normalized = normalizedInput(operation, input);
|
|
2156
|
+
if (normalized.error)
|
|
2157
|
+
return outcome({ code: normalized.error, operation });
|
|
2158
|
+
const config = configurationRefusal(operation);
|
|
2159
|
+
if (config)
|
|
2160
|
+
return config;
|
|
2161
|
+
let reserved = null;
|
|
2162
|
+
for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
|
|
2163
|
+
const loaded = await readRecord(normalized.escrowKey);
|
|
2164
|
+
if (loaded.error)
|
|
2165
|
+
return outcome({ code: loaded.error, operation });
|
|
2166
|
+
if (!loaded.record)
|
|
2167
|
+
return outcome({ code: 'escrow_not_found', operation });
|
|
2168
|
+
const record = loaded.record;
|
|
2169
|
+
if (!operationBindingMatches(record, normalized)) {
|
|
2170
|
+
return outcome({ code: 'operation_binding_mismatch', operation, record });
|
|
2171
|
+
}
|
|
2172
|
+
const repeated = idempotentResult(record, normalized, operation);
|
|
2173
|
+
if (repeated)
|
|
2174
|
+
return repeated;
|
|
2175
|
+
if (['released', 'completed'].includes(record.state)) {
|
|
2176
|
+
return outcome({ code: 'release_already_applied', operation, record });
|
|
2177
|
+
}
|
|
2178
|
+
if (['release_reserved', 'release_indeterminate'].includes(record.state)) {
|
|
2179
|
+
return outcome({
|
|
2180
|
+
code: 'release_reconciliation_required',
|
|
2181
|
+
operation,
|
|
2182
|
+
record,
|
|
2183
|
+
type: 'indeterminate',
|
|
2184
|
+
});
|
|
2185
|
+
}
|
|
2186
|
+
if (record.state !== 'milestone_submitted') {
|
|
2187
|
+
return outcome({ code: 'invalid_state_transition', operation, record });
|
|
2188
|
+
}
|
|
2189
|
+
const operationTime = operationInstant(record);
|
|
2190
|
+
if (operationTime.error) {
|
|
2191
|
+
return outcome({ code: operationTime.error, operation, record });
|
|
2192
|
+
}
|
|
2193
|
+
const { at } = operationTime;
|
|
2194
|
+
const precondition = await releasePreconditions(record, at);
|
|
2195
|
+
if (precondition) {
|
|
2196
|
+
return outcome({
|
|
2197
|
+
code: precondition.code,
|
|
2198
|
+
operation,
|
|
2199
|
+
record,
|
|
2200
|
+
details: precondition.details ?? null,
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
const request = providerRequestFor(record);
|
|
2204
|
+
const finalized = finalizeMutation(record, normalized, operation, 'release_reserved', at, (next) => {
|
|
2205
|
+
next.state = 'release_reserved';
|
|
2206
|
+
next.release = {
|
|
2207
|
+
release_key: request.release_key,
|
|
2208
|
+
provider_idempotency_key: request.idempotency_key,
|
|
2209
|
+
operation_idempotency_key: normalized.snapshot.idempotency_key,
|
|
2210
|
+
status: 'reserved',
|
|
2211
|
+
reserved_at: at,
|
|
2212
|
+
reconciled_at: null,
|
|
2213
|
+
provider_request: request,
|
|
2214
|
+
provider_statement: null,
|
|
2215
|
+
provider_verification: null,
|
|
2216
|
+
};
|
|
2217
|
+
}, { ok: false, type: 'reserved' });
|
|
2218
|
+
if (finalized.error) {
|
|
2219
|
+
return outcome({ code: finalized.error, operation, record });
|
|
2220
|
+
}
|
|
2221
|
+
const written = await writeRecord(normalized.escrowKey, record.revision, finalized.next);
|
|
2222
|
+
if (written.error)
|
|
2223
|
+
return outcome({ code: written.error, operation, record });
|
|
2224
|
+
if (written.applied) {
|
|
2225
|
+
reserved = deepFreeze(finalized.next);
|
|
2226
|
+
break;
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
if (!reserved)
|
|
2230
|
+
return outcome({ code: 'store_conflict', operation });
|
|
2231
|
+
try {
|
|
2232
|
+
// reserved was just checked non-null above; releaseProvider is
|
|
2233
|
+
// always assigned once configurationError is null (see the provider
|
|
2234
|
+
// validation above). Both checks precede this closure, which (being
|
|
2235
|
+
// a plain arrow function, not a hoisted declaration) still needs
|
|
2236
|
+
// explicit assertions since `reserved` is a mutable `let`.
|
|
2237
|
+
await withProviderTimeout(() => releaseProvider(deepFreeze(canonicalSnapshot(reserved.release.provider_request))));
|
|
2238
|
+
}
|
|
2239
|
+
catch {
|
|
2240
|
+
return freezeIndeterminate(reserved, operation, 'release_effect_indeterminate');
|
|
2241
|
+
}
|
|
2242
|
+
const reconciled = await authoritativeProviderRelease(reserved);
|
|
2243
|
+
if (reconciled.error) {
|
|
2244
|
+
return freezeIndeterminate(reserved, operation, 'release_effect_indeterminate');
|
|
2245
|
+
}
|
|
2246
|
+
return commitReleaseResult(reserved,
|
|
2247
|
+
/** @type {{artifact: any, verification: any}} */ (reconciled), operation);
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
async function reconcileRelease(input = {}) {
|
|
2251
|
+
const operation = 'reconcile_release';
|
|
2252
|
+
return safe(operation, async () => {
|
|
2253
|
+
const normalized = normalizedInput(operation, input);
|
|
2254
|
+
if (normalized.error)
|
|
2255
|
+
return outcome({ code: normalized.error, operation });
|
|
2256
|
+
const config = configurationRefusal(operation);
|
|
2257
|
+
if (config)
|
|
2258
|
+
return config;
|
|
2259
|
+
for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
|
|
2260
|
+
const loaded = await readRecord(normalized.escrowKey);
|
|
2261
|
+
if (loaded.error)
|
|
2262
|
+
return outcome({ code: loaded.error, operation });
|
|
2263
|
+
if (!loaded.record)
|
|
2264
|
+
return outcome({ code: 'escrow_not_found', operation });
|
|
2265
|
+
const record = loaded.record;
|
|
2266
|
+
if (!operationBindingMatches(record, normalized)) {
|
|
2267
|
+
return outcome({ code: 'operation_binding_mismatch', operation, record });
|
|
2268
|
+
}
|
|
2269
|
+
const repeated = idempotentResult(record, normalized, operation);
|
|
2270
|
+
if (repeated)
|
|
2271
|
+
return repeated;
|
|
2272
|
+
if (record.state === 'released') {
|
|
2273
|
+
return outcome({
|
|
2274
|
+
ok: true,
|
|
2275
|
+
type: 'idempotent',
|
|
2276
|
+
code: 'release_already_applied',
|
|
2277
|
+
operation,
|
|
2278
|
+
record,
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2281
|
+
if (!['release_reserved', 'release_indeterminate'].includes(record.state)) {
|
|
2282
|
+
return outcome({ code: 'invalid_state_transition', operation, record });
|
|
2283
|
+
}
|
|
2284
|
+
if (record.release?.provider_idempotency_key !== providerIdempotencyKey(record)
|
|
2285
|
+
|| record.release?.release_key !== releaseReservationKey(record)) {
|
|
2286
|
+
return outcome({ code: 'release_binding_corrupt', operation, record });
|
|
2287
|
+
}
|
|
2288
|
+
const providerResult = await authoritativeProviderRelease(record);
|
|
2289
|
+
if (providerResult.error) {
|
|
2290
|
+
return outcome({
|
|
2291
|
+
code: providerResult.error,
|
|
2292
|
+
operation,
|
|
2293
|
+
record,
|
|
2294
|
+
type: 'indeterminate',
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
const operationTime = operationInstant(record);
|
|
2298
|
+
if (operationTime.error) {
|
|
2299
|
+
return outcome({
|
|
2300
|
+
code: operationTime.error,
|
|
2301
|
+
operation,
|
|
2302
|
+
record,
|
|
2303
|
+
type: 'indeterminate',
|
|
2304
|
+
});
|
|
2305
|
+
}
|
|
2306
|
+
const { at } = operationTime;
|
|
2307
|
+
const providerVerification = providerResult.verification;
|
|
2308
|
+
const status = providerVerification.status;
|
|
2309
|
+
const targetState = status === 'released'
|
|
2310
|
+
? 'released'
|
|
2311
|
+
: status === 'not_released'
|
|
2312
|
+
? record.pending_amendment ? 'amendment_pending' : 'milestone_submitted'
|
|
2313
|
+
: 'release_indeterminate';
|
|
2314
|
+
const code = status === 'released'
|
|
2315
|
+
? 'release_reconciled_released'
|
|
2316
|
+
: status === 'not_released'
|
|
2317
|
+
? 'release_reconciled_not_released'
|
|
2318
|
+
: 'release_still_indeterminate';
|
|
2319
|
+
const draft = canonicalSnapshot(record);
|
|
2320
|
+
draft.state = targetState;
|
|
2321
|
+
draft.release.status = status === 'pending' ? 'indeterminate' : status;
|
|
2322
|
+
draft.release.reconciled_at = at;
|
|
2323
|
+
draft.release.provider_statement = providerResult.artifact;
|
|
2324
|
+
draft.release.provider_verification = providerVerification;
|
|
2325
|
+
if (status === 'not_released' && draft.pending_amendment) {
|
|
2326
|
+
draft.funding = null;
|
|
2327
|
+
draft.milestone_evidence = null;
|
|
2328
|
+
draft.release_approvals = [];
|
|
2329
|
+
}
|
|
2330
|
+
updateReleaseOperation(draft, draft.release.operation_idempotency_key, code, targetState);
|
|
2331
|
+
const finalized = finalizeMutation(record, normalized, operation, code, at, (next) => Object.assign(next, draft), {
|
|
2332
|
+
ok: status !== 'pending',
|
|
2333
|
+
type: status === 'pending' ? 'indeterminate' : 'reconciled',
|
|
2334
|
+
});
|
|
2335
|
+
if (finalized.error) {
|
|
2336
|
+
return outcome({
|
|
2337
|
+
code: finalized.error,
|
|
2338
|
+
operation,
|
|
2339
|
+
record,
|
|
2340
|
+
type: 'indeterminate',
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
const written = await writeRecord(normalized.escrowKey, record.revision, finalized.next);
|
|
2344
|
+
if (written.error) {
|
|
2345
|
+
return outcome({
|
|
2346
|
+
code: written.error,
|
|
2347
|
+
operation,
|
|
2348
|
+
record,
|
|
2349
|
+
type: 'indeterminate',
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
if (written.applied) {
|
|
2353
|
+
return outcome({
|
|
2354
|
+
ok: status !== 'pending',
|
|
2355
|
+
type: status === 'pending' ? 'indeterminate' : 'reconciled',
|
|
2356
|
+
code,
|
|
2357
|
+
operation,
|
|
2358
|
+
record: finalized.next,
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
return outcome({
|
|
2363
|
+
code: 'store_conflict',
|
|
2364
|
+
operation,
|
|
2365
|
+
type: 'indeterminate',
|
|
2366
|
+
});
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
async function openDispute(input = {}) {
|
|
2370
|
+
return mutate('open_dispute', input, {
|
|
2371
|
+
extraAllowed: ['party_id', 'reason', 'command_authorization'],
|
|
2372
|
+
async transform(draft, normalized, at) {
|
|
2373
|
+
if (!['funded', 'milestone_submitted'].includes(draft.state)) {
|
|
2374
|
+
return { refusal: 'invalid_state_transition' };
|
|
2375
|
+
}
|
|
2376
|
+
const partyId = normalized.snapshot.party_id;
|
|
2377
|
+
if (!draft.parties.some(
|
|
2378
|
+
/** @param {{ party_id: string, role: string }} party */
|
|
2379
|
+
(party) => party.party_id === partyId)
|
|
2380
|
+
|| !validString(normalized.snapshot.reason, 2048)) {
|
|
2381
|
+
return { refusal: 'dispute_input_invalid' };
|
|
2382
|
+
}
|
|
2383
|
+
const authorization = await verifyCommandAuthorization(normalized.snapshot.command_authorization, draft, 'open_dispute', partyId, { reason: normalized.snapshot.reason });
|
|
2384
|
+
if (authorization.error)
|
|
2385
|
+
return { refusal: authorization.error };
|
|
2386
|
+
draft.dispute = {
|
|
2387
|
+
party_id: partyId,
|
|
2388
|
+
reason: normalized.snapshot.reason,
|
|
2389
|
+
authorization,
|
|
2390
|
+
opened_at: at,
|
|
2391
|
+
};
|
|
2392
|
+
draft.state = 'disputed';
|
|
2393
|
+
return { code: 'dispute_opened' };
|
|
2394
|
+
},
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2397
|
+
async function proposeAmendment(input = {}) {
|
|
2398
|
+
return mutate('propose_amendment', input, {
|
|
2399
|
+
extraAllowed: [
|
|
2400
|
+
'party_id',
|
|
2401
|
+
'command_authorization',
|
|
2402
|
+
'next_document_action_binding_digest',
|
|
2403
|
+
'next_release_action_digest',
|
|
2404
|
+
'next_document_action_binding',
|
|
2405
|
+
],
|
|
2406
|
+
async transform(draft, normalized, at) {
|
|
2407
|
+
if (draft.state === 'awaiting_funding') {
|
|
2408
|
+
return { refusal: 'amendment_requires_funding_reconciliation' };
|
|
2409
|
+
}
|
|
2410
|
+
if (draft.funding !== null
|
|
2411
|
+
|| draft.release !== null
|
|
2412
|
+
|| ['funded', 'milestone_submitted', 'disputed'].includes(draft.state)) {
|
|
2413
|
+
return { refusal: 'amendment_requires_custodian_unwind' };
|
|
2414
|
+
}
|
|
2415
|
+
if (draft.state !== 'effective'
|
|
2416
|
+
|| draft.pending_amendment) {
|
|
2417
|
+
return { refusal: 'invalid_state_transition' };
|
|
2418
|
+
}
|
|
2419
|
+
const nextBindingDigest = normalized.snapshot.next_document_action_binding_digest;
|
|
2420
|
+
const nextActionDigest = normalized.snapshot.next_release_action_digest;
|
|
2421
|
+
if (!validDigest(nextBindingDigest)
|
|
2422
|
+
|| !validDigest(nextActionDigest)
|
|
2423
|
+
|| nextBindingDigest === draft.document_action_binding_digest) {
|
|
2424
|
+
return { refusal: 'amendment_binding_invalid' };
|
|
2425
|
+
}
|
|
2426
|
+
const partyId = normalized.snapshot.party_id;
|
|
2427
|
+
const authorization = await verifyCommandAuthorization(normalized.snapshot.command_authorization, draft, 'propose_amendment', partyId, {
|
|
2428
|
+
next_document_action_binding_digest: nextBindingDigest,
|
|
2429
|
+
next_release_action_digest: nextActionDigest,
|
|
2430
|
+
});
|
|
2431
|
+
if (authorization.error)
|
|
2432
|
+
return { refusal: authorization.error };
|
|
2433
|
+
const nextContext = {
|
|
2434
|
+
...normalized.context,
|
|
2435
|
+
document_action_binding_digest: nextBindingDigest,
|
|
2436
|
+
release_action_digest: nextActionDigest,
|
|
2437
|
+
supersedes_document_action_binding_digest: draft.document_action_binding_digest,
|
|
2438
|
+
};
|
|
2439
|
+
const verified = await invokeVerifier(verifyDocumentActionBinding, normalized.snapshot.next_document_action_binding, nextContext);
|
|
2440
|
+
const bindingDetails = verified.error
|
|
2441
|
+
? null
|
|
2442
|
+
: bindingVerificationDetails(verified.result, nextContext);
|
|
2443
|
+
if (!bindingDetails
|
|
2444
|
+
|| verified.result.supersedes_document_action_binding_digest
|
|
2445
|
+
!== draft.document_action_binding_digest) {
|
|
2446
|
+
return { refusal: verified.error ?? 'amendment_document_binding_invalid' };
|
|
2447
|
+
}
|
|
2448
|
+
draft.pending_amendment = {
|
|
2449
|
+
from_state: draft.state,
|
|
2450
|
+
document_action_binding_digest: nextBindingDigest,
|
|
2451
|
+
release_action_digest: nextActionDigest,
|
|
2452
|
+
document_action_binding: {
|
|
2453
|
+
artifact: verified.artifact,
|
|
2454
|
+
verification: boundVerificationSummary(verified.result, nextContext, {
|
|
2455
|
+
...bindingDetails,
|
|
2456
|
+
supersedes_document_action_binding_digest: draft.document_action_binding_digest,
|
|
2457
|
+
}),
|
|
2458
|
+
},
|
|
2459
|
+
agreement_acceptances: [],
|
|
2460
|
+
proposer_party_id: partyId,
|
|
2461
|
+
proposal_authorization: authorization,
|
|
2462
|
+
proposed_at: at,
|
|
2463
|
+
};
|
|
2464
|
+
draft.funding = null;
|
|
2465
|
+
draft.milestone_evidence = null;
|
|
2466
|
+
draft.release_approvals = [];
|
|
2467
|
+
draft.release = null;
|
|
2468
|
+
draft.dispute = null;
|
|
2469
|
+
draft.cancellation = null;
|
|
2470
|
+
draft.state = 'amendment_pending';
|
|
2471
|
+
return { code: 'amendment_pending' };
|
|
2472
|
+
},
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
async function acceptAmendment(input = {}) {
|
|
2476
|
+
return mutate('accept_amendment', input, {
|
|
2477
|
+
extraAllowed: ['party_id', 'agreement_acceptance'],
|
|
2478
|
+
bindingMode: 'pending',
|
|
2479
|
+
async transform(draft, normalized, at) {
|
|
2480
|
+
if (draft.state !== 'amendment_pending' || !draft.pending_amendment) {
|
|
2481
|
+
return { refusal: 'invalid_state_transition' };
|
|
2482
|
+
}
|
|
2483
|
+
const partyId = normalized.snapshot.party_id;
|
|
2484
|
+
if (!draft.profile.required_acceptance_party_ids.includes(partyId)) {
|
|
2485
|
+
return { refusal: 'acceptance_party_not_required' };
|
|
2486
|
+
}
|
|
2487
|
+
if (draft.pending_amendment.agreement_acceptances
|
|
2488
|
+
.some((entry) => entry.party_id === partyId)) {
|
|
2489
|
+
return { refusal: 'amendment_already_accepted' };
|
|
2490
|
+
}
|
|
2491
|
+
const expected = {
|
|
2492
|
+
...normalized.context,
|
|
2493
|
+
party_id: partyId,
|
|
2494
|
+
};
|
|
2495
|
+
const verified = await invokeVerifier(verifyAgreementAcceptance, normalized.snapshot.agreement_acceptance, expected);
|
|
2496
|
+
if (verified.error
|
|
2497
|
+
|| !boundVerificationMatches(verified.result, expectedBindings(expected))
|
|
2498
|
+
|| verified.result.party_id !== partyId
|
|
2499
|
+
|| !validString(verified.result.principal_key_id, 512)
|
|
2500
|
+
|| draft.pending_amendment.agreement_acceptances.some((entry) => entry.verification?.principal_key_id
|
|
2501
|
+
=== verified.result.principal_key_id)
|
|
2502
|
+
|| !validDigest(verified.result.acceptance_digest)) {
|
|
2503
|
+
return { refusal: verified.error ?? 'amendment_acceptance_invalid' };
|
|
2504
|
+
}
|
|
2505
|
+
draft.pending_amendment.agreement_acceptances.push({
|
|
2506
|
+
party_id: partyId,
|
|
2507
|
+
artifact: verified.artifact,
|
|
2508
|
+
verification: boundVerificationSummary(verified.result, expected, {
|
|
2509
|
+
party_id: partyId,
|
|
2510
|
+
principal_key_id: verified.result.principal_key_id,
|
|
2511
|
+
acceptance_digest: verified.result.acceptance_digest,
|
|
2512
|
+
}),
|
|
2513
|
+
});
|
|
2514
|
+
const accepted = new Set(draft.pending_amendment.agreement_acceptances
|
|
2515
|
+
.map((entry) => entry.party_id));
|
|
2516
|
+
if (!draft.profile.required_acceptance_party_ids
|
|
2517
|
+
.every((id) => accepted.has(id))) {
|
|
2518
|
+
return { code: 'amendment_acceptance_recorded' };
|
|
2519
|
+
}
|
|
2520
|
+
if (draft.superseded_bindings.length >= MAX_SUPERSEDED_BINDINGS) {
|
|
2521
|
+
return { refusal: 'supersession_history_limit_reached' };
|
|
2522
|
+
}
|
|
2523
|
+
const pending = draft.pending_amendment;
|
|
2524
|
+
draft.superseded_bindings.push({
|
|
2525
|
+
document_action_binding_digest: draft.document_action_binding_digest,
|
|
2526
|
+
release_action_digest: draft.release_action_digest,
|
|
2527
|
+
superseded_by_binding_digest: pending.document_action_binding_digest,
|
|
2528
|
+
superseded_at: at,
|
|
2529
|
+
});
|
|
2530
|
+
draft.document_action_binding_digest =
|
|
2531
|
+
pending.document_action_binding_digest;
|
|
2532
|
+
draft.release_action_digest = pending.release_action_digest;
|
|
2533
|
+
draft.document_action_binding = pending.document_action_binding;
|
|
2534
|
+
draft.agreement_acceptances = pending.agreement_acceptances;
|
|
2535
|
+
draft.funding = null;
|
|
2536
|
+
draft.milestone_evidence = null;
|
|
2537
|
+
draft.release_approvals = [];
|
|
2538
|
+
draft.release = null;
|
|
2539
|
+
draft.dispute = null;
|
|
2540
|
+
draft.cancellation = null;
|
|
2541
|
+
draft.pending_amendment = null;
|
|
2542
|
+
draft.state = 'effective';
|
|
2543
|
+
return { code: 'amendment_effective' };
|
|
2544
|
+
},
|
|
2545
|
+
});
|
|
2546
|
+
}
|
|
2547
|
+
async function cancel(input = {}) {
|
|
2548
|
+
return mutate('cancel', input, {
|
|
2549
|
+
extraAllowed: ['party_id', 'reason', 'command_authorization'],
|
|
2550
|
+
extraRequired: ['party_id', 'command_authorization'],
|
|
2551
|
+
async transform(draft, normalized, at) {
|
|
2552
|
+
if (draft.state === 'awaiting_funding') {
|
|
2553
|
+
return { refusal: 'cancellation_requires_funding_reconciliation' };
|
|
2554
|
+
}
|
|
2555
|
+
if (draft.funding !== null || draft.release !== null) {
|
|
2556
|
+
return { refusal: 'cancellation_requires_custodian_unwind' };
|
|
2557
|
+
}
|
|
2558
|
+
if (![
|
|
2559
|
+
'draft',
|
|
2560
|
+
'awaiting_acceptance',
|
|
2561
|
+
'effective',
|
|
2562
|
+
'amendment_pending',
|
|
2563
|
+
].includes(draft.state)) {
|
|
2564
|
+
return { refusal: 'invalid_state_transition' };
|
|
2565
|
+
}
|
|
2566
|
+
if (normalized.snapshot.reason !== undefined
|
|
2567
|
+
&& !validString(normalized.snapshot.reason, 2048)) {
|
|
2568
|
+
return { refusal: 'cancellation_reason_invalid' };
|
|
2569
|
+
}
|
|
2570
|
+
const authorization = await verifyCommandAuthorization(normalized.snapshot.command_authorization, draft, 'cancel', normalized.snapshot.party_id, { reason: normalized.snapshot.reason ?? null });
|
|
2571
|
+
if (authorization.error)
|
|
2572
|
+
return { refusal: authorization.error };
|
|
2573
|
+
draft.state = 'cancelled';
|
|
2574
|
+
draft.cancellation = {
|
|
2575
|
+
party_id: normalized.snapshot.party_id,
|
|
2576
|
+
reason: normalized.snapshot.reason ?? null,
|
|
2577
|
+
authorization,
|
|
2578
|
+
cancelled_at: at,
|
|
2579
|
+
};
|
|
2580
|
+
return { code: 'escrow_cancelled' };
|
|
2581
|
+
},
|
|
2582
|
+
});
|
|
2583
|
+
}
|
|
2584
|
+
async function complete(input = {}) {
|
|
2585
|
+
return mutate('complete', input, {
|
|
2586
|
+
extraAllowed: ['party_id', 'command_authorization'],
|
|
2587
|
+
extraRequired: ['party_id', 'command_authorization'],
|
|
2588
|
+
async transform(draft, normalized, at) {
|
|
2589
|
+
if (draft.state !== 'released') {
|
|
2590
|
+
return { refusal: 'invalid_state_transition' };
|
|
2591
|
+
}
|
|
2592
|
+
const authorization = await verifyCommandAuthorization(normalized.snapshot.command_authorization, draft, 'complete', normalized.snapshot.party_id, { meaning: 'administrative_archive_only' });
|
|
2593
|
+
if (authorization.error)
|
|
2594
|
+
return { refusal: authorization.error };
|
|
2595
|
+
draft.state = 'completed';
|
|
2596
|
+
draft.completion = {
|
|
2597
|
+
party_id: normalized.snapshot.party_id,
|
|
2598
|
+
meaning: 'administrative_archive_only',
|
|
2599
|
+
authorization,
|
|
2600
|
+
completed_at: at,
|
|
2601
|
+
};
|
|
2602
|
+
return { code: 'escrow_completed' };
|
|
2603
|
+
},
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
const methods = {
|
|
2607
|
+
create,
|
|
2608
|
+
beginAcceptance,
|
|
2609
|
+
acceptAgreement,
|
|
2610
|
+
requestFunding,
|
|
2611
|
+
recordFunding,
|
|
2612
|
+
submitMilestone,
|
|
2613
|
+
approveRelease,
|
|
2614
|
+
release,
|
|
2615
|
+
reconcileRelease,
|
|
2616
|
+
openDispute,
|
|
2617
|
+
proposeAmendment,
|
|
2618
|
+
acceptAmendment,
|
|
2619
|
+
cancel,
|
|
2620
|
+
complete,
|
|
2621
|
+
};
|
|
2622
|
+
async function apply(operation, input = {}) {
|
|
2623
|
+
try {
|
|
2624
|
+
const selected = methods[operation];
|
|
2625
|
+
if (typeof selected !== 'function') {
|
|
2626
|
+
return outcome({
|
|
2627
|
+
code: 'unknown_operation',
|
|
2628
|
+
operation: validString(operation, 128) ? operation : null,
|
|
2629
|
+
});
|
|
2630
|
+
}
|
|
2631
|
+
return selected(input);
|
|
2632
|
+
}
|
|
2633
|
+
catch {
|
|
2634
|
+
return outcome({
|
|
2635
|
+
code: 'invalid_operation_input',
|
|
2636
|
+
operation: null,
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
return Object.freeze({
|
|
2641
|
+
ready: configurationError === null,
|
|
2642
|
+
configuration: Object.freeze({
|
|
2643
|
+
ok: configurationError === null,
|
|
2644
|
+
reason: configurationError,
|
|
2645
|
+
}),
|
|
2646
|
+
apply,
|
|
2647
|
+
...methods,
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
export default Object.freeze({
|
|
2651
|
+
ACTION_ESCROW_STATE_VERSION,
|
|
2652
|
+
ACTION_ESCROW_OUTCOME_VERSION,
|
|
2653
|
+
ACTION_ESCROW_PROFILE_VERSION,
|
|
2654
|
+
ACTION_ESCROW_STATES,
|
|
2655
|
+
ACTION_ESCROW_TRANSITIONS,
|
|
2656
|
+
createActionEscrowReleaseBindingMoment,
|
|
2657
|
+
computeActionEscrowReleaseBindingMomentDigest,
|
|
2658
|
+
computeActionEscrowResolutionNonce,
|
|
2659
|
+
createActionEscrowKernel,
|
|
2660
|
+
});
|
|
2661
|
+
//# sourceMappingURL=action-escrow.js.map
|