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