@emilia-protocol/gate 0.11.0 → 0.14.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 +80 -0
- package/README.md +310 -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 +1003 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1737 -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/proposal-to-effect.d.ts +146 -0
- package/dist/proposal-to-effect.d.ts.map +1 -0
- package/dist/proposal-to-effect.js +427 -0
- package/dist/proposal-to-effect.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 +35 -4
- package/proposal-to-effect.js +4 -0
- 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 +2127 -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/proposal-to-effect.ts +571 -0
- package/src/protocol-state.ts +143 -0
- package/src/protocol-state.typecheck.ts +94 -0
- package/src/receipt-program.ts +1105 -0
- package/src/reliance-kernel.ts +119 -0
- package/src/reliance-packet.ts +206 -0
- package/src/remedy-program-postgres.ts +367 -0
- package/src/remedy-program-receipt.ts +891 -0
- package/src/remedy-program.ts +1388 -0
- package/src/reports/art14.ts +361 -0
- package/src/reports/assurance-package.ts +330 -0
- package/src/reports/auditor-workpaper.ts +558 -0
- package/src/reports/external-verification.ts +232 -0
- package/src/reports/reperform.ts +441 -0
- package/src/reports/underwriter.ts +356 -0
- package/src/retention.ts +100 -0
- package/src/roster.ts +273 -0
- package/src/runtime-monitor.ts +217 -0
- package/src/settlement.ts +313 -0
- package/src/siem.ts +247 -0
- package/src/store-postgres.ts +167 -0
- package/src/store.ts +214 -0
- package/src/strict-json.ts +115 -0
- package/src/trust-program-adapters.ts +847 -0
- package/src/trust-program-postgres.ts +429 -0
- package/src/trust-program-revocation.ts +751 -0
- package/src/trust-program.ts +1460 -0
- package/src/witness-postgres.ts +106 -0
- package/src/zk-range-proof.ts +250 -0
- package/store-postgres.js +3 -161
- package/store.js +4 -198
- package/strict-json.js +3 -102
- package/trust-program-adapters.js +4 -0
- package/trust-program-postgres.js +4 -0
- package/trust-program-revocation.js +4 -0
- package/trust-program.js +4 -0
- package/witness-postgres.js +3 -96
- package/zk-range-proof.js +4 -0
|
@@ -0,0 +1,1346 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* EMILIA Gate Remedy Program Profile v1.
|
|
5
|
+
*
|
|
6
|
+
* A fail-closed, post-effect compensation state machine. The kernel never
|
|
7
|
+
* rewrites an already-observed effect: it verifies the original effect,
|
|
8
|
+
* records disputes and late revocations, and authorizes separately bound
|
|
9
|
+
* compensating operations through atomic compare-and-swap transitions.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import { canonicalize } from '../execution-binding.js';
|
|
13
|
+
export const REMEDY_PROGRAM_VERSION = 'EP-GATE-REMEDY-PROGRAM-PROFILE-v1';
|
|
14
|
+
const DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
15
|
+
const CAID = /^caid:1:[a-z][a-z0-9.-]*\.[1-9][0-9]*:jcs-sha256:[A-Za-z0-9_-]{43}$/;
|
|
16
|
+
const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
|
|
17
|
+
const MAX_CONTEXT_BYTES = 512;
|
|
18
|
+
const MAX_REMEDY_ATTEMPTS = 1024;
|
|
19
|
+
const DEFAULT_MAX_DISPUTE_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
20
|
+
const CONFIG_KEYS = new Set([
|
|
21
|
+
'store', 'verifyOriginalEffect', 'verifyRevocation', 'verifyDispute',
|
|
22
|
+
'verifyRemedyAuthorization', 'verifyRemedyOutcome', 'verifyOriginalReconciliation',
|
|
23
|
+
'verifyResolution',
|
|
24
|
+
'now', 'maxDisputeAgeMs', 'allowEphemeralState', 'production',
|
|
25
|
+
]);
|
|
26
|
+
const CREATE_KEYS = new Set([
|
|
27
|
+
'instanceId', 'tenantId', 'environment', 'audience', 'original',
|
|
28
|
+
'remedyProfileDigest', 'destinationBindingDigest', 'maxRemedyUnits',
|
|
29
|
+
'unit', 'evidence',
|
|
30
|
+
]);
|
|
31
|
+
const ORIGINAL_KEYS = new Set([
|
|
32
|
+
'caid', 'action_digest', 'operation_id', 'consequence_mode',
|
|
33
|
+
'consequence_digest', 'terminal_evidence_digest', 'outcome', 'occurred_at',
|
|
34
|
+
]);
|
|
35
|
+
const VERIFIED_ORIGINAL_KEYS = new Set(['ok', ...ORIGINAL_KEYS, 'evidence_digest']);
|
|
36
|
+
const ORIGINAL_RECONCILIATION_INPUT_KEYS = new Set([
|
|
37
|
+
'tenantId', 'instanceId', 'outcome', 'evidence',
|
|
38
|
+
]);
|
|
39
|
+
const ORIGINAL_RECONCILIATION_EVIDENCE_KEYS = new Set([
|
|
40
|
+
'evidence_id', 'evidence_digest', 'observed_at',
|
|
41
|
+
]);
|
|
42
|
+
const VERIFIED_ORIGINAL_RECONCILIATION_KEYS = new Set([
|
|
43
|
+
'ok', ...ORIGINAL_RECONCILIATION_EVIDENCE_KEYS, 'original_operation_id',
|
|
44
|
+
'original_action_digest', 'terminal_evidence_digest', 'outcome',
|
|
45
|
+
]);
|
|
46
|
+
const REVOCATION_INPUT_KEYS = new Set(['tenantId', 'instanceId', 'evidence']);
|
|
47
|
+
const REVOCATION_EVIDENCE_KEYS = new Set(['id', 'digest']);
|
|
48
|
+
const VERIFIED_REVOCATION_KEYS = new Set([
|
|
49
|
+
'ok', 'evidence_id', 'evidence_digest', 'target_operation_id',
|
|
50
|
+
'action_digest', 'authority_id', 'revoked_at',
|
|
51
|
+
]);
|
|
52
|
+
const DISPUTE_INPUT_KEYS = new Set(['tenantId', 'instanceId', 'dispute']);
|
|
53
|
+
const DISPUTE_KEYS = new Set([
|
|
54
|
+
'dispute_id', 'evidence_id', 'evidence_digest', 'challenger_id',
|
|
55
|
+
'requested_units', 'opened_at',
|
|
56
|
+
]);
|
|
57
|
+
const VERIFIED_DISPUTE_KEYS = new Set([
|
|
58
|
+
'ok', ...DISPUTE_KEYS, 'original_operation_id', 'original_action_digest',
|
|
59
|
+
]);
|
|
60
|
+
const AUTHORIZATION_INPUT_KEYS = new Set(['tenantId', 'instanceId', 'authorization']);
|
|
61
|
+
const AUTHORIZATION_KEYS = new Set([
|
|
62
|
+
'evidence_id', 'evidence_digest', 'remedy_operation_id', 'remedy_caid',
|
|
63
|
+
'remedy_action_digest', 'consequence_mode', 'capability_template_digest',
|
|
64
|
+
'escrow_profile_digest', 'units', 'authorized_at',
|
|
65
|
+
]);
|
|
66
|
+
const VERIFIED_AUTHORIZATION_KEYS = new Set([
|
|
67
|
+
'ok', ...AUTHORIZATION_KEYS, 'dispute_id', 'original_operation_id',
|
|
68
|
+
'destination_binding_digest', 'unit',
|
|
69
|
+
]);
|
|
70
|
+
const CLAIM_KEYS = new Set(['tenantId', 'instanceId', 'remedyOperationId', 'claimToken']);
|
|
71
|
+
const FINALIZE_KEYS = new Set([
|
|
72
|
+
'tenantId', 'instanceId', 'remedyOperationId', 'claimToken', 'outcome', 'evidence',
|
|
73
|
+
]);
|
|
74
|
+
const RECONCILE_KEYS = new Set(['tenantId', 'instanceId', 'remedyOperationId', 'outcome', 'evidence']);
|
|
75
|
+
const OUTCOME_EVIDENCE_KEYS = new Set(['evidence_id', 'evidence_digest', 'observed_at']);
|
|
76
|
+
const VERIFIED_OUTCOME_KEYS = new Set([
|
|
77
|
+
'ok', ...OUTCOME_EVIDENCE_KEYS, 'remedy_operation_id',
|
|
78
|
+
'remedy_action_digest', 'destination_binding_digest', 'units', 'unit',
|
|
79
|
+
'outcome',
|
|
80
|
+
]);
|
|
81
|
+
const RESOLUTION_INPUT_KEYS = new Set(['tenantId', 'instanceId', 'resolution']);
|
|
82
|
+
const RESOLUTION_KEYS = new Set([
|
|
83
|
+
'evidence_id', 'evidence_digest', 'outcome', 'resolved_at',
|
|
84
|
+
]);
|
|
85
|
+
const VERIFIED_RESOLUTION_KEYS = new Set(['ok', ...RESOLUTION_KEYS, 'dispute_id']);
|
|
86
|
+
const STATE_KEYS = new Set([
|
|
87
|
+
'version', 'instance_id', 'tenant_id', 'environment', 'audience', 'status',
|
|
88
|
+
'revision', 'created_at', 'updated_at', 'original', 'remedy_profile_digest',
|
|
89
|
+
'destination_binding_digest', 'max_remedy_units', 'unit', 'remedied_units',
|
|
90
|
+
'remaining_units', 'used_evidence_ids', 'used_evidence_digests',
|
|
91
|
+
'original_reconciliation', 'revocation', 'dispute', 'active_remedy',
|
|
92
|
+
'remedies', 'resolution',
|
|
93
|
+
'create_request_digest',
|
|
94
|
+
]);
|
|
95
|
+
const STORED_ORIGINAL_KEYS = new Set([...ORIGINAL_KEYS, 'evidence_digest']);
|
|
96
|
+
const STORED_ORIGINAL_RECONCILIATION_KEYS = new Set([
|
|
97
|
+
...VERIFIED_ORIGINAL_RECONCILIATION_KEYS,
|
|
98
|
+
'request_digest',
|
|
99
|
+
]);
|
|
100
|
+
STORED_ORIGINAL_RECONCILIATION_KEYS.delete('ok');
|
|
101
|
+
const STORED_REVOCATION_KEYS = new Set([
|
|
102
|
+
'evidence_id', 'evidence_digest', 'target_operation_id', 'action_digest',
|
|
103
|
+
'authority_id', 'revoked_at', 'effect', 'request_digest',
|
|
104
|
+
]);
|
|
105
|
+
const STORED_DISPUTE_KEYS = new Set([
|
|
106
|
+
...DISPUTE_KEYS, 'original_operation_id', 'original_action_digest',
|
|
107
|
+
'request_digest',
|
|
108
|
+
]);
|
|
109
|
+
const ATTEMPT_KEYS = new Set([
|
|
110
|
+
'evidence_id', 'evidence_digest', 'dispute_id', 'original_operation_id',
|
|
111
|
+
'remedy_operation_id', 'remedy_caid', 'remedy_action_digest',
|
|
112
|
+
'consequence_mode', 'capability_template_digest', 'escrow_profile_digest',
|
|
113
|
+
'destination_binding_digest', 'units', 'unit', 'authorized_at',
|
|
114
|
+
'request_digest', 'status', 'claim_token_digest', 'claimed_at',
|
|
115
|
+
'claim_request_digest', 'outcome', 'outcome_evidence',
|
|
116
|
+
'finalize_request_digest', 'reconciliation', 'reconcile_request_digest',
|
|
117
|
+
]);
|
|
118
|
+
const STORED_OUTCOME_KEYS = new Set([
|
|
119
|
+
...OUTCOME_EVIDENCE_KEYS, 'remedy_operation_id', 'remedy_action_digest',
|
|
120
|
+
'destination_binding_digest', 'units', 'unit', 'outcome',
|
|
121
|
+
]);
|
|
122
|
+
const STORED_RESOLUTION_KEYS = new Set([
|
|
123
|
+
...RESOLUTION_KEYS, 'dispute_id', 'request_digest',
|
|
124
|
+
]);
|
|
125
|
+
function deepFreeze(value) {
|
|
126
|
+
if (value === null || typeof value !== 'object')
|
|
127
|
+
return value;
|
|
128
|
+
const stack = [value];
|
|
129
|
+
const seen = new WeakSet();
|
|
130
|
+
while (stack.length > 0) {
|
|
131
|
+
const current = stack.pop();
|
|
132
|
+
if (seen.has(current))
|
|
133
|
+
continue;
|
|
134
|
+
seen.add(current);
|
|
135
|
+
for (const child of Object.values(current)) {
|
|
136
|
+
if (child !== null && typeof child === 'object')
|
|
137
|
+
stack.push(child);
|
|
138
|
+
}
|
|
139
|
+
Object.freeze(current);
|
|
140
|
+
}
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
function snapshot(value, label) {
|
|
144
|
+
try {
|
|
145
|
+
canonicalize(value);
|
|
146
|
+
return deepFreeze(structuredClone(value));
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
throw new TypeError(`${label} must be bounded canonical JSON`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function clone(value) {
|
|
153
|
+
return structuredClone(value);
|
|
154
|
+
}
|
|
155
|
+
function isRecord(value) {
|
|
156
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
157
|
+
return false;
|
|
158
|
+
const prototype = Object.getPrototypeOf(value);
|
|
159
|
+
return prototype === Object.prototype || prototype === null;
|
|
160
|
+
}
|
|
161
|
+
function isDataRecord(value) {
|
|
162
|
+
return isRecord(value) && Reflect.ownKeys(value).every((key) => {
|
|
163
|
+
if (typeof key !== 'string')
|
|
164
|
+
return false;
|
|
165
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
166
|
+
return descriptor?.enumerable === true && Object.hasOwn(descriptor, 'value');
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
function exactKeys(value, keys) {
|
|
170
|
+
return isDataRecord(value)
|
|
171
|
+
&& Reflect.ownKeys(value).length === keys.size
|
|
172
|
+
&& Object.keys(value).every((key) => keys.has(key));
|
|
173
|
+
}
|
|
174
|
+
function validId(value) {
|
|
175
|
+
return typeof value === 'string' && ID.test(value);
|
|
176
|
+
}
|
|
177
|
+
function validContext(value) {
|
|
178
|
+
return typeof value === 'string' && value.length > 0
|
|
179
|
+
&& Buffer.byteLength(value, 'utf8') <= MAX_CONTEXT_BYTES
|
|
180
|
+
&& !/[\u0000-\u001f\u007f]/.test(value);
|
|
181
|
+
}
|
|
182
|
+
function instant(value) {
|
|
183
|
+
if (typeof value !== 'string'
|
|
184
|
+
|| !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value))
|
|
185
|
+
return NaN;
|
|
186
|
+
const parsed = Date.parse(value);
|
|
187
|
+
return Number.isFinite(parsed) && new Date(parsed).toISOString() === value ? parsed : NaN;
|
|
188
|
+
}
|
|
189
|
+
function digest(value) {
|
|
190
|
+
return `sha256:${createHash('sha256').update(canonicalize(value)).digest('hex')}`;
|
|
191
|
+
}
|
|
192
|
+
function same(left, right) {
|
|
193
|
+
try {
|
|
194
|
+
return canonicalize(left) === canonicalize(right);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function fail(reason) {
|
|
201
|
+
return Object.freeze({ ok: false, reason });
|
|
202
|
+
}
|
|
203
|
+
function isFailure(value) {
|
|
204
|
+
return value.ok === false && typeof value.reason === 'string';
|
|
205
|
+
}
|
|
206
|
+
function pass(fields) {
|
|
207
|
+
return deepFreeze(fields);
|
|
208
|
+
}
|
|
209
|
+
function withoutOk(projection) {
|
|
210
|
+
const result = {};
|
|
211
|
+
for (const [key, value] of Object.entries(projection)) {
|
|
212
|
+
if (key !== 'ok')
|
|
213
|
+
result[key] = clone(value);
|
|
214
|
+
}
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
function validOriginal(value, stored = false) {
|
|
218
|
+
const keys = stored ? STORED_ORIGINAL_KEYS : ORIGINAL_KEYS;
|
|
219
|
+
return exactKeys(value, keys)
|
|
220
|
+
&& typeof value.caid === 'string' && CAID.test(value.caid)
|
|
221
|
+
&& typeof value.action_digest === 'string' && DIGEST.test(value.action_digest)
|
|
222
|
+
&& validId(value.operation_id)
|
|
223
|
+
&& ['receipt-program', 'action-escrow'].includes(value.consequence_mode)
|
|
224
|
+
&& typeof value.consequence_digest === 'string' && DIGEST.test(value.consequence_digest)
|
|
225
|
+
&& typeof value.terminal_evidence_digest === 'string' && DIGEST.test(value.terminal_evidence_digest)
|
|
226
|
+
&& ['executed', 'indeterminate'].includes(value.outcome)
|
|
227
|
+
&& Number.isFinite(instant(value.occurred_at))
|
|
228
|
+
&& (!stored || (typeof value.evidence_digest === 'string' && DIGEST.test(value.evidence_digest)));
|
|
229
|
+
}
|
|
230
|
+
function validOriginalReconciliation(value) {
|
|
231
|
+
return exactKeys(value, STORED_ORIGINAL_RECONCILIATION_KEYS)
|
|
232
|
+
&& validEvidencePair(value.evidence_id, value.evidence_digest)
|
|
233
|
+
&& validId(value.original_operation_id)
|
|
234
|
+
&& typeof value.original_action_digest === 'string' && DIGEST.test(value.original_action_digest)
|
|
235
|
+
&& typeof value.terminal_evidence_digest === 'string' && DIGEST.test(value.terminal_evidence_digest)
|
|
236
|
+
&& ['executed', 'proved_no_effect'].includes(value.outcome)
|
|
237
|
+
&& Number.isFinite(instant(value.observed_at))
|
|
238
|
+
&& typeof value.request_digest === 'string' && DIGEST.test(value.request_digest);
|
|
239
|
+
}
|
|
240
|
+
function validDispute(value) {
|
|
241
|
+
return exactKeys(value, STORED_DISPUTE_KEYS)
|
|
242
|
+
&& validId(value.dispute_id) && validId(value.evidence_id)
|
|
243
|
+
&& typeof value.evidence_digest === 'string' && DIGEST.test(value.evidence_digest)
|
|
244
|
+
&& validContext(value.challenger_id)
|
|
245
|
+
&& validId(value.original_operation_id)
|
|
246
|
+
&& typeof value.original_action_digest === 'string' && DIGEST.test(value.original_action_digest)
|
|
247
|
+
&& Number.isSafeInteger(value.requested_units) && value.requested_units > 0
|
|
248
|
+
&& Number.isFinite(instant(value.opened_at))
|
|
249
|
+
&& typeof value.request_digest === 'string' && DIGEST.test(value.request_digest);
|
|
250
|
+
}
|
|
251
|
+
function validStoredOutcome(value, reconciliation) {
|
|
252
|
+
return exactKeys(value, STORED_OUTCOME_KEYS)
|
|
253
|
+
&& validId(value.evidence_id)
|
|
254
|
+
&& typeof value.evidence_digest === 'string' && DIGEST.test(value.evidence_digest)
|
|
255
|
+
&& validId(value.remedy_operation_id)
|
|
256
|
+
&& typeof value.remedy_action_digest === 'string' && DIGEST.test(value.remedy_action_digest)
|
|
257
|
+
&& typeof value.destination_binding_digest === 'string' && DIGEST.test(value.destination_binding_digest)
|
|
258
|
+
&& Number.isSafeInteger(value.units) && value.units > 0
|
|
259
|
+
&& validContext(value.unit)
|
|
260
|
+
&& (reconciliation
|
|
261
|
+
? ['executed', 'proved_no_effect'].includes(value.outcome)
|
|
262
|
+
: ['executed', 'proved_no_effect', 'indeterminate'].includes(value.outcome))
|
|
263
|
+
&& Number.isFinite(instant(value.observed_at));
|
|
264
|
+
}
|
|
265
|
+
function validRemedyOwner(value) {
|
|
266
|
+
return (value.consequence_mode === 'receipt-program'
|
|
267
|
+
&& typeof value.capability_template_digest === 'string'
|
|
268
|
+
&& DIGEST.test(value.capability_template_digest)
|
|
269
|
+
&& value.escrow_profile_digest === null)
|
|
270
|
+
|| (value.consequence_mode === 'action-escrow'
|
|
271
|
+
&& value.capability_template_digest === null
|
|
272
|
+
&& typeof value.escrow_profile_digest === 'string'
|
|
273
|
+
&& DIGEST.test(value.escrow_profile_digest));
|
|
274
|
+
}
|
|
275
|
+
function validAttempt(value) {
|
|
276
|
+
if (!exactKeys(value, ATTEMPT_KEYS)
|
|
277
|
+
|| !validId(value.evidence_id)
|
|
278
|
+
|| typeof value.evidence_digest !== 'string' || !DIGEST.test(value.evidence_digest)
|
|
279
|
+
|| !validId(value.dispute_id) || !validId(value.original_operation_id)
|
|
280
|
+
|| !validId(value.remedy_operation_id)
|
|
281
|
+
|| typeof value.remedy_caid !== 'string' || !CAID.test(value.remedy_caid)
|
|
282
|
+
|| typeof value.remedy_action_digest !== 'string' || !DIGEST.test(value.remedy_action_digest)
|
|
283
|
+
|| !validRemedyOwner(value)
|
|
284
|
+
|| typeof value.destination_binding_digest !== 'string' || !DIGEST.test(value.destination_binding_digest)
|
|
285
|
+
|| !Number.isSafeInteger(value.units) || value.units < 1
|
|
286
|
+
|| !validContext(value.unit) || !Number.isFinite(instant(value.authorized_at))
|
|
287
|
+
|| typeof value.request_digest !== 'string' || !DIGEST.test(value.request_digest)
|
|
288
|
+
|| !['authorized', 'claimed', 'indeterminate', 'executed', 'proved_no_effect'].includes(value.status)) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
const claimed = value.status !== 'authorized';
|
|
292
|
+
if (claimed !== (typeof value.claim_token_digest === 'string' && DIGEST.test(value.claim_token_digest))
|
|
293
|
+
|| claimed !== Number.isFinite(instant(value.claimed_at))
|
|
294
|
+
|| claimed !== (typeof value.claim_request_digest === 'string' && DIGEST.test(value.claim_request_digest))) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
const finalized = ['indeterminate', 'executed', 'proved_no_effect'].includes(value.status);
|
|
298
|
+
if (finalized !== (typeof value.finalize_request_digest === 'string' && DIGEST.test(value.finalize_request_digest))
|
|
299
|
+
|| finalized !== validStoredOutcome(value.outcome_evidence, false)
|
|
300
|
+
|| finalized !== (typeof value.outcome === 'string'))
|
|
301
|
+
return false;
|
|
302
|
+
const reconciled = value.reconciliation !== null;
|
|
303
|
+
if (reconciled !== (typeof value.reconcile_request_digest === 'string' && DIGEST.test(value.reconcile_request_digest))) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
if (reconciled && !validStoredOutcome(value.reconciliation, true))
|
|
307
|
+
return false;
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
function validState(value, tenantId, instanceId) {
|
|
311
|
+
if (!exactKeys(value, STATE_KEYS)
|
|
312
|
+
|| value.version !== REMEDY_PROGRAM_VERSION
|
|
313
|
+
|| !validId(value.instance_id) || (instanceId !== undefined && value.instance_id !== instanceId)
|
|
314
|
+
|| !validContext(value.tenant_id) || (tenantId !== undefined && value.tenant_id !== tenantId)
|
|
315
|
+
|| !validContext(value.environment) || !validContext(value.audience)
|
|
316
|
+
|| ![
|
|
317
|
+
'effect_executed', 'effect_indeterminate', 'disputed', 'remedy_authorized',
|
|
318
|
+
'remedy_claimed', 'remedy_indeterminate', 'partially_remedied',
|
|
319
|
+
'remedied', 'resolved_no_remedy', 'original_proved_no_effect',
|
|
320
|
+
].includes(value.status)
|
|
321
|
+
|| !Number.isSafeInteger(value.revision) || value.revision < 0
|
|
322
|
+
|| !Number.isFinite(instant(value.created_at)) || !Number.isFinite(instant(value.updated_at))
|
|
323
|
+
|| instant(value.updated_at) < instant(value.created_at)
|
|
324
|
+
|| !validOriginal(value.original, true)
|
|
325
|
+
|| typeof value.remedy_profile_digest !== 'string' || !DIGEST.test(value.remedy_profile_digest)
|
|
326
|
+
|| typeof value.destination_binding_digest !== 'string' || !DIGEST.test(value.destination_binding_digest)
|
|
327
|
+
|| !Number.isSafeInteger(value.max_remedy_units) || value.max_remedy_units < 1
|
|
328
|
+
|| !validContext(value.unit)
|
|
329
|
+
|| !Number.isSafeInteger(value.remedied_units) || value.remedied_units < 0
|
|
330
|
+
|| !Number.isSafeInteger(value.remaining_units) || value.remaining_units < 0
|
|
331
|
+
|| value.remedied_units + value.remaining_units !== value.max_remedy_units
|
|
332
|
+
|| !Array.isArray(value.used_evidence_ids) || !value.used_evidence_ids.every(validId)
|
|
333
|
+
|| new Set(value.used_evidence_ids).size !== value.used_evidence_ids.length
|
|
334
|
+
|| !Array.isArray(value.used_evidence_digests)
|
|
335
|
+
|| !value.used_evidence_digests.every((entry) => typeof entry === 'string' && DIGEST.test(entry))
|
|
336
|
+
|| new Set(value.used_evidence_digests).size !== value.used_evidence_digests.length
|
|
337
|
+
|| !Array.isArray(value.remedies) || value.remedies.length > MAX_REMEDY_ATTEMPTS
|
|
338
|
+
|| !value.remedies.every(validAttempt)
|
|
339
|
+
|| (value.dispute !== null && !validDispute(value.dispute))
|
|
340
|
+
|| (value.active_remedy !== null && !validAttempt(value.active_remedy))
|
|
341
|
+
|| typeof value.create_request_digest !== 'string' || !DIGEST.test(value.create_request_digest)) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
if (value.revocation !== null && (!exactKeys(value.revocation, STORED_REVOCATION_KEYS)
|
|
345
|
+
|| !validId(value.revocation.evidence_id)
|
|
346
|
+
|| typeof value.revocation.evidence_digest !== 'string' || !DIGEST.test(value.revocation.evidence_digest)
|
|
347
|
+
|| !validId(value.revocation.target_operation_id)
|
|
348
|
+
|| typeof value.revocation.action_digest !== 'string' || !DIGEST.test(value.revocation.action_digest)
|
|
349
|
+
|| !validContext(value.revocation.authority_id)
|
|
350
|
+
|| !Number.isFinite(instant(value.revocation.revoked_at))
|
|
351
|
+
|| value.revocation.effect !== 'future_authority_only'
|
|
352
|
+
|| typeof value.revocation.request_digest !== 'string' || !DIGEST.test(value.revocation.request_digest))) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
if (value.original_reconciliation !== null
|
|
356
|
+
&& !validOriginalReconciliation(value.original_reconciliation))
|
|
357
|
+
return false;
|
|
358
|
+
if (value.original_reconciliation !== null
|
|
359
|
+
&& (value.original.outcome !== 'indeterminate'
|
|
360
|
+
|| value.original_reconciliation.original_operation_id !== value.original.operation_id
|
|
361
|
+
|| value.original_reconciliation.original_action_digest !== value.original.action_digest
|
|
362
|
+
|| value.original_reconciliation.terminal_evidence_digest !== value.original.terminal_evidence_digest
|
|
363
|
+
|| instant(value.original_reconciliation.observed_at) < instant(value.original.occurred_at))) {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
if (value.resolution !== null && (!exactKeys(value.resolution, STORED_RESOLUTION_KEYS)
|
|
367
|
+
|| !validId(value.resolution.evidence_id)
|
|
368
|
+
|| typeof value.resolution.evidence_digest !== 'string' || !DIGEST.test(value.resolution.evidence_digest)
|
|
369
|
+
|| value.resolution.outcome !== 'no_remedy'
|
|
370
|
+
|| !Number.isFinite(instant(value.resolution.resolved_at))
|
|
371
|
+
|| !validId(value.resolution.dispute_id)
|
|
372
|
+
|| typeof value.resolution.request_digest !== 'string' || !DIGEST.test(value.resolution.request_digest))) {
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
const committed = value.remedies
|
|
376
|
+
.filter((attempt) => attempt.status === 'executed')
|
|
377
|
+
.reduce((total, attempt) => total + attempt.units, 0);
|
|
378
|
+
if (committed !== value.remedied_units || committed > value.max_remedy_units)
|
|
379
|
+
return false;
|
|
380
|
+
const effectiveOriginalOutcome = value.original.outcome === 'executed'
|
|
381
|
+
? 'executed' : value.original_reconciliation?.outcome ?? 'indeterminate';
|
|
382
|
+
if (value.status === 'effect_indeterminate' && effectiveOriginalOutcome !== 'indeterminate')
|
|
383
|
+
return false;
|
|
384
|
+
if (value.status === 'original_proved_no_effect') {
|
|
385
|
+
if (effectiveOriginalOutcome !== 'proved_no_effect'
|
|
386
|
+
|| value.active_remedy !== null || value.remedies.length !== 0
|
|
387
|
+
|| value.remedied_units !== 0)
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
else if (effectiveOriginalOutcome === 'proved_no_effect')
|
|
391
|
+
return false;
|
|
392
|
+
if (!['effect_indeterminate', 'original_proved_no_effect', 'disputed', 'resolved_no_remedy'].includes(value.status)
|
|
393
|
+
&& effectiveOriginalOutcome !== 'executed')
|
|
394
|
+
return false;
|
|
395
|
+
if (value.status === 'disputed' && effectiveOriginalOutcome === 'proved_no_effect')
|
|
396
|
+
return false;
|
|
397
|
+
if (['remedy_authorized', 'remedy_claimed', 'remedy_indeterminate'].includes(value.status)
|
|
398
|
+
!== (value.active_remedy !== null))
|
|
399
|
+
return false;
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
function tokenDigest(token) {
|
|
403
|
+
return digest({ claim_token: token });
|
|
404
|
+
}
|
|
405
|
+
function validEvidencePair(id, evidenceDigest) {
|
|
406
|
+
return validId(id) && typeof evidenceDigest === 'string' && DIGEST.test(evidenceDigest);
|
|
407
|
+
}
|
|
408
|
+
function evidenceUsed(state, id, evidenceDigest) {
|
|
409
|
+
return state.used_evidence_ids.includes(id) || state.used_evidence_digests.includes(evidenceDigest);
|
|
410
|
+
}
|
|
411
|
+
function consumeEvidence(state, id, evidenceDigest) {
|
|
412
|
+
state.used_evidence_ids.push(id);
|
|
413
|
+
state.used_evidence_digests.push(evidenceDigest);
|
|
414
|
+
}
|
|
415
|
+
function expectedContext(state) {
|
|
416
|
+
return snapshot({
|
|
417
|
+
instance_id: state.instance_id,
|
|
418
|
+
tenant_id: state.tenant_id,
|
|
419
|
+
environment: state.environment,
|
|
420
|
+
audience: state.audience,
|
|
421
|
+
original: clone(state.original),
|
|
422
|
+
original_reconciliation: state.original_reconciliation === null
|
|
423
|
+
? null : clone(state.original_reconciliation),
|
|
424
|
+
remedy_profile_digest: state.remedy_profile_digest,
|
|
425
|
+
destination_binding_digest: state.destination_binding_digest,
|
|
426
|
+
max_remedy_units: state.max_remedy_units,
|
|
427
|
+
unit: state.unit,
|
|
428
|
+
dispute: state.dispute === null ? null : clone(state.dispute),
|
|
429
|
+
}, 'verifier expectation');
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* In-process atomic CAS store. It is intentionally marked non-durable; callers
|
|
433
|
+
* selecting production mode must provide a durable external CAS store.
|
|
434
|
+
*/
|
|
435
|
+
export function createRemedyMemoryStore() {
|
|
436
|
+
const records = new Map();
|
|
437
|
+
const storageKey = (tenantId, instanceId) => `${tenantId.length}:${tenantId}${instanceId}`;
|
|
438
|
+
const evidenceIds = new Set();
|
|
439
|
+
const evidenceDigests = new Set();
|
|
440
|
+
const remedyOperations = new Set();
|
|
441
|
+
const remedyActions = new Set();
|
|
442
|
+
const remedyCaids = new Set();
|
|
443
|
+
const claimKey = (tenantId, value) => `${tenantId.length}:${tenantId}${value}`;
|
|
444
|
+
const attempts = (state) => [
|
|
445
|
+
...(state.active_remedy === null ? [] : [state.active_remedy]),
|
|
446
|
+
...state.remedies,
|
|
447
|
+
];
|
|
448
|
+
const claimEvidence = (state) => {
|
|
449
|
+
for (const id of state.used_evidence_ids)
|
|
450
|
+
evidenceIds.add(claimKey(state.tenant_id, id));
|
|
451
|
+
for (const value of state.used_evidence_digests) {
|
|
452
|
+
evidenceDigests.add(claimKey(state.tenant_id, value));
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
const claimAttempts = (state) => {
|
|
456
|
+
for (const attempt of attempts(state)) {
|
|
457
|
+
remedyOperations.add(claimKey(state.tenant_id, attempt.remedy_operation_id));
|
|
458
|
+
remedyActions.add(claimKey(state.tenant_id, attempt.remedy_action_digest));
|
|
459
|
+
remedyCaids.add(claimKey(state.tenant_id, attempt.remedy_caid));
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
return Object.freeze({
|
|
463
|
+
durable: false,
|
|
464
|
+
async create(state) {
|
|
465
|
+
let candidate;
|
|
466
|
+
try {
|
|
467
|
+
candidate = snapshot(state, 'remedy state');
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
return fail('store_state_invalid');
|
|
471
|
+
}
|
|
472
|
+
if (!validState(candidate))
|
|
473
|
+
return fail('store_state_invalid');
|
|
474
|
+
const key = storageKey(candidate.tenant_id, candidate.instance_id);
|
|
475
|
+
if (records.has(key))
|
|
476
|
+
return fail('instance_exists');
|
|
477
|
+
if (candidate.used_evidence_ids.some((id) => evidenceIds.has(claimKey(candidate.tenant_id, id)))
|
|
478
|
+
|| candidate.used_evidence_digests.some((value) => (evidenceDigests.has(claimKey(candidate.tenant_id, value)))))
|
|
479
|
+
return fail('evidence_replayed');
|
|
480
|
+
records.set(key, clone(candidate));
|
|
481
|
+
claimEvidence(candidate);
|
|
482
|
+
claimAttempts(candidate);
|
|
483
|
+
return { ok: true, state: clone(candidate) };
|
|
484
|
+
},
|
|
485
|
+
async get({ tenantId, instanceId }) {
|
|
486
|
+
const state = records.get(storageKey(tenantId, instanceId));
|
|
487
|
+
return state ? { ok: true, state: clone(state) } : fail('instance_not_found');
|
|
488
|
+
},
|
|
489
|
+
async compareAndSwap({ tenantId, instanceId, expectedRevision, state }) {
|
|
490
|
+
const key = storageKey(tenantId, instanceId);
|
|
491
|
+
const current = records.get(key);
|
|
492
|
+
if (!current)
|
|
493
|
+
return fail('instance_not_found');
|
|
494
|
+
if (current.revision !== expectedRevision)
|
|
495
|
+
return fail('revision_conflict');
|
|
496
|
+
let candidate;
|
|
497
|
+
try {
|
|
498
|
+
candidate = snapshot(state, 'remedy state');
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
return fail('store_state_invalid');
|
|
502
|
+
}
|
|
503
|
+
if (!validState(candidate, tenantId, instanceId)
|
|
504
|
+
|| candidate.revision !== expectedRevision + 1
|
|
505
|
+
|| instant(candidate.updated_at) < instant(current.updated_at)) {
|
|
506
|
+
return fail('store_state_invalid');
|
|
507
|
+
}
|
|
508
|
+
const newEvidenceIds = candidate.used_evidence_ids.filter((id) => (!current.used_evidence_ids.includes(id)));
|
|
509
|
+
const newEvidenceDigests = candidate.used_evidence_digests.filter((value) => (!current.used_evidence_digests.includes(value)));
|
|
510
|
+
if (newEvidenceIds.some((id) => evidenceIds.has(claimKey(tenantId, id)))
|
|
511
|
+
|| newEvidenceDigests.some((value) => evidenceDigests.has(claimKey(tenantId, value)))) {
|
|
512
|
+
return fail('evidence_replayed');
|
|
513
|
+
}
|
|
514
|
+
const currentOperations = new Set(attempts(current).map((attempt) => attempt.remedy_operation_id));
|
|
515
|
+
const newAttempts = attempts(candidate).filter((attempt) => (!currentOperations.has(attempt.remedy_operation_id)));
|
|
516
|
+
if (newAttempts.some((attempt) => (remedyOperations.has(claimKey(tenantId, attempt.remedy_operation_id))
|
|
517
|
+
|| remedyActions.has(claimKey(tenantId, attempt.remedy_action_digest))
|
|
518
|
+
|| remedyCaids.has(claimKey(tenantId, attempt.remedy_caid)))))
|
|
519
|
+
return fail('remedy_operation_replayed');
|
|
520
|
+
records.set(key, clone(candidate));
|
|
521
|
+
claimEvidence(candidate);
|
|
522
|
+
claimAttempts(candidate);
|
|
523
|
+
return { ok: true, state: clone(candidate) };
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
function verifierInput(fields) {
|
|
528
|
+
return snapshot(fields, 'verifier input');
|
|
529
|
+
}
|
|
530
|
+
function validClock(value) {
|
|
531
|
+
return Number.isSafeInteger(value) && Number(value) >= 0 && Number(value) <= 8_640_000_000_000_000;
|
|
532
|
+
}
|
|
533
|
+
/** Build a remedy kernel with all trust callbacks and store methods pinned. */
|
|
534
|
+
export function createRemedyProgramKernel(options) {
|
|
535
|
+
if (!isDataRecord(options) || Object.keys(options).some((key) => !CONFIG_KEYS.has(key))) {
|
|
536
|
+
throw new TypeError('remedy program configuration invalid');
|
|
537
|
+
}
|
|
538
|
+
const sourceStore = options.store;
|
|
539
|
+
if (!sourceStore || typeof sourceStore.create !== 'function'
|
|
540
|
+
|| typeof sourceStore.get !== 'function'
|
|
541
|
+
|| typeof sourceStore.compareAndSwap !== 'function') {
|
|
542
|
+
throw new TypeError('remedy program CAS store required');
|
|
543
|
+
}
|
|
544
|
+
for (const key of [
|
|
545
|
+
'verifyOriginalEffect', 'verifyRevocation', 'verifyDispute',
|
|
546
|
+
'verifyRemedyAuthorization', 'verifyRemedyOutcome', 'verifyOriginalReconciliation',
|
|
547
|
+
]) {
|
|
548
|
+
if (typeof options[key] !== 'function')
|
|
549
|
+
throw new TypeError(`${key} callback required`);
|
|
550
|
+
}
|
|
551
|
+
if (options.verifyResolution !== undefined && typeof options.verifyResolution !== 'function') {
|
|
552
|
+
throw new TypeError('verifyResolution callback invalid');
|
|
553
|
+
}
|
|
554
|
+
if (options.now !== undefined && typeof options.now !== 'function') {
|
|
555
|
+
throw new TypeError('remedy program clock invalid');
|
|
556
|
+
}
|
|
557
|
+
if (options.production === true && options.allowEphemeralState === true) {
|
|
558
|
+
throw new TypeError('production remedy program cannot allow ephemeral state');
|
|
559
|
+
}
|
|
560
|
+
if ((options.production === true || options.allowEphemeralState === false)
|
|
561
|
+
&& sourceStore.durable !== true) {
|
|
562
|
+
throw new TypeError('durable remedy program CAS store required');
|
|
563
|
+
}
|
|
564
|
+
const maxDisputeAgeMs = options.maxDisputeAgeMs ?? DEFAULT_MAX_DISPUTE_AGE_MS;
|
|
565
|
+
if (!Number.isSafeInteger(maxDisputeAgeMs) || maxDisputeAgeMs < 1) {
|
|
566
|
+
throw new TypeError('maxDisputeAgeMs invalid');
|
|
567
|
+
}
|
|
568
|
+
const store = Object.freeze({
|
|
569
|
+
durable: sourceStore.durable === true,
|
|
570
|
+
create: sourceStore.create.bind(sourceStore),
|
|
571
|
+
get: sourceStore.get.bind(sourceStore),
|
|
572
|
+
compareAndSwap: sourceStore.compareAndSwap.bind(sourceStore),
|
|
573
|
+
});
|
|
574
|
+
const verifyOriginalEffect = options.verifyOriginalEffect;
|
|
575
|
+
const verifyRevocation = options.verifyRevocation;
|
|
576
|
+
const verifyDispute = options.verifyDispute;
|
|
577
|
+
const verifyRemedyAuthorization = options.verifyRemedyAuthorization;
|
|
578
|
+
const verifyRemedyOutcome = options.verifyRemedyOutcome;
|
|
579
|
+
const verifyOriginalReconciliation = options.verifyOriginalReconciliation;
|
|
580
|
+
const verifyResolution = options.verifyResolution;
|
|
581
|
+
const now = options.now ?? Date.now;
|
|
582
|
+
const clock = () => {
|
|
583
|
+
let value;
|
|
584
|
+
try {
|
|
585
|
+
value = now();
|
|
586
|
+
}
|
|
587
|
+
catch {
|
|
588
|
+
return NaN;
|
|
589
|
+
}
|
|
590
|
+
return validClock(value) ? Number(value) : NaN;
|
|
591
|
+
};
|
|
592
|
+
const checkedState = (value, tenantId, instanceId) => {
|
|
593
|
+
let state;
|
|
594
|
+
try {
|
|
595
|
+
state = snapshot(value, 'stored remedy state');
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
return fail('store_state_invalid');
|
|
599
|
+
}
|
|
600
|
+
return validState(state, tenantId, instanceId)
|
|
601
|
+
? pass({ ok: true, state })
|
|
602
|
+
: fail('store_state_invalid');
|
|
603
|
+
};
|
|
604
|
+
const normalizeStoreResult = (value, tenantId, instanceId, proposedState) => {
|
|
605
|
+
if (!isDataRecord(value))
|
|
606
|
+
return fail('store_response_invalid');
|
|
607
|
+
if (value.ok === true)
|
|
608
|
+
return checkedState(value.state ?? proposedState, tenantId, instanceId);
|
|
609
|
+
if (value.applied === true && proposedState)
|
|
610
|
+
return checkedState(value.state ?? proposedState, tenantId, instanceId);
|
|
611
|
+
const reason = value.reason;
|
|
612
|
+
if ([
|
|
613
|
+
'instance_not_found', 'instance_exists', 'revision_conflict', 'clock_regression',
|
|
614
|
+
'evidence_replayed', 'remedy_operation_replayed',
|
|
615
|
+
].includes(reason)) {
|
|
616
|
+
return fail(reason);
|
|
617
|
+
}
|
|
618
|
+
if (value.ok === false || value.applied === false)
|
|
619
|
+
return fail('store_operation_failed');
|
|
620
|
+
return fail('store_response_invalid');
|
|
621
|
+
};
|
|
622
|
+
const load = async (tenantId, instanceId, creationProbe = false) => {
|
|
623
|
+
if (!validContext(tenantId) || !validId(instanceId))
|
|
624
|
+
return fail('remedy_lookup_invalid');
|
|
625
|
+
try {
|
|
626
|
+
const result = normalizeStoreResult(await store.get({ tenantId, instanceId }), tenantId, instanceId);
|
|
627
|
+
return result.reason === 'instance_not_found' && !creationProbe
|
|
628
|
+
? fail('remedy_case_not_found') : result;
|
|
629
|
+
}
|
|
630
|
+
catch {
|
|
631
|
+
return fail('store_unavailable');
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
const commit = async (state, next) => {
|
|
635
|
+
try {
|
|
636
|
+
const result = normalizeStoreResult(await store.compareAndSwap({
|
|
637
|
+
tenantId: state.tenant_id,
|
|
638
|
+
instanceId: state.instance_id,
|
|
639
|
+
expectedRevision: state.revision,
|
|
640
|
+
state: next,
|
|
641
|
+
}), state.tenant_id, state.instance_id, next);
|
|
642
|
+
return result.reason === 'revision_conflict' ? fail('state_transition_conflict') : result;
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
return fail('store_unavailable');
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
const operationInput = (input, keys, reason) => {
|
|
649
|
+
let value;
|
|
650
|
+
try {
|
|
651
|
+
value = snapshot(input, reason);
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
return fail(reason);
|
|
655
|
+
}
|
|
656
|
+
return exactKeys(value, keys) ? value : fail(reason);
|
|
657
|
+
};
|
|
658
|
+
const transitionTime = (state) => {
|
|
659
|
+
const current = clock();
|
|
660
|
+
if (!Number.isFinite(current))
|
|
661
|
+
return fail('clock_invalid');
|
|
662
|
+
if (current < instant(state.updated_at))
|
|
663
|
+
return fail('clock_regression');
|
|
664
|
+
return current;
|
|
665
|
+
};
|
|
666
|
+
const touch = (state, at) => {
|
|
667
|
+
const next = clone(state);
|
|
668
|
+
next.revision += 1;
|
|
669
|
+
next.updated_at = new Date(at).toISOString();
|
|
670
|
+
return next;
|
|
671
|
+
};
|
|
672
|
+
const invoke = async (callback, input) => {
|
|
673
|
+
try {
|
|
674
|
+
const result = await callback(verifierInput(input));
|
|
675
|
+
return snapshot(result, 'verifier projection');
|
|
676
|
+
}
|
|
677
|
+
catch {
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
async function create(input) {
|
|
682
|
+
const parsed = operationInput(input, CREATE_KEYS, 'create_input_invalid');
|
|
683
|
+
if (isFailure(parsed))
|
|
684
|
+
return parsed;
|
|
685
|
+
const value = parsed;
|
|
686
|
+
if (!validId(value.instanceId) || !validContext(value.tenantId)
|
|
687
|
+
|| !validContext(value.environment) || !validContext(value.audience)
|
|
688
|
+
|| !validOriginal(value.original)
|
|
689
|
+
|| typeof value.remedyProfileDigest !== 'string' || !DIGEST.test(value.remedyProfileDigest)
|
|
690
|
+
|| typeof value.destinationBindingDigest !== 'string' || !DIGEST.test(value.destinationBindingDigest)
|
|
691
|
+
|| !Number.isSafeInteger(value.maxRemedyUnits) || value.maxRemedyUnits < 1
|
|
692
|
+
|| !validContext(value.unit) || !isDataRecord(value.evidence)) {
|
|
693
|
+
return fail('create_input_invalid');
|
|
694
|
+
}
|
|
695
|
+
const requestDigest = digest(value);
|
|
696
|
+
const existing = await load(value.tenantId, value.instanceId, true);
|
|
697
|
+
if (existing.ok) {
|
|
698
|
+
return existing.state.create_request_digest === requestDigest
|
|
699
|
+
? pass({ ok: true, idempotent: true, state: existing.state })
|
|
700
|
+
: fail('instance_exists');
|
|
701
|
+
}
|
|
702
|
+
if (existing.reason !== 'instance_not_found')
|
|
703
|
+
return existing;
|
|
704
|
+
const current = clock();
|
|
705
|
+
if (!Number.isFinite(current))
|
|
706
|
+
return fail('clock_invalid');
|
|
707
|
+
if (instant(value.original.occurred_at) > current)
|
|
708
|
+
return fail('original_effect_time_invalid');
|
|
709
|
+
const verified = await invoke(verifyOriginalEffect, {
|
|
710
|
+
original: clone(value.original),
|
|
711
|
+
evidence: clone(value.evidence),
|
|
712
|
+
expected: {
|
|
713
|
+
instance_id: value.instanceId,
|
|
714
|
+
tenant_id: value.tenantId,
|
|
715
|
+
environment: value.environment,
|
|
716
|
+
audience: value.audience,
|
|
717
|
+
remedy_profile_digest: value.remedyProfileDigest,
|
|
718
|
+
destination_binding_digest: value.destinationBindingDigest,
|
|
719
|
+
max_remedy_units: value.maxRemedyUnits,
|
|
720
|
+
unit: value.unit,
|
|
721
|
+
},
|
|
722
|
+
});
|
|
723
|
+
if (!verified || verified.ok !== true || !exactKeys(verified, VERIFIED_ORIGINAL_KEYS)) {
|
|
724
|
+
return fail('original_effect_invalid');
|
|
725
|
+
}
|
|
726
|
+
if (!ORIGINAL_KEYS.size || [...ORIGINAL_KEYS].some((key) => !same(verified[key], value.original[key]))
|
|
727
|
+
|| verified.evidence_digest !== value.original.terminal_evidence_digest) {
|
|
728
|
+
return fail('original_effect_binding_mismatch');
|
|
729
|
+
}
|
|
730
|
+
const at = new Date(current).toISOString();
|
|
731
|
+
const state = {
|
|
732
|
+
version: REMEDY_PROGRAM_VERSION,
|
|
733
|
+
instance_id: value.instanceId,
|
|
734
|
+
tenant_id: value.tenantId,
|
|
735
|
+
environment: value.environment,
|
|
736
|
+
audience: value.audience,
|
|
737
|
+
status: value.original.outcome === 'executed' ? 'effect_executed' : 'effect_indeterminate',
|
|
738
|
+
revision: 0,
|
|
739
|
+
created_at: at,
|
|
740
|
+
updated_at: at,
|
|
741
|
+
original: withoutOk(verified),
|
|
742
|
+
remedy_profile_digest: value.remedyProfileDigest,
|
|
743
|
+
destination_binding_digest: value.destinationBindingDigest,
|
|
744
|
+
max_remedy_units: value.maxRemedyUnits,
|
|
745
|
+
unit: value.unit,
|
|
746
|
+
remedied_units: 0,
|
|
747
|
+
remaining_units: value.maxRemedyUnits,
|
|
748
|
+
used_evidence_ids: [],
|
|
749
|
+
used_evidence_digests: [value.original.terminal_evidence_digest],
|
|
750
|
+
original_reconciliation: null,
|
|
751
|
+
revocation: null,
|
|
752
|
+
dispute: null,
|
|
753
|
+
active_remedy: null,
|
|
754
|
+
remedies: [],
|
|
755
|
+
resolution: null,
|
|
756
|
+
create_request_digest: requestDigest,
|
|
757
|
+
};
|
|
758
|
+
if (!validState(state, value.tenantId, value.instanceId))
|
|
759
|
+
return fail('original_effect_invalid');
|
|
760
|
+
try {
|
|
761
|
+
const result = normalizeStoreResult(await store.create(snapshot(state, 'initial remedy state')), value.tenantId, value.instanceId, state);
|
|
762
|
+
if (result.reason !== 'instance_exists')
|
|
763
|
+
return result;
|
|
764
|
+
const raced = await load(value.tenantId, value.instanceId, true);
|
|
765
|
+
return raced.ok && raced.state.create_request_digest === requestDigest
|
|
766
|
+
? pass({ ok: true, idempotent: true, state: raced.state })
|
|
767
|
+
: fail('instance_exists');
|
|
768
|
+
}
|
|
769
|
+
catch {
|
|
770
|
+
return fail('store_unavailable');
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
async function reconcileOriginalEffect(input) {
|
|
774
|
+
const parsed = operationInput(input, ORIGINAL_RECONCILIATION_INPUT_KEYS, 'original_reconciliation_input_invalid');
|
|
775
|
+
if (isFailure(parsed))
|
|
776
|
+
return parsed;
|
|
777
|
+
const value = parsed;
|
|
778
|
+
if (!validContext(value.tenantId) || !validId(value.instanceId)
|
|
779
|
+
|| !['executed', 'proved_no_effect'].includes(value.outcome)
|
|
780
|
+
|| !exactKeys(value.evidence, ORIGINAL_RECONCILIATION_EVIDENCE_KEYS)
|
|
781
|
+
|| !validEvidencePair(value.evidence.evidence_id, value.evidence.evidence_digest)
|
|
782
|
+
|| !Number.isFinite(instant(value.evidence.observed_at))) {
|
|
783
|
+
return fail('original_reconciliation_input_invalid');
|
|
784
|
+
}
|
|
785
|
+
const loaded = await load(value.tenantId, value.instanceId);
|
|
786
|
+
if (!loaded.ok)
|
|
787
|
+
return loaded;
|
|
788
|
+
const state = loaded.state;
|
|
789
|
+
const requestDigest = digest(value);
|
|
790
|
+
if (state.original.outcome !== 'indeterminate')
|
|
791
|
+
return fail('original_effect_already_conclusive');
|
|
792
|
+
if (state.original_reconciliation !== null) {
|
|
793
|
+
return state.original_reconciliation.request_digest === requestDigest
|
|
794
|
+
? pass({ ok: true, idempotent: true, state })
|
|
795
|
+
: fail('original_reconciliation_conflict');
|
|
796
|
+
}
|
|
797
|
+
if (!['effect_indeterminate', 'disputed'].includes(state.status)
|
|
798
|
+
|| state.active_remedy !== null || state.remedies.length !== 0) {
|
|
799
|
+
return fail('original_reconciliation_unavailable');
|
|
800
|
+
}
|
|
801
|
+
if (evidenceUsed(state, value.evidence.evidence_id, value.evidence.evidence_digest)) {
|
|
802
|
+
return fail('evidence_replayed');
|
|
803
|
+
}
|
|
804
|
+
const at = transitionTime(state);
|
|
805
|
+
if (typeof at !== 'number')
|
|
806
|
+
return at;
|
|
807
|
+
const observedAt = instant(value.evidence.observed_at);
|
|
808
|
+
if (observedAt < instant(state.original.occurred_at) || observedAt > at) {
|
|
809
|
+
return fail('original_reconciliation_time_invalid');
|
|
810
|
+
}
|
|
811
|
+
const verified = await invoke(verifyOriginalReconciliation, {
|
|
812
|
+
evidence: clone(value.evidence), outcome: value.outcome,
|
|
813
|
+
expected: expectedContext(state),
|
|
814
|
+
});
|
|
815
|
+
if (!verified || verified.ok !== true
|
|
816
|
+
|| !exactKeys(verified, VERIFIED_ORIGINAL_RECONCILIATION_KEYS)) {
|
|
817
|
+
return fail('original_reconciliation_invalid');
|
|
818
|
+
}
|
|
819
|
+
if (verified.evidence_id !== value.evidence.evidence_id
|
|
820
|
+
|| verified.evidence_digest !== value.evidence.evidence_digest
|
|
821
|
+
|| verified.original_operation_id !== state.original.operation_id
|
|
822
|
+
|| verified.original_action_digest !== state.original.action_digest
|
|
823
|
+
|| verified.terminal_evidence_digest !== state.original.terminal_evidence_digest
|
|
824
|
+
|| verified.outcome !== value.outcome
|
|
825
|
+
|| verified.observed_at !== value.evidence.observed_at) {
|
|
826
|
+
return fail('original_reconciliation_binding_mismatch');
|
|
827
|
+
}
|
|
828
|
+
const next = touch(state, at);
|
|
829
|
+
next.original_reconciliation = {
|
|
830
|
+
...withoutOk(verified), request_digest: requestDigest,
|
|
831
|
+
};
|
|
832
|
+
next.status = value.outcome === 'executed'
|
|
833
|
+
? (state.dispute === null ? 'effect_executed' : 'disputed')
|
|
834
|
+
: 'original_proved_no_effect';
|
|
835
|
+
consumeEvidence(next, verified.evidence_id, verified.evidence_digest);
|
|
836
|
+
const committed = await commit(state, next);
|
|
837
|
+
return committed.ok ? pass({ ok: true, state: committed.state }) : committed;
|
|
838
|
+
}
|
|
839
|
+
async function recordRevocation(input) {
|
|
840
|
+
const parsed = operationInput(input, REVOCATION_INPUT_KEYS, 'revocation_input_invalid');
|
|
841
|
+
if (isFailure(parsed))
|
|
842
|
+
return parsed;
|
|
843
|
+
const value = parsed;
|
|
844
|
+
if (!validContext(value.tenantId) || !validId(value.instanceId)
|
|
845
|
+
|| !exactKeys(value.evidence, REVOCATION_EVIDENCE_KEYS)
|
|
846
|
+
|| !validEvidencePair(value.evidence.id, value.evidence.digest)) {
|
|
847
|
+
return fail('revocation_input_invalid');
|
|
848
|
+
}
|
|
849
|
+
const loaded = await load(value.tenantId, value.instanceId);
|
|
850
|
+
if (!loaded.ok)
|
|
851
|
+
return loaded;
|
|
852
|
+
const state = loaded.state;
|
|
853
|
+
const requestDigest = digest(value);
|
|
854
|
+
if (state.revocation !== null) {
|
|
855
|
+
return state.revocation.request_digest === requestDigest
|
|
856
|
+
? pass({ ok: true, idempotent: true, code: 'late_revocation_recorded', state })
|
|
857
|
+
: fail('revocation_already_recorded');
|
|
858
|
+
}
|
|
859
|
+
if (evidenceUsed(state, value.evidence.id, value.evidence.digest))
|
|
860
|
+
return fail('evidence_replayed');
|
|
861
|
+
const at = transitionTime(state);
|
|
862
|
+
if (typeof at !== 'number')
|
|
863
|
+
return at;
|
|
864
|
+
const verified = await invoke(verifyRevocation, {
|
|
865
|
+
evidence: clone(value.evidence), expected: expectedContext(state),
|
|
866
|
+
});
|
|
867
|
+
if (!verified || verified.ok !== true || !exactKeys(verified, VERIFIED_REVOCATION_KEYS)) {
|
|
868
|
+
return fail('revocation_invalid');
|
|
869
|
+
}
|
|
870
|
+
if (verified.evidence_id !== value.evidence.id
|
|
871
|
+
|| verified.evidence_digest !== value.evidence.digest
|
|
872
|
+
|| verified.target_operation_id !== state.original.operation_id
|
|
873
|
+
|| verified.action_digest !== state.original.action_digest) {
|
|
874
|
+
return fail('revocation_binding_mismatch');
|
|
875
|
+
}
|
|
876
|
+
const revokedAt = instant(verified.revoked_at);
|
|
877
|
+
if (!validContext(verified.authority_id) || !Number.isFinite(revokedAt)
|
|
878
|
+
|| revokedAt < instant(state.original.occurred_at) || revokedAt > at) {
|
|
879
|
+
return fail('revocation_time_invalid');
|
|
880
|
+
}
|
|
881
|
+
const next = touch(state, at);
|
|
882
|
+
next.revocation = {
|
|
883
|
+
...withoutOk(verified),
|
|
884
|
+
effect: 'future_authority_only',
|
|
885
|
+
request_digest: requestDigest,
|
|
886
|
+
};
|
|
887
|
+
consumeEvidence(next, verified.evidence_id, verified.evidence_digest);
|
|
888
|
+
const committed = await commit(state, next);
|
|
889
|
+
return committed.ok
|
|
890
|
+
? pass({ ok: true, code: 'late_revocation_recorded', state: committed.state })
|
|
891
|
+
: committed;
|
|
892
|
+
}
|
|
893
|
+
async function openDispute(input) {
|
|
894
|
+
const parsed = operationInput(input, DISPUTE_INPUT_KEYS, 'dispute_input_invalid');
|
|
895
|
+
if (isFailure(parsed))
|
|
896
|
+
return parsed;
|
|
897
|
+
const value = parsed;
|
|
898
|
+
if (!validContext(value.tenantId) || !validId(value.instanceId)
|
|
899
|
+
|| !exactKeys(value.dispute, DISPUTE_KEYS)
|
|
900
|
+
|| !validId(value.dispute.dispute_id)
|
|
901
|
+
|| !validEvidencePair(value.dispute.evidence_id, value.dispute.evidence_digest)
|
|
902
|
+
|| !validContext(value.dispute.challenger_id)
|
|
903
|
+
|| !Number.isSafeInteger(value.dispute.requested_units) || value.dispute.requested_units < 1
|
|
904
|
+
|| !Number.isFinite(instant(value.dispute.opened_at))) {
|
|
905
|
+
return fail('dispute_input_invalid');
|
|
906
|
+
}
|
|
907
|
+
const loaded = await load(value.tenantId, value.instanceId);
|
|
908
|
+
if (!loaded.ok)
|
|
909
|
+
return loaded;
|
|
910
|
+
const state = loaded.state;
|
|
911
|
+
const requestDigest = digest(value);
|
|
912
|
+
if (state.dispute !== null) {
|
|
913
|
+
return state.dispute.request_digest === requestDigest
|
|
914
|
+
? pass({ ok: true, idempotent: true, state })
|
|
915
|
+
: fail('dispute_already_open');
|
|
916
|
+
}
|
|
917
|
+
if (!['effect_executed', 'effect_indeterminate'].includes(state.status)) {
|
|
918
|
+
return fail('remedy_case_unavailable');
|
|
919
|
+
}
|
|
920
|
+
if (evidenceUsed(state, value.dispute.evidence_id, value.dispute.evidence_digest)) {
|
|
921
|
+
return fail('evidence_replayed');
|
|
922
|
+
}
|
|
923
|
+
const at = transitionTime(state);
|
|
924
|
+
if (typeof at !== 'number')
|
|
925
|
+
return at;
|
|
926
|
+
const openedAt = instant(value.dispute.opened_at);
|
|
927
|
+
if (openedAt < instant(state.original.occurred_at) || openedAt > at || at - openedAt > maxDisputeAgeMs) {
|
|
928
|
+
return fail('dispute_not_fresh');
|
|
929
|
+
}
|
|
930
|
+
if (value.dispute.requested_units > state.remaining_units)
|
|
931
|
+
return fail('dispute_limit_exceeded');
|
|
932
|
+
const verified = await invoke(verifyDispute, {
|
|
933
|
+
dispute: clone(value.dispute), expected: expectedContext(state),
|
|
934
|
+
});
|
|
935
|
+
if (!verified || verified.ok !== true || !exactKeys(verified, VERIFIED_DISPUTE_KEYS)) {
|
|
936
|
+
return fail('dispute_invalid');
|
|
937
|
+
}
|
|
938
|
+
if ([...DISPUTE_KEYS].some((key) => !same(verified[key], value.dispute[key]))
|
|
939
|
+
|| verified.original_operation_id !== state.original.operation_id
|
|
940
|
+
|| verified.original_action_digest !== state.original.action_digest) {
|
|
941
|
+
return fail('dispute_binding_mismatch');
|
|
942
|
+
}
|
|
943
|
+
const next = touch(state, at);
|
|
944
|
+
next.status = 'disputed';
|
|
945
|
+
next.dispute = { ...withoutOk(verified), request_digest: requestDigest };
|
|
946
|
+
consumeEvidence(next, verified.evidence_id, verified.evidence_digest);
|
|
947
|
+
const committed = await commit(state, next);
|
|
948
|
+
return committed.ok ? pass({ ok: true, state: committed.state }) : committed;
|
|
949
|
+
}
|
|
950
|
+
const allAttempts = (state) => [
|
|
951
|
+
...(state.active_remedy ? [state.active_remedy] : []), ...state.remedies,
|
|
952
|
+
];
|
|
953
|
+
async function authorizeRemedy(input) {
|
|
954
|
+
const parsed = operationInput(input, AUTHORIZATION_INPUT_KEYS, 'remedy_authorization_input_invalid');
|
|
955
|
+
if (isFailure(parsed))
|
|
956
|
+
return parsed;
|
|
957
|
+
const value = parsed;
|
|
958
|
+
if (!validContext(value.tenantId) || !validId(value.instanceId)
|
|
959
|
+
|| !exactKeys(value.authorization, AUTHORIZATION_KEYS)
|
|
960
|
+
|| !validEvidencePair(value.authorization.evidence_id, value.authorization.evidence_digest)
|
|
961
|
+
|| !validId(value.authorization.remedy_operation_id)
|
|
962
|
+
|| typeof value.authorization.remedy_caid !== 'string' || !CAID.test(value.authorization.remedy_caid)
|
|
963
|
+
|| typeof value.authorization.remedy_action_digest !== 'string'
|
|
964
|
+
|| !DIGEST.test(value.authorization.remedy_action_digest)
|
|
965
|
+
|| !Number.isSafeInteger(value.authorization.units) || value.authorization.units < 1
|
|
966
|
+
|| !Number.isFinite(instant(value.authorization.authorized_at))) {
|
|
967
|
+
return fail('remedy_authorization_input_invalid');
|
|
968
|
+
}
|
|
969
|
+
if (!validRemedyOwner(value.authorization))
|
|
970
|
+
return fail('remedy_owner_invalid');
|
|
971
|
+
const loaded = await load(value.tenantId, value.instanceId);
|
|
972
|
+
if (!loaded.ok)
|
|
973
|
+
return loaded;
|
|
974
|
+
const state = loaded.state;
|
|
975
|
+
const requestDigest = digest(value);
|
|
976
|
+
if (state.status === 'original_proved_no_effect')
|
|
977
|
+
return fail('remedy_case_terminal');
|
|
978
|
+
if (state.original.outcome === 'indeterminate'
|
|
979
|
+
&& state.original_reconciliation?.outcome !== 'executed') {
|
|
980
|
+
return fail('original_effect_indeterminate');
|
|
981
|
+
}
|
|
982
|
+
const priorOperation = allAttempts(state).find((attempt) => (attempt.remedy_operation_id === value.authorization.remedy_operation_id));
|
|
983
|
+
if (priorOperation) {
|
|
984
|
+
return priorOperation.request_digest === requestDigest
|
|
985
|
+
? pass({ ok: true, idempotent: true, state })
|
|
986
|
+
: fail('remedy_operation_replayed');
|
|
987
|
+
}
|
|
988
|
+
if (state.remaining_units === 0)
|
|
989
|
+
return fail('remedy_limit_exhausted');
|
|
990
|
+
if (['remedied', 'resolved_no_remedy'].includes(state.status))
|
|
991
|
+
return fail('remedy_case_terminal');
|
|
992
|
+
if (state.active_remedy !== null) {
|
|
993
|
+
return state.status === 'remedy_indeterminate'
|
|
994
|
+
? fail('remedy_indeterminate') : fail('remedy_already_active');
|
|
995
|
+
}
|
|
996
|
+
if (!['disputed', 'partially_remedied'].includes(state.status) || state.dispute === null) {
|
|
997
|
+
return fail('remedy_case_unavailable');
|
|
998
|
+
}
|
|
999
|
+
if (evidenceUsed(state, value.authorization.evidence_id, value.authorization.evidence_digest)) {
|
|
1000
|
+
return fail('evidence_replayed');
|
|
1001
|
+
}
|
|
1002
|
+
const at = transitionTime(state);
|
|
1003
|
+
if (typeof at !== 'number')
|
|
1004
|
+
return at;
|
|
1005
|
+
if (instant(value.authorization.authorized_at) > at
|
|
1006
|
+
|| instant(value.authorization.authorized_at) < instant(state.dispute.opened_at)) {
|
|
1007
|
+
return fail('remedy_authorization_time_invalid');
|
|
1008
|
+
}
|
|
1009
|
+
const disputeRemaining = state.dispute.requested_units - state.remedied_units;
|
|
1010
|
+
if (value.authorization.units > state.remaining_units || value.authorization.units > disputeRemaining) {
|
|
1011
|
+
return fail('remedy_limit_exceeded');
|
|
1012
|
+
}
|
|
1013
|
+
if (value.authorization.remedy_operation_id === state.original.operation_id
|
|
1014
|
+
|| value.authorization.remedy_action_digest === state.original.action_digest) {
|
|
1015
|
+
return fail('remedy_must_be_compensating');
|
|
1016
|
+
}
|
|
1017
|
+
const verified = await invoke(verifyRemedyAuthorization, {
|
|
1018
|
+
authorization: clone(value.authorization), expected: expectedContext(state),
|
|
1019
|
+
});
|
|
1020
|
+
if (!verified || verified.ok !== true || !exactKeys(verified, VERIFIED_AUTHORIZATION_KEYS)) {
|
|
1021
|
+
return fail('remedy_authorization_invalid');
|
|
1022
|
+
}
|
|
1023
|
+
if (!validRemedyOwner(verified)) {
|
|
1024
|
+
return fail('remedy_owner_invalid');
|
|
1025
|
+
}
|
|
1026
|
+
if ([...AUTHORIZATION_KEYS].some((key) => !same(verified[key], value.authorization[key]))
|
|
1027
|
+
|| verified.dispute_id !== state.dispute.dispute_id
|
|
1028
|
+
|| verified.original_operation_id !== state.original.operation_id
|
|
1029
|
+
|| verified.destination_binding_digest !== state.destination_binding_digest
|
|
1030
|
+
|| verified.unit !== state.unit) {
|
|
1031
|
+
return fail('remedy_authorization_binding_mismatch');
|
|
1032
|
+
}
|
|
1033
|
+
const next = touch(state, at);
|
|
1034
|
+
next.status = 'remedy_authorized';
|
|
1035
|
+
next.active_remedy = {
|
|
1036
|
+
...withoutOk(verified),
|
|
1037
|
+
request_digest: requestDigest,
|
|
1038
|
+
status: 'authorized',
|
|
1039
|
+
claim_token_digest: null,
|
|
1040
|
+
claimed_at: null,
|
|
1041
|
+
claim_request_digest: null,
|
|
1042
|
+
outcome: null,
|
|
1043
|
+
outcome_evidence: null,
|
|
1044
|
+
finalize_request_digest: null,
|
|
1045
|
+
reconciliation: null,
|
|
1046
|
+
reconcile_request_digest: null,
|
|
1047
|
+
};
|
|
1048
|
+
consumeEvidence(next, verified.evidence_id, verified.evidence_digest);
|
|
1049
|
+
const committed = await commit(state, next);
|
|
1050
|
+
return committed.ok ? pass({ ok: true, state: committed.state }) : committed;
|
|
1051
|
+
}
|
|
1052
|
+
async function claimRemedy(input) {
|
|
1053
|
+
const parsed = operationInput(input, CLAIM_KEYS, 'remedy_claim_input_invalid');
|
|
1054
|
+
if (isFailure(parsed))
|
|
1055
|
+
return parsed;
|
|
1056
|
+
const value = parsed;
|
|
1057
|
+
if (!validContext(value.tenantId) || !validId(value.instanceId)
|
|
1058
|
+
|| !validId(value.remedyOperationId)
|
|
1059
|
+
|| !validContext(value.claimToken) || Buffer.byteLength(value.claimToken, 'utf8') > 256) {
|
|
1060
|
+
return fail('remedy_claim_input_invalid');
|
|
1061
|
+
}
|
|
1062
|
+
const loaded = await load(value.tenantId, value.instanceId);
|
|
1063
|
+
if (!loaded.ok)
|
|
1064
|
+
return loaded;
|
|
1065
|
+
const state = loaded.state;
|
|
1066
|
+
if (state.status === 'original_proved_no_effect')
|
|
1067
|
+
return fail('remedy_case_terminal');
|
|
1068
|
+
if (state.original.outcome === 'indeterminate'
|
|
1069
|
+
&& state.original_reconciliation?.outcome !== 'executed') {
|
|
1070
|
+
return fail('original_effect_indeterminate');
|
|
1071
|
+
}
|
|
1072
|
+
const requestDigest = digest(value);
|
|
1073
|
+
const prior = allAttempts(state).find((attempt) => attempt.remedy_operation_id === value.remedyOperationId);
|
|
1074
|
+
if (prior?.claim_request_digest === requestDigest) {
|
|
1075
|
+
if (state.status === 'remedy_indeterminate' && state.active_remedy === prior) {
|
|
1076
|
+
return fail('remedy_indeterminate');
|
|
1077
|
+
}
|
|
1078
|
+
return pass({ ok: true, idempotent: true, state });
|
|
1079
|
+
}
|
|
1080
|
+
if (state.status === 'remedy_indeterminate')
|
|
1081
|
+
return fail('remedy_indeterminate');
|
|
1082
|
+
if (!state.active_remedy)
|
|
1083
|
+
return prior ? fail('remedy_already_finalized') : fail('remedy_not_authorized');
|
|
1084
|
+
if (state.active_remedy.remedy_operation_id !== value.remedyOperationId) {
|
|
1085
|
+
return fail('remedy_operation_mismatch');
|
|
1086
|
+
}
|
|
1087
|
+
if (state.active_remedy.status === 'claimed')
|
|
1088
|
+
return fail('remedy_claim_owned');
|
|
1089
|
+
if (state.active_remedy.status !== 'authorized' || state.status !== 'remedy_authorized') {
|
|
1090
|
+
return fail('remedy_not_authorized');
|
|
1091
|
+
}
|
|
1092
|
+
const at = transitionTime(state);
|
|
1093
|
+
if (typeof at !== 'number')
|
|
1094
|
+
return at;
|
|
1095
|
+
const next = touch(state, at);
|
|
1096
|
+
next.status = 'remedy_claimed';
|
|
1097
|
+
next.active_remedy.status = 'claimed';
|
|
1098
|
+
next.active_remedy.claim_token_digest = tokenDigest(value.claimToken);
|
|
1099
|
+
next.active_remedy.claimed_at = new Date(at).toISOString();
|
|
1100
|
+
next.active_remedy.claim_request_digest = requestDigest;
|
|
1101
|
+
const committed = await commit(state, next);
|
|
1102
|
+
return committed.ok ? pass({ ok: true, state: committed.state }) : committed;
|
|
1103
|
+
}
|
|
1104
|
+
const outcomeExpected = (state, attempt) => ({
|
|
1105
|
+
...clone(expectedContext(state)),
|
|
1106
|
+
remedy_operation_id: attempt.remedy_operation_id,
|
|
1107
|
+
remedy_caid: attempt.remedy_caid,
|
|
1108
|
+
remedy_action_digest: attempt.remedy_action_digest,
|
|
1109
|
+
destination_binding_digest: attempt.destination_binding_digest,
|
|
1110
|
+
units: attempt.units,
|
|
1111
|
+
unit: attempt.unit,
|
|
1112
|
+
});
|
|
1113
|
+
const outcomeBound = (verified, value, attempt) => verified.evidence_id === value.evidence.evidence_id
|
|
1114
|
+
&& verified.evidence_digest === value.evidence.evidence_digest
|
|
1115
|
+
&& verified.remedy_operation_id === attempt.remedy_operation_id
|
|
1116
|
+
&& verified.remedy_action_digest === attempt.remedy_action_digest
|
|
1117
|
+
&& verified.destination_binding_digest === attempt.destination_binding_digest
|
|
1118
|
+
&& verified.units === attempt.units && verified.unit === attempt.unit
|
|
1119
|
+
&& verified.outcome === value.outcome
|
|
1120
|
+
&& verified.observed_at === value.evidence.observed_at;
|
|
1121
|
+
async function finalizeRemedy(input) {
|
|
1122
|
+
const parsed = operationInput(input, FINALIZE_KEYS, 'remedy_outcome_input_invalid');
|
|
1123
|
+
if (isFailure(parsed))
|
|
1124
|
+
return parsed;
|
|
1125
|
+
const value = parsed;
|
|
1126
|
+
if (!validContext(value.tenantId) || !validId(value.instanceId)
|
|
1127
|
+
|| !validId(value.remedyOperationId)
|
|
1128
|
+
|| !validContext(value.claimToken) || Buffer.byteLength(value.claimToken, 'utf8') > 256
|
|
1129
|
+
|| !['executed', 'proved_no_effect', 'indeterminate'].includes(value.outcome)
|
|
1130
|
+
|| !exactKeys(value.evidence, OUTCOME_EVIDENCE_KEYS)
|
|
1131
|
+
|| !validEvidencePair(value.evidence.evidence_id, value.evidence.evidence_digest)
|
|
1132
|
+
|| !Number.isFinite(instant(value.evidence.observed_at))) {
|
|
1133
|
+
return fail('remedy_outcome_input_invalid');
|
|
1134
|
+
}
|
|
1135
|
+
const loaded = await load(value.tenantId, value.instanceId);
|
|
1136
|
+
if (!loaded.ok)
|
|
1137
|
+
return loaded;
|
|
1138
|
+
const state = loaded.state;
|
|
1139
|
+
const requestDigest = digest(value);
|
|
1140
|
+
const prior = allAttempts(state).find((attempt) => attempt.remedy_operation_id === value.remedyOperationId);
|
|
1141
|
+
if (prior?.finalize_request_digest === requestDigest) {
|
|
1142
|
+
return pass({ ok: true, idempotent: true, state });
|
|
1143
|
+
}
|
|
1144
|
+
if (state.status === 'remedy_indeterminate')
|
|
1145
|
+
return fail('remedy_indeterminate');
|
|
1146
|
+
if (!state.active_remedy)
|
|
1147
|
+
return prior ? fail('remedy_already_finalized') : fail('remedy_not_authorized');
|
|
1148
|
+
const attempt = state.active_remedy;
|
|
1149
|
+
if (attempt.remedy_operation_id !== value.remedyOperationId)
|
|
1150
|
+
return fail('remedy_operation_mismatch');
|
|
1151
|
+
if (attempt.status !== 'claimed' || state.status !== 'remedy_claimed')
|
|
1152
|
+
return fail('remedy_not_claimed');
|
|
1153
|
+
if (attempt.claim_token_digest !== tokenDigest(value.claimToken))
|
|
1154
|
+
return fail('remedy_claim_owned');
|
|
1155
|
+
if (evidenceUsed(state, value.evidence.evidence_id, value.evidence.evidence_digest)) {
|
|
1156
|
+
return fail('evidence_replayed');
|
|
1157
|
+
}
|
|
1158
|
+
const at = transitionTime(state);
|
|
1159
|
+
if (typeof at !== 'number')
|
|
1160
|
+
return at;
|
|
1161
|
+
if (instant(value.evidence.observed_at) < instant(attempt.authorized_at)
|
|
1162
|
+
|| instant(value.evidence.observed_at) > at)
|
|
1163
|
+
return fail('remedy_outcome_time_invalid');
|
|
1164
|
+
const verified = await invoke(verifyRemedyOutcome, {
|
|
1165
|
+
evidence: clone(value.evidence), outcome: value.outcome,
|
|
1166
|
+
expected: outcomeExpected(state, attempt), reconciliation: false,
|
|
1167
|
+
});
|
|
1168
|
+
if (!verified || verified.ok !== true || !exactKeys(verified, VERIFIED_OUTCOME_KEYS)) {
|
|
1169
|
+
return fail('remedy_outcome_invalid');
|
|
1170
|
+
}
|
|
1171
|
+
if (!outcomeBound(verified, value, attempt)) {
|
|
1172
|
+
return fail('remedy_outcome_binding_mismatch');
|
|
1173
|
+
}
|
|
1174
|
+
if (value.outcome === 'executed' && attempt.units > state.remaining_units) {
|
|
1175
|
+
return fail('remedy_limit_exceeded');
|
|
1176
|
+
}
|
|
1177
|
+
const next = touch(state, at);
|
|
1178
|
+
const completed = next.active_remedy;
|
|
1179
|
+
completed.status = value.outcome;
|
|
1180
|
+
completed.outcome = value.outcome;
|
|
1181
|
+
completed.outcome_evidence = withoutOk(verified);
|
|
1182
|
+
completed.finalize_request_digest = requestDigest;
|
|
1183
|
+
consumeEvidence(next, verified.evidence_id, verified.evidence_digest);
|
|
1184
|
+
if (value.outcome === 'indeterminate') {
|
|
1185
|
+
next.status = 'remedy_indeterminate';
|
|
1186
|
+
}
|
|
1187
|
+
else {
|
|
1188
|
+
next.active_remedy = null;
|
|
1189
|
+
next.remedies.push(completed);
|
|
1190
|
+
if (value.outcome === 'executed') {
|
|
1191
|
+
next.remedied_units += completed.units;
|
|
1192
|
+
next.remaining_units -= completed.units;
|
|
1193
|
+
next.status = next.remaining_units === 0 ? 'remedied' : 'partially_remedied';
|
|
1194
|
+
}
|
|
1195
|
+
else {
|
|
1196
|
+
next.status = 'disputed';
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
const committed = await commit(state, next);
|
|
1200
|
+
return committed.ok ? pass({ ok: true, state: committed.state }) : committed;
|
|
1201
|
+
}
|
|
1202
|
+
async function reconcileRemedy(input) {
|
|
1203
|
+
const parsed = operationInput(input, RECONCILE_KEYS, 'remedy_reconciliation_input_invalid');
|
|
1204
|
+
if (isFailure(parsed))
|
|
1205
|
+
return parsed;
|
|
1206
|
+
const value = parsed;
|
|
1207
|
+
if (!validContext(value.tenantId) || !validId(value.instanceId)
|
|
1208
|
+
|| !validId(value.remedyOperationId)
|
|
1209
|
+
|| !['executed', 'proved_no_effect'].includes(value.outcome)
|
|
1210
|
+
|| !exactKeys(value.evidence, OUTCOME_EVIDENCE_KEYS)
|
|
1211
|
+
|| !validEvidencePair(value.evidence.evidence_id, value.evidence.evidence_digest)
|
|
1212
|
+
|| !Number.isFinite(instant(value.evidence.observed_at))) {
|
|
1213
|
+
return fail('remedy_reconciliation_input_invalid');
|
|
1214
|
+
}
|
|
1215
|
+
const loaded = await load(value.tenantId, value.instanceId);
|
|
1216
|
+
if (!loaded.ok)
|
|
1217
|
+
return loaded;
|
|
1218
|
+
const state = loaded.state;
|
|
1219
|
+
const requestDigest = digest(value);
|
|
1220
|
+
const prior = allAttempts(state).find((attempt) => attempt.remedy_operation_id === value.remedyOperationId);
|
|
1221
|
+
if (prior?.reconcile_request_digest === requestDigest) {
|
|
1222
|
+
return pass({ ok: true, idempotent: true, state });
|
|
1223
|
+
}
|
|
1224
|
+
if (state.status !== 'remedy_indeterminate' || !state.active_remedy
|
|
1225
|
+
|| state.active_remedy.status !== 'indeterminate') {
|
|
1226
|
+
return prior ? fail('remedy_reconciliation_conflict') : fail('remedy_not_indeterminate');
|
|
1227
|
+
}
|
|
1228
|
+
const attempt = state.active_remedy;
|
|
1229
|
+
if (attempt.remedy_operation_id !== value.remedyOperationId)
|
|
1230
|
+
return fail('remedy_operation_mismatch');
|
|
1231
|
+
if (evidenceUsed(state, value.evidence.evidence_id, value.evidence.evidence_digest)) {
|
|
1232
|
+
return fail('evidence_replayed');
|
|
1233
|
+
}
|
|
1234
|
+
const at = transitionTime(state);
|
|
1235
|
+
if (typeof at !== 'number')
|
|
1236
|
+
return at;
|
|
1237
|
+
if (instant(value.evidence.observed_at) < instant(attempt.outcome_evidence.observed_at)
|
|
1238
|
+
|| instant(value.evidence.observed_at) > at)
|
|
1239
|
+
return fail('remedy_reconciliation_time_invalid');
|
|
1240
|
+
const verified = await invoke(verifyRemedyOutcome, {
|
|
1241
|
+
evidence: clone(value.evidence), outcome: value.outcome,
|
|
1242
|
+
expected: outcomeExpected(state, attempt), reconciliation: true,
|
|
1243
|
+
});
|
|
1244
|
+
if (!verified || verified.ok !== true || !exactKeys(verified, VERIFIED_OUTCOME_KEYS)) {
|
|
1245
|
+
return fail('remedy_reconciliation_invalid');
|
|
1246
|
+
}
|
|
1247
|
+
if (!outcomeBound(verified, value, attempt)) {
|
|
1248
|
+
return fail('remedy_reconciliation_binding_mismatch');
|
|
1249
|
+
}
|
|
1250
|
+
if (value.outcome === 'executed' && attempt.units > state.remaining_units) {
|
|
1251
|
+
return fail('remedy_limit_exceeded');
|
|
1252
|
+
}
|
|
1253
|
+
const next = touch(state, at);
|
|
1254
|
+
const completed = next.active_remedy;
|
|
1255
|
+
completed.status = value.outcome;
|
|
1256
|
+
completed.outcome = value.outcome;
|
|
1257
|
+
completed.reconciliation = withoutOk(verified);
|
|
1258
|
+
completed.reconcile_request_digest = requestDigest;
|
|
1259
|
+
next.active_remedy = null;
|
|
1260
|
+
next.remedies.push(completed);
|
|
1261
|
+
consumeEvidence(next, verified.evidence_id, verified.evidence_digest);
|
|
1262
|
+
if (value.outcome === 'executed') {
|
|
1263
|
+
next.remedied_units += completed.units;
|
|
1264
|
+
next.remaining_units -= completed.units;
|
|
1265
|
+
next.status = next.remaining_units === 0 ? 'remedied' : 'partially_remedied';
|
|
1266
|
+
}
|
|
1267
|
+
else {
|
|
1268
|
+
next.status = 'disputed';
|
|
1269
|
+
}
|
|
1270
|
+
const committed = await commit(state, next);
|
|
1271
|
+
return committed.ok ? pass({ ok: true, state: committed.state }) : committed;
|
|
1272
|
+
}
|
|
1273
|
+
async function resolveDispute(input) {
|
|
1274
|
+
const parsed = operationInput(input, RESOLUTION_INPUT_KEYS, 'resolution_input_invalid');
|
|
1275
|
+
if (isFailure(parsed))
|
|
1276
|
+
return parsed;
|
|
1277
|
+
const value = parsed;
|
|
1278
|
+
if (!validContext(value.tenantId) || !validId(value.instanceId)
|
|
1279
|
+
|| !exactKeys(value.resolution, RESOLUTION_KEYS)
|
|
1280
|
+
|| !validEvidencePair(value.resolution.evidence_id, value.resolution.evidence_digest)
|
|
1281
|
+
|| value.resolution.outcome !== 'no_remedy'
|
|
1282
|
+
|| !Number.isFinite(instant(value.resolution.resolved_at))) {
|
|
1283
|
+
return fail('resolution_input_invalid');
|
|
1284
|
+
}
|
|
1285
|
+
const loaded = await load(value.tenantId, value.instanceId);
|
|
1286
|
+
if (!loaded.ok)
|
|
1287
|
+
return loaded;
|
|
1288
|
+
const state = loaded.state;
|
|
1289
|
+
const requestDigest = digest(value);
|
|
1290
|
+
if (state.resolution !== null) {
|
|
1291
|
+
return state.resolution.request_digest === requestDigest
|
|
1292
|
+
? pass({ ok: true, idempotent: true, state })
|
|
1293
|
+
: fail('remedy_case_terminal');
|
|
1294
|
+
}
|
|
1295
|
+
if (['remedied', 'resolved_no_remedy'].includes(state.status))
|
|
1296
|
+
return fail('remedy_case_terminal');
|
|
1297
|
+
if (!['disputed', 'partially_remedied'].includes(state.status)
|
|
1298
|
+
|| state.dispute === null || state.active_remedy !== null) {
|
|
1299
|
+
return fail('resolution_unavailable');
|
|
1300
|
+
}
|
|
1301
|
+
if (typeof verifyResolution !== 'function')
|
|
1302
|
+
return fail('resolution_verifier_unavailable');
|
|
1303
|
+
if (evidenceUsed(state, value.resolution.evidence_id, value.resolution.evidence_digest)) {
|
|
1304
|
+
return fail('evidence_replayed');
|
|
1305
|
+
}
|
|
1306
|
+
const at = transitionTime(state);
|
|
1307
|
+
if (typeof at !== 'number')
|
|
1308
|
+
return at;
|
|
1309
|
+
if (instant(value.resolution.resolved_at) < instant(state.dispute.opened_at)
|
|
1310
|
+
|| instant(value.resolution.resolved_at) > at)
|
|
1311
|
+
return fail('resolution_time_invalid');
|
|
1312
|
+
const verified = await invoke(verifyResolution, {
|
|
1313
|
+
resolution: clone(value.resolution), expected: expectedContext(state),
|
|
1314
|
+
});
|
|
1315
|
+
if (!verified || verified.ok !== true || !exactKeys(verified, VERIFIED_RESOLUTION_KEYS)) {
|
|
1316
|
+
return fail('resolution_invalid');
|
|
1317
|
+
}
|
|
1318
|
+
if ([...RESOLUTION_KEYS].some((key) => !same(verified[key], value.resolution[key]))
|
|
1319
|
+
|| verified.dispute_id !== state.dispute.dispute_id) {
|
|
1320
|
+
return fail('resolution_binding_mismatch');
|
|
1321
|
+
}
|
|
1322
|
+
const next = touch(state, at);
|
|
1323
|
+
next.status = 'resolved_no_remedy';
|
|
1324
|
+
next.resolution = { ...withoutOk(verified), request_digest: requestDigest };
|
|
1325
|
+
consumeEvidence(next, verified.evidence_id, verified.evidence_digest);
|
|
1326
|
+
const committed = await commit(state, next);
|
|
1327
|
+
return committed.ok ? pass({ ok: true, state: committed.state }) : committed;
|
|
1328
|
+
}
|
|
1329
|
+
async function status(input) {
|
|
1330
|
+
const parsed = operationInput(input, new Set(['tenantId', 'instanceId']), 'remedy_lookup_invalid');
|
|
1331
|
+
if (isFailure(parsed))
|
|
1332
|
+
return parsed;
|
|
1333
|
+
const value = parsed;
|
|
1334
|
+
return load(value.tenantId, value.instanceId);
|
|
1335
|
+
}
|
|
1336
|
+
return Object.freeze({
|
|
1337
|
+
create, status, recordRevocation, reconcileOriginalEffect, openDispute, authorizeRemedy,
|
|
1338
|
+
claimRemedy, finalizeRemedy, reconcileRemedy, resolveDispute,
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
export default {
|
|
1342
|
+
REMEDY_PROGRAM_VERSION,
|
|
1343
|
+
createRemedyMemoryStore,
|
|
1344
|
+
createRemedyProgramKernel,
|
|
1345
|
+
};
|
|
1346
|
+
//# sourceMappingURL=remedy-program.js.map
|