@emilia-protocol/gate 0.10.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 +91 -0
- package/README.md +272 -0
- package/action-control-manifest.js +3 -268
- package/action-escrow-custodian.js +4 -0
- package/action-escrow-evidence.js +4 -0
- package/action-escrow-package.js +4 -0
- package/action-escrow-postgres.js +4 -0
- package/action-escrow-state.js +4 -0
- package/action-escrow-verifiers.js +4 -0
- package/action-escrow.js +4 -0
- 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 +47 -4
- 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 -85
- 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,891 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* EP-ACTION-REMEDY-RECEIPT-v1
|
|
4
|
+
*
|
|
5
|
+
* A portable, offline-verifiable operator receipt over one exact Remedy
|
|
6
|
+
* Program state snapshot. The receipt preserves the original effect as an
|
|
7
|
+
* immutable fact and describes a later remedy only as a compensating action.
|
|
8
|
+
* It never claims that the original effect was rolled back or erased.
|
|
9
|
+
*/
|
|
10
|
+
import crypto from 'node:crypto';
|
|
11
|
+
|
|
12
|
+
import { canonicalize } from '../execution-binding.js';
|
|
13
|
+
|
|
14
|
+
export const ACTION_REMEDY_RECEIPT_VERSION = 'EP-ACTION-REMEDY-RECEIPT-v1';
|
|
15
|
+
export const REMEDY_PROGRAM_RECEIPT_VERSION = ACTION_REMEDY_RECEIPT_VERSION;
|
|
16
|
+
export const ACTION_REMEDY_RECEIPT_DOMAIN = `${ACTION_REMEDY_RECEIPT_VERSION}\0`;
|
|
17
|
+
|
|
18
|
+
const REMEDY_PROGRAM_VERSION = 'EP-GATE-REMEDY-PROGRAM-PROFILE-v1';
|
|
19
|
+
const DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
20
|
+
const CAID = /^caid:1:[a-z][a-z0-9.-]*\.[1-9][0-9]*:jcs-sha256:[A-Za-z0-9_-]{43}$/;
|
|
21
|
+
const ID = /^[A-Za-z0-9][A-Za-z0-9._:/#@+-]{0,255}$/;
|
|
22
|
+
const BASE64URL = /^[A-Za-z0-9_-]+$/;
|
|
23
|
+
const FORBIDDEN_FIELD_NAMES = new Set(['__proto__', 'prototype', 'constructor']);
|
|
24
|
+
const MAX_CONTEXT_BYTES = 512;
|
|
25
|
+
const MAX_REMEDIES = 1024;
|
|
26
|
+
|
|
27
|
+
const TOP_KEYS = new Set(['version', 'issuer', 'payload', 'content_digest', 'signature']);
|
|
28
|
+
const ISSUER_KEYS = new Set(['issuer', 'tenant', 'environment', 'audience', 'key_id']);
|
|
29
|
+
const PAYLOAD_KEYS = new Set([
|
|
30
|
+
'case', 'original_effect', 'original_reconciliation', 'remedy', 'semantics',
|
|
31
|
+
]);
|
|
32
|
+
const CASE_KEYS = new Set([
|
|
33
|
+
'instance_id', 'revision', 'status', 'state_snapshot_digest', 'updated_at',
|
|
34
|
+
]);
|
|
35
|
+
const ORIGINAL_KEYS = new Set([
|
|
36
|
+
'caid', 'action_digest', 'operation_id', 'consequence_mode',
|
|
37
|
+
'consequence_digest', 'terminal_evidence_digest', 'outcome', 'occurred_at',
|
|
38
|
+
'evidence_digest',
|
|
39
|
+
]);
|
|
40
|
+
const REMEDY_KEYS = new Set([
|
|
41
|
+
'operation_id', 'caid', 'action_digest', 'destination_binding_digest',
|
|
42
|
+
'units', 'unit', 'owner_mode', 'owner_digest', 'status', 'outcome',
|
|
43
|
+
]);
|
|
44
|
+
const SEMANTICS_KEYS = new Set(['original_effect', 'remedy_effect', 'rollback']);
|
|
45
|
+
const SIGNATURE_KEYS = new Set(['algorithm', 'value']);
|
|
46
|
+
const EXPECTED_KEYS = new Set([
|
|
47
|
+
'original_operation_id', 'original_action_digest',
|
|
48
|
+
'original_terminal_evidence_digest', 'case_instance_id', 'case_revision',
|
|
49
|
+
'case_status', 'remedy_operation_id', 'remedy_action_digest', 'remedy_caid',
|
|
50
|
+
'destination_binding_digest', 'units', 'unit', 'owner_mode', 'owner_digest',
|
|
51
|
+
]);
|
|
52
|
+
const STATE_KEYS = new Set([
|
|
53
|
+
'version', 'instance_id', 'tenant_id', 'environment', 'audience', 'status',
|
|
54
|
+
'revision', 'created_at', 'updated_at', 'original', 'remedy_profile_digest',
|
|
55
|
+
'destination_binding_digest', 'max_remedy_units', 'unit', 'remedied_units',
|
|
56
|
+
'remaining_units', 'used_evidence_ids', 'used_evidence_digests',
|
|
57
|
+
'original_reconciliation', 'revocation', 'dispute', 'active_remedy',
|
|
58
|
+
'remedies', 'resolution',
|
|
59
|
+
'create_request_digest',
|
|
60
|
+
]);
|
|
61
|
+
const ATTEMPT_KEYS = new Set([
|
|
62
|
+
'evidence_id', 'evidence_digest', 'dispute_id', 'original_operation_id',
|
|
63
|
+
'remedy_operation_id', 'remedy_caid', 'remedy_action_digest',
|
|
64
|
+
'consequence_mode', 'capability_template_digest', 'escrow_profile_digest',
|
|
65
|
+
'destination_binding_digest', 'units', 'unit', 'authorized_at',
|
|
66
|
+
'request_digest', 'status', 'claim_token_digest', 'claimed_at',
|
|
67
|
+
'claim_request_digest', 'outcome', 'outcome_evidence',
|
|
68
|
+
'finalize_request_digest', 'reconciliation', 'reconcile_request_digest',
|
|
69
|
+
]);
|
|
70
|
+
const OUTCOME_EVIDENCE_KEYS = new Set([
|
|
71
|
+
'evidence_id', 'evidence_digest', 'remedy_operation_id',
|
|
72
|
+
'remedy_action_digest', 'destination_binding_digest', 'units', 'unit',
|
|
73
|
+
'outcome', 'observed_at',
|
|
74
|
+
]);
|
|
75
|
+
const DISPUTE_KEYS = new Set([
|
|
76
|
+
'dispute_id', 'evidence_id', 'evidence_digest', 'challenger_id',
|
|
77
|
+
'requested_units', 'opened_at', 'original_operation_id',
|
|
78
|
+
'original_action_digest', 'request_digest',
|
|
79
|
+
]);
|
|
80
|
+
const REVOCATION_KEYS = new Set([
|
|
81
|
+
'evidence_id', 'evidence_digest', 'target_operation_id', 'action_digest',
|
|
82
|
+
'authority_id', 'revoked_at', 'effect', 'request_digest',
|
|
83
|
+
]);
|
|
84
|
+
const RESOLUTION_KEYS = new Set([
|
|
85
|
+
'evidence_id', 'evidence_digest', 'outcome', 'resolved_at', 'dispute_id',
|
|
86
|
+
'request_digest',
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
const CASE_STATUSES = new Set([
|
|
90
|
+
'effect_executed', 'effect_indeterminate', 'disputed', 'remedy_authorized',
|
|
91
|
+
'remedy_claimed', 'remedy_indeterminate', 'partially_remedied', 'remedied',
|
|
92
|
+
'resolved_no_remedy',
|
|
93
|
+
'original_proved_no_effect',
|
|
94
|
+
]);
|
|
95
|
+
const REMEDY_STATUSES = new Set([
|
|
96
|
+
'authorized', 'claimed', 'indeterminate', 'executed', 'proved_no_effect',
|
|
97
|
+
]);
|
|
98
|
+
const REMEDY_OUTCOMES = new Set(['executed', 'proved_no_effect', 'indeterminate']);
|
|
99
|
+
|
|
100
|
+
type DataRecord = Record<string, any>;
|
|
101
|
+
|
|
102
|
+
export interface RemedyReceiptExpectedBindings extends Record<string, unknown> {
|
|
103
|
+
original_operation_id: string;
|
|
104
|
+
original_action_digest: string;
|
|
105
|
+
original_terminal_evidence_digest: string;
|
|
106
|
+
case_instance_id: string;
|
|
107
|
+
case_revision: number;
|
|
108
|
+
case_status: string;
|
|
109
|
+
remedy_operation_id: string;
|
|
110
|
+
remedy_action_digest: string;
|
|
111
|
+
remedy_caid: string;
|
|
112
|
+
destination_binding_digest: string;
|
|
113
|
+
units: number;
|
|
114
|
+
unit: string;
|
|
115
|
+
owner_mode: string;
|
|
116
|
+
owner_digest: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isRecord(value: unknown): value is DataRecord {
|
|
120
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
121
|
+
const prototype = Object.getPrototypeOf(value);
|
|
122
|
+
return prototype === Object.prototype || prototype === null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isDataRecord(value: unknown): value is DataRecord {
|
|
126
|
+
return isRecord(value) && Reflect.ownKeys(value).every((key) => {
|
|
127
|
+
if (typeof key !== 'string') return false;
|
|
128
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
129
|
+
return descriptor?.enumerable === true && Object.hasOwn(descriptor, 'value');
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function exactKeys(value: unknown, keys: Set<string>): value is DataRecord {
|
|
134
|
+
return isDataRecord(value)
|
|
135
|
+
&& Reflect.ownKeys(value).length === keys.size
|
|
136
|
+
&& Object.keys(value).every((key) => keys.has(key));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function assertNoPrototypeNamedFields(value: unknown): void {
|
|
140
|
+
if (value === null || typeof value !== 'object') return;
|
|
141
|
+
const stack: object[] = [value as object];
|
|
142
|
+
const seen = new WeakSet<object>();
|
|
143
|
+
while (stack.length > 0) {
|
|
144
|
+
const current = stack.pop()!;
|
|
145
|
+
if (seen.has(current)) continue;
|
|
146
|
+
seen.add(current);
|
|
147
|
+
for (const key of Reflect.ownKeys(current)) {
|
|
148
|
+
if (typeof key === 'string' && FORBIDDEN_FIELD_NAMES.has(key)) {
|
|
149
|
+
throw new TypeError(`prototype-named field ${key} is forbidden`);
|
|
150
|
+
}
|
|
151
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
152
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value')) continue;
|
|
153
|
+
const child = descriptor.value;
|
|
154
|
+
if (typeof child === 'number' && Object.is(child, -0)) {
|
|
155
|
+
throw new TypeError('negative zero is not canonical JSON');
|
|
156
|
+
}
|
|
157
|
+
if (child !== null && typeof child === 'object') stack.push(child);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function canonicalCopy<T>(value: T, label: string): T {
|
|
163
|
+
try {
|
|
164
|
+
assertNoPrototypeNamedFields(value);
|
|
165
|
+
return JSON.parse(canonicalize(value)) as T;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (error instanceof TypeError && /prototype-named field/.test(error.message)) throw error;
|
|
168
|
+
throw new TypeError(`${label} must be bounded canonical JSON`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function deepFreeze<T>(value: T): Readonly<T> {
|
|
173
|
+
if (value === null || typeof value !== 'object') return value;
|
|
174
|
+
const stack: object[] = [value as object];
|
|
175
|
+
const seen = new WeakSet<object>();
|
|
176
|
+
while (stack.length > 0) {
|
|
177
|
+
const current = stack.pop()!;
|
|
178
|
+
if (seen.has(current)) continue;
|
|
179
|
+
seen.add(current);
|
|
180
|
+
for (const child of Object.values(current)) {
|
|
181
|
+
if (child !== null && typeof child === 'object') stack.push(child);
|
|
182
|
+
}
|
|
183
|
+
Object.freeze(current);
|
|
184
|
+
}
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function validId(value: unknown): value is string {
|
|
189
|
+
return typeof value === 'string' && ID.test(value)
|
|
190
|
+
&& !FORBIDDEN_FIELD_NAMES.has(value);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function validContext(value: unknown): value is string {
|
|
194
|
+
return typeof value === 'string' && value.length > 0
|
|
195
|
+
&& Buffer.byteLength(value, 'utf8') <= MAX_CONTEXT_BYTES
|
|
196
|
+
&& !/[\u0000-\u001f\u007f]/.test(value)
|
|
197
|
+
&& !FORBIDDEN_FIELD_NAMES.has(value);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function strictInstant(value: unknown): number {
|
|
201
|
+
if (typeof value !== 'string'
|
|
202
|
+
|| !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)) return NaN;
|
|
203
|
+
const parsed = Date.parse(value);
|
|
204
|
+
return Number.isFinite(parsed) && new Date(parsed).toISOString() === value ? parsed : NaN;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function canonicalDigest(value: unknown): string {
|
|
208
|
+
return `sha256:${crypto.createHash('sha256').update(canonicalize(value)).digest('hex')}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function strictBase64url(value: unknown, length?: number): Buffer | null {
|
|
212
|
+
if (typeof value !== 'string' || !BASE64URL.test(value) || value.length % 4 === 1) return null;
|
|
213
|
+
const bytes = Buffer.from(value, 'base64url');
|
|
214
|
+
if (bytes.toString('base64url') !== value
|
|
215
|
+
|| (length !== undefined && bytes.length !== length)) return null;
|
|
216
|
+
return bytes;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function publicKey(value: unknown): crypto.KeyObject | null {
|
|
220
|
+
try {
|
|
221
|
+
if (value instanceof crypto.KeyObject) {
|
|
222
|
+
return value.type === 'public' && value.asymmetricKeyType === 'ed25519' ? value : null;
|
|
223
|
+
}
|
|
224
|
+
const bytes = strictBase64url(value);
|
|
225
|
+
if (!bytes) return null;
|
|
226
|
+
const key = crypto.createPublicKey({ key: bytes, type: 'spki', format: 'der' });
|
|
227
|
+
return key.asymmetricKeyType === 'ed25519' ? key : null;
|
|
228
|
+
} catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function privateKey(value: unknown): crypto.KeyObject {
|
|
234
|
+
try {
|
|
235
|
+
const key = value instanceof crypto.KeyObject ? value : crypto.createPrivateKey(value as any);
|
|
236
|
+
if (key.type !== 'private' || key.asymmetricKeyType !== 'ed25519') throw new Error('wrong key');
|
|
237
|
+
return key;
|
|
238
|
+
} catch {
|
|
239
|
+
throw new TypeError('remedy receipt private key must be Ed25519');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function publicKeyB64u(value: crypto.KeyObject): string {
|
|
244
|
+
return crypto.createPublicKey(value as any)
|
|
245
|
+
.export({ type: 'spki', format: 'der' }).toString('base64url');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function canonicalSignature(value: unknown): { bytes: Buffer; encoded: string } {
|
|
249
|
+
const bytes = Buffer.isBuffer(value)
|
|
250
|
+
? Buffer.from(value)
|
|
251
|
+
: value instanceof Uint8Array ? Buffer.from(value)
|
|
252
|
+
: strictBase64url(value, 64);
|
|
253
|
+
if (!bytes || bytes.length !== 64) {
|
|
254
|
+
throw new TypeError('remedy receipt signer returned a malformed Ed25519 signature');
|
|
255
|
+
}
|
|
256
|
+
const encoded = bytes.toString('base64url');
|
|
257
|
+
if (typeof value === 'string' && value !== encoded) {
|
|
258
|
+
throw new TypeError('remedy receipt signer returned a noncanonical signature');
|
|
259
|
+
}
|
|
260
|
+
return { bytes, encoded };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function validOriginal(value: unknown): value is DataRecord {
|
|
264
|
+
return exactKeys(value, ORIGINAL_KEYS)
|
|
265
|
+
&& typeof value.caid === 'string' && CAID.test(value.caid)
|
|
266
|
+
&& typeof value.action_digest === 'string' && DIGEST.test(value.action_digest)
|
|
267
|
+
&& validId(value.operation_id)
|
|
268
|
+
&& validContext(value.consequence_mode)
|
|
269
|
+
&& typeof value.consequence_digest === 'string' && DIGEST.test(value.consequence_digest)
|
|
270
|
+
&& typeof value.terminal_evidence_digest === 'string'
|
|
271
|
+
&& DIGEST.test(value.terminal_evidence_digest)
|
|
272
|
+
&& ['executed', 'indeterminate'].includes(value.outcome)
|
|
273
|
+
&& Number.isFinite(strictInstant(value.occurred_at))
|
|
274
|
+
&& typeof value.evidence_digest === 'string' && DIGEST.test(value.evidence_digest);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function validOutcomeEvidence(value: unknown): value is DataRecord {
|
|
278
|
+
return exactKeys(value, OUTCOME_EVIDENCE_KEYS)
|
|
279
|
+
&& validId(value.evidence_id)
|
|
280
|
+
&& typeof value.evidence_digest === 'string' && DIGEST.test(value.evidence_digest)
|
|
281
|
+
&& validId(value.remedy_operation_id)
|
|
282
|
+
&& typeof value.remedy_action_digest === 'string' && DIGEST.test(value.remedy_action_digest)
|
|
283
|
+
&& typeof value.destination_binding_digest === 'string'
|
|
284
|
+
&& DIGEST.test(value.destination_binding_digest)
|
|
285
|
+
&& Number.isSafeInteger(value.units) && value.units > 0
|
|
286
|
+
&& validContext(value.unit)
|
|
287
|
+
&& REMEDY_OUTCOMES.has(value.outcome)
|
|
288
|
+
&& Number.isFinite(strictInstant(value.observed_at));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function ownerDigest(value: DataRecord): string | null {
|
|
292
|
+
if (value.consequence_mode === 'receipt-program'
|
|
293
|
+
&& typeof value.capability_template_digest === 'string'
|
|
294
|
+
&& DIGEST.test(value.capability_template_digest)
|
|
295
|
+
&& value.escrow_profile_digest === null) return value.capability_template_digest;
|
|
296
|
+
if (value.consequence_mode === 'action-escrow'
|
|
297
|
+
&& value.capability_template_digest === null
|
|
298
|
+
&& typeof value.escrow_profile_digest === 'string'
|
|
299
|
+
&& DIGEST.test(value.escrow_profile_digest)) return value.escrow_profile_digest;
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function validNullableDigest(value: unknown): boolean {
|
|
304
|
+
return value === null || (typeof value === 'string' && DIGEST.test(value));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function validAttempt(value: unknown): value is DataRecord {
|
|
308
|
+
return exactKeys(value, ATTEMPT_KEYS)
|
|
309
|
+
&& validId(value.evidence_id)
|
|
310
|
+
&& typeof value.evidence_digest === 'string' && DIGEST.test(value.evidence_digest)
|
|
311
|
+
&& validId(value.dispute_id) && validId(value.original_operation_id)
|
|
312
|
+
&& validId(value.remedy_operation_id)
|
|
313
|
+
&& typeof value.remedy_caid === 'string' && CAID.test(value.remedy_caid)
|
|
314
|
+
&& typeof value.remedy_action_digest === 'string' && DIGEST.test(value.remedy_action_digest)
|
|
315
|
+
&& ownerDigest(value) !== null
|
|
316
|
+
&& typeof value.destination_binding_digest === 'string'
|
|
317
|
+
&& DIGEST.test(value.destination_binding_digest)
|
|
318
|
+
&& Number.isSafeInteger(value.units) && value.units > 0
|
|
319
|
+
&& validContext(value.unit)
|
|
320
|
+
&& Number.isFinite(strictInstant(value.authorized_at))
|
|
321
|
+
&& typeof value.request_digest === 'string' && DIGEST.test(value.request_digest)
|
|
322
|
+
&& REMEDY_STATUSES.has(value.status)
|
|
323
|
+
&& validNullableDigest(value.claim_token_digest)
|
|
324
|
+
&& (value.claimed_at === null || Number.isFinite(strictInstant(value.claimed_at)))
|
|
325
|
+
&& validNullableDigest(value.claim_request_digest)
|
|
326
|
+
&& (value.outcome === null || REMEDY_OUTCOMES.has(value.outcome))
|
|
327
|
+
&& (value.outcome_evidence === null || validOutcomeEvidence(value.outcome_evidence))
|
|
328
|
+
&& validNullableDigest(value.finalize_request_digest)
|
|
329
|
+
&& (value.reconciliation === null || validOutcomeEvidence(value.reconciliation))
|
|
330
|
+
&& validNullableDigest(value.reconcile_request_digest);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function validDispute(value: unknown): boolean {
|
|
334
|
+
return exactKeys(value, DISPUTE_KEYS)
|
|
335
|
+
&& validId(value.dispute_id) && validId(value.evidence_id)
|
|
336
|
+
&& typeof value.evidence_digest === 'string' && DIGEST.test(value.evidence_digest)
|
|
337
|
+
&& validContext(value.challenger_id)
|
|
338
|
+
&& Number.isSafeInteger(value.requested_units) && value.requested_units > 0
|
|
339
|
+
&& Number.isFinite(strictInstant(value.opened_at))
|
|
340
|
+
&& validId(value.original_operation_id)
|
|
341
|
+
&& typeof value.original_action_digest === 'string' && DIGEST.test(value.original_action_digest)
|
|
342
|
+
&& typeof value.request_digest === 'string' && DIGEST.test(value.request_digest);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function validRevocation(value: unknown): boolean {
|
|
346
|
+
return exactKeys(value, REVOCATION_KEYS)
|
|
347
|
+
&& validId(value.evidence_id)
|
|
348
|
+
&& typeof value.evidence_digest === 'string' && DIGEST.test(value.evidence_digest)
|
|
349
|
+
&& validId(value.target_operation_id)
|
|
350
|
+
&& typeof value.action_digest === 'string' && DIGEST.test(value.action_digest)
|
|
351
|
+
&& validContext(value.authority_id)
|
|
352
|
+
&& Number.isFinite(strictInstant(value.revoked_at))
|
|
353
|
+
&& value.effect === 'future_authority_only'
|
|
354
|
+
&& typeof value.request_digest === 'string' && DIGEST.test(value.request_digest);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function validResolution(value: unknown): boolean {
|
|
358
|
+
return exactKeys(value, RESOLUTION_KEYS)
|
|
359
|
+
&& validId(value.evidence_id)
|
|
360
|
+
&& typeof value.evidence_digest === 'string' && DIGEST.test(value.evidence_digest)
|
|
361
|
+
&& value.outcome === 'no_remedy'
|
|
362
|
+
&& Number.isFinite(strictInstant(value.resolved_at))
|
|
363
|
+
&& validId(value.dispute_id)
|
|
364
|
+
&& typeof value.request_digest === 'string' && DIGEST.test(value.request_digest);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function validOriginalReconciliation(value: unknown): boolean {
|
|
368
|
+
return exactKeys(value, new Set([
|
|
369
|
+
'evidence_id', 'evidence_digest', 'original_operation_id',
|
|
370
|
+
'original_action_digest', 'terminal_evidence_digest', 'outcome',
|
|
371
|
+
'observed_at', 'request_digest',
|
|
372
|
+
]))
|
|
373
|
+
&& validId(value.evidence_id)
|
|
374
|
+
&& typeof value.evidence_digest === 'string' && DIGEST.test(value.evidence_digest)
|
|
375
|
+
&& validId(value.original_operation_id)
|
|
376
|
+
&& typeof value.original_action_digest === 'string' && DIGEST.test(value.original_action_digest)
|
|
377
|
+
&& typeof value.terminal_evidence_digest === 'string' && DIGEST.test(value.terminal_evidence_digest)
|
|
378
|
+
&& ['executed', 'proved_no_effect'].includes(value.outcome)
|
|
379
|
+
&& Number.isFinite(strictInstant(value.observed_at))
|
|
380
|
+
&& typeof value.request_digest === 'string' && DIGEST.test(value.request_digest);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function validState(state: unknown): state is DataRecord {
|
|
384
|
+
if (!exactKeys(state, STATE_KEYS)
|
|
385
|
+
|| state.version !== REMEDY_PROGRAM_VERSION
|
|
386
|
+
|| !validId(state.instance_id)
|
|
387
|
+
|| !validContext(state.tenant_id)
|
|
388
|
+
|| !validContext(state.environment)
|
|
389
|
+
|| !validContext(state.audience)
|
|
390
|
+
|| !CASE_STATUSES.has(state.status)
|
|
391
|
+
|| !Number.isSafeInteger(state.revision) || state.revision < 0
|
|
392
|
+
|| !Number.isFinite(strictInstant(state.created_at))
|
|
393
|
+
|| !Number.isFinite(strictInstant(state.updated_at))
|
|
394
|
+
|| strictInstant(state.updated_at) < strictInstant(state.created_at)
|
|
395
|
+
|| !validOriginal(state.original)
|
|
396
|
+
|| typeof state.remedy_profile_digest !== 'string' || !DIGEST.test(state.remedy_profile_digest)
|
|
397
|
+
|| typeof state.destination_binding_digest !== 'string'
|
|
398
|
+
|| !DIGEST.test(state.destination_binding_digest)
|
|
399
|
+
|| !Number.isSafeInteger(state.max_remedy_units) || state.max_remedy_units < 1
|
|
400
|
+
|| !validContext(state.unit)
|
|
401
|
+
|| !Number.isSafeInteger(state.remedied_units) || state.remedied_units < 0
|
|
402
|
+
|| !Number.isSafeInteger(state.remaining_units) || state.remaining_units < 0
|
|
403
|
+
|| state.remedied_units + state.remaining_units !== state.max_remedy_units
|
|
404
|
+
|| !Array.isArray(state.used_evidence_ids)
|
|
405
|
+
|| !state.used_evidence_ids.every(validId)
|
|
406
|
+
|| new Set(state.used_evidence_ids).size !== state.used_evidence_ids.length
|
|
407
|
+
|| !Array.isArray(state.used_evidence_digests)
|
|
408
|
+
|| !state.used_evidence_digests.every((entry: unknown) => (
|
|
409
|
+
typeof entry === 'string' && DIGEST.test(entry)
|
|
410
|
+
))
|
|
411
|
+
|| new Set(state.used_evidence_digests).size !== state.used_evidence_digests.length
|
|
412
|
+
|| !Array.isArray(state.remedies) || state.remedies.length > MAX_REMEDIES
|
|
413
|
+
|| !state.remedies.every(validAttempt)
|
|
414
|
+
|| (state.active_remedy !== null && !validAttempt(state.active_remedy))
|
|
415
|
+
|| (state.dispute !== null && !validDispute(state.dispute))
|
|
416
|
+
|| (state.revocation !== null && !validRevocation(state.revocation))
|
|
417
|
+
|| (state.original_reconciliation !== null
|
|
418
|
+
&& !validOriginalReconciliation(state.original_reconciliation))
|
|
419
|
+
|| (state.resolution !== null && !validResolution(state.resolution))
|
|
420
|
+
|| typeof state.create_request_digest !== 'string'
|
|
421
|
+
|| !DIGEST.test(state.create_request_digest)) return false;
|
|
422
|
+
|
|
423
|
+
if (state.original_reconciliation !== null
|
|
424
|
+
&& (state.original.outcome !== 'indeterminate'
|
|
425
|
+
|| state.original_reconciliation.original_operation_id !== state.original.operation_id
|
|
426
|
+
|| state.original_reconciliation.original_action_digest !== state.original.action_digest
|
|
427
|
+
|| state.original_reconciliation.terminal_evidence_digest !== state.original.terminal_evidence_digest
|
|
428
|
+
|| strictInstant(state.original_reconciliation.observed_at) < strictInstant(state.original.occurred_at))) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const attempts = [
|
|
433
|
+
...(state.active_remedy === null ? [] : [state.active_remedy]),
|
|
434
|
+
...state.remedies,
|
|
435
|
+
];
|
|
436
|
+
return attempts.every((attempt: DataRecord) => (
|
|
437
|
+
attempt.original_operation_id === state.original.operation_id
|
|
438
|
+
&& attempt.destination_binding_digest === state.destination_binding_digest
|
|
439
|
+
&& attempt.unit === state.unit
|
|
440
|
+
));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function selectAttempt(state: DataRecord, remedyOperationId: string): DataRecord {
|
|
444
|
+
const attempts = [
|
|
445
|
+
...(state.active_remedy === null ? [] : [state.active_remedy]),
|
|
446
|
+
...state.remedies,
|
|
447
|
+
].filter((attempt: DataRecord) => attempt.remedy_operation_id === remedyOperationId);
|
|
448
|
+
if (attempts.length === 0) throw new TypeError('remedy operation is absent from the state snapshot');
|
|
449
|
+
if (attempts.length !== 1) throw new TypeError('remedy operation is not unique in the state snapshot');
|
|
450
|
+
const attempt = attempts[0];
|
|
451
|
+
if (attempt.remedy_operation_id === state.original.operation_id
|
|
452
|
+
|| attempt.remedy_action_digest === state.original.action_digest) {
|
|
453
|
+
throw new TypeError('a remedy receipt must describe a compensating action, never rollback');
|
|
454
|
+
}
|
|
455
|
+
return attempt;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function expectedBindings(state: DataRecord, attempt: DataRecord): RemedyReceiptExpectedBindings {
|
|
459
|
+
return {
|
|
460
|
+
original_operation_id: state.original.operation_id,
|
|
461
|
+
original_action_digest: state.original.action_digest,
|
|
462
|
+
original_terminal_evidence_digest: state.original.terminal_evidence_digest,
|
|
463
|
+
case_instance_id: state.instance_id,
|
|
464
|
+
case_revision: state.revision,
|
|
465
|
+
case_status: state.status,
|
|
466
|
+
remedy_operation_id: attempt.remedy_operation_id,
|
|
467
|
+
remedy_action_digest: attempt.remedy_action_digest,
|
|
468
|
+
remedy_caid: attempt.remedy_caid,
|
|
469
|
+
destination_binding_digest: attempt.destination_binding_digest,
|
|
470
|
+
units: attempt.units,
|
|
471
|
+
unit: attempt.unit,
|
|
472
|
+
owner_mode: attempt.consequence_mode,
|
|
473
|
+
owner_digest: ownerDigest(attempt)!,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function snapshotAndAttempt(
|
|
478
|
+
value: unknown,
|
|
479
|
+
remedyOperationId: unknown,
|
|
480
|
+
): { state: DataRecord; attempt: DataRecord } {
|
|
481
|
+
const state = canonicalCopy(value, 'remedy state snapshot') as unknown;
|
|
482
|
+
if (!validState(state)) throw new TypeError('remedy state snapshot is invalid');
|
|
483
|
+
if (!validId(remedyOperationId)) throw new TypeError('remedyOperationId is invalid');
|
|
484
|
+
return { state, attempt: selectAttempt(state, remedyOperationId) };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Derive every relying-party binding that must be independently expected. */
|
|
488
|
+
export function expectedRemedyProgramReceiptBindings(
|
|
489
|
+
state: unknown,
|
|
490
|
+
remedyOperationId: string,
|
|
491
|
+
): Readonly<RemedyReceiptExpectedBindings> {
|
|
492
|
+
const selected = snapshotAndAttempt(state, remedyOperationId);
|
|
493
|
+
return deepFreeze(expectedBindings(selected.state, selected.attempt));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function payloadFor(state: DataRecord, attempt: DataRecord): DataRecord {
|
|
497
|
+
return {
|
|
498
|
+
case: {
|
|
499
|
+
instance_id: state.instance_id,
|
|
500
|
+
revision: state.revision,
|
|
501
|
+
status: state.status,
|
|
502
|
+
state_snapshot_digest: canonicalDigest(state),
|
|
503
|
+
updated_at: state.updated_at,
|
|
504
|
+
},
|
|
505
|
+
original_effect: canonicalCopy(state.original, 'original effect'),
|
|
506
|
+
original_reconciliation: state.original_reconciliation === null
|
|
507
|
+
? null : canonicalCopy(state.original_reconciliation, 'original reconciliation'),
|
|
508
|
+
remedy: {
|
|
509
|
+
operation_id: attempt.remedy_operation_id,
|
|
510
|
+
caid: attempt.remedy_caid,
|
|
511
|
+
action_digest: attempt.remedy_action_digest,
|
|
512
|
+
destination_binding_digest: attempt.destination_binding_digest,
|
|
513
|
+
units: attempt.units,
|
|
514
|
+
unit: attempt.unit,
|
|
515
|
+
owner_mode: attempt.consequence_mode,
|
|
516
|
+
owner_digest: ownerDigest(attempt),
|
|
517
|
+
status: attempt.status,
|
|
518
|
+
outcome: attempt.outcome,
|
|
519
|
+
},
|
|
520
|
+
semantics: {
|
|
521
|
+
original_effect: 'immutable_fact',
|
|
522
|
+
remedy_effect: 'compensating_action',
|
|
523
|
+
rollback: false,
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function contentBody(receipt: DataRecord): DataRecord {
|
|
529
|
+
return {
|
|
530
|
+
version: receipt.version,
|
|
531
|
+
issuer: receipt.issuer,
|
|
532
|
+
payload: receipt.payload,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function signingBody(receipt: DataRecord): DataRecord {
|
|
537
|
+
return {
|
|
538
|
+
...contentBody(receipt),
|
|
539
|
+
content_digest: receipt.content_digest,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** Return the exact domain-separated canonical bytes signed by Ed25519. */
|
|
544
|
+
export function remedyProgramReceiptSigningBytes(receipt: unknown): Buffer {
|
|
545
|
+
if (!isDataRecord(receipt)) throw new TypeError('remedy receipt signing body is invalid');
|
|
546
|
+
return Buffer.from(
|
|
547
|
+
ACTION_REMEDY_RECEIPT_DOMAIN + canonicalize(signingBody(receipt)),
|
|
548
|
+
'utf8',
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function validIssuer(value: unknown): value is DataRecord {
|
|
553
|
+
return exactKeys(value, ISSUER_KEYS) && Object.values(value).every(validContext);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function validExpected(value: unknown): value is RemedyReceiptExpectedBindings {
|
|
557
|
+
return exactKeys(value, EXPECTED_KEYS)
|
|
558
|
+
&& validId(value.original_operation_id)
|
|
559
|
+
&& typeof value.original_action_digest === 'string' && DIGEST.test(value.original_action_digest)
|
|
560
|
+
&& typeof value.original_terminal_evidence_digest === 'string'
|
|
561
|
+
&& DIGEST.test(value.original_terminal_evidence_digest)
|
|
562
|
+
&& validId(value.case_instance_id)
|
|
563
|
+
&& Number.isSafeInteger(value.case_revision) && value.case_revision >= 0
|
|
564
|
+
&& CASE_STATUSES.has(value.case_status)
|
|
565
|
+
&& validId(value.remedy_operation_id)
|
|
566
|
+
&& typeof value.remedy_action_digest === 'string' && DIGEST.test(value.remedy_action_digest)
|
|
567
|
+
&& typeof value.remedy_caid === 'string' && CAID.test(value.remedy_caid)
|
|
568
|
+
&& typeof value.destination_binding_digest === 'string'
|
|
569
|
+
&& DIGEST.test(value.destination_binding_digest)
|
|
570
|
+
&& Number.isSafeInteger(value.units) && value.units > 0
|
|
571
|
+
&& validContext(value.unit)
|
|
572
|
+
&& ['receipt-program', 'action-escrow'].includes(value.owner_mode)
|
|
573
|
+
&& typeof value.owner_digest === 'string' && DIGEST.test(value.owner_digest);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function validPayload(value: unknown): value is DataRecord {
|
|
577
|
+
return exactKeys(value, PAYLOAD_KEYS)
|
|
578
|
+
&& exactKeys(value.case, CASE_KEYS)
|
|
579
|
+
&& validId(value.case.instance_id)
|
|
580
|
+
&& Number.isSafeInteger(value.case.revision) && value.case.revision >= 0
|
|
581
|
+
&& CASE_STATUSES.has(value.case.status)
|
|
582
|
+
&& typeof value.case.state_snapshot_digest === 'string'
|
|
583
|
+
&& DIGEST.test(value.case.state_snapshot_digest)
|
|
584
|
+
&& Number.isFinite(strictInstant(value.case.updated_at))
|
|
585
|
+
&& validOriginal(value.original_effect)
|
|
586
|
+
&& (value.original_reconciliation === null
|
|
587
|
+
|| validOriginalReconciliation(value.original_reconciliation))
|
|
588
|
+
&& exactKeys(value.remedy, REMEDY_KEYS)
|
|
589
|
+
&& validId(value.remedy.operation_id)
|
|
590
|
+
&& typeof value.remedy.caid === 'string' && CAID.test(value.remedy.caid)
|
|
591
|
+
&& typeof value.remedy.action_digest === 'string' && DIGEST.test(value.remedy.action_digest)
|
|
592
|
+
&& typeof value.remedy.destination_binding_digest === 'string'
|
|
593
|
+
&& DIGEST.test(value.remedy.destination_binding_digest)
|
|
594
|
+
&& Number.isSafeInteger(value.remedy.units) && value.remedy.units > 0
|
|
595
|
+
&& validContext(value.remedy.unit)
|
|
596
|
+
&& ['receipt-program', 'action-escrow'].includes(value.remedy.owner_mode)
|
|
597
|
+
&& typeof value.remedy.owner_digest === 'string' && DIGEST.test(value.remedy.owner_digest)
|
|
598
|
+
&& REMEDY_STATUSES.has(value.remedy.status)
|
|
599
|
+
&& (value.remedy.outcome === null || REMEDY_OUTCOMES.has(value.remedy.outcome))
|
|
600
|
+
&& exactKeys(value.semantics, SEMANTICS_KEYS)
|
|
601
|
+
&& value.semantics.original_effect === 'immutable_fact'
|
|
602
|
+
&& value.semantics.remedy_effect === 'compensating_action'
|
|
603
|
+
&& value.semantics.rollback === false
|
|
604
|
+
&& value.remedy.operation_id !== value.original_effect.operation_id
|
|
605
|
+
&& value.remedy.action_digest !== value.original_effect.action_digest;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function validReceipt(value: unknown): value is DataRecord {
|
|
609
|
+
return exactKeys(value, TOP_KEYS)
|
|
610
|
+
&& value.version === ACTION_REMEDY_RECEIPT_VERSION
|
|
611
|
+
&& validIssuer(value.issuer)
|
|
612
|
+
&& validPayload(value.payload)
|
|
613
|
+
&& typeof value.content_digest === 'string' && DIGEST.test(value.content_digest)
|
|
614
|
+
&& exactKeys(value.signature, SIGNATURE_KEYS)
|
|
615
|
+
&& value.signature.algorithm === 'Ed25519'
|
|
616
|
+
&& strictBase64url(value.signature.value, 64) !== null;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function sameCanonical(left: unknown, right: unknown): boolean {
|
|
620
|
+
try {
|
|
621
|
+
return canonicalize(left) === canonicalize(right);
|
|
622
|
+
} catch {
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function receiptExpected(receipt: DataRecord): RemedyReceiptExpectedBindings {
|
|
628
|
+
return {
|
|
629
|
+
original_operation_id: receipt.payload.original_effect.operation_id,
|
|
630
|
+
original_action_digest: receipt.payload.original_effect.action_digest,
|
|
631
|
+
original_terminal_evidence_digest: receipt.payload.original_effect.terminal_evidence_digest,
|
|
632
|
+
case_instance_id: receipt.payload.case.instance_id,
|
|
633
|
+
case_revision: receipt.payload.case.revision,
|
|
634
|
+
case_status: receipt.payload.case.status,
|
|
635
|
+
remedy_operation_id: receipt.payload.remedy.operation_id,
|
|
636
|
+
remedy_action_digest: receipt.payload.remedy.action_digest,
|
|
637
|
+
remedy_caid: receipt.payload.remedy.caid,
|
|
638
|
+
destination_binding_digest: receipt.payload.remedy.destination_binding_digest,
|
|
639
|
+
units: receipt.payload.remedy.units,
|
|
640
|
+
unit: receipt.payload.remedy.unit,
|
|
641
|
+
owner_mode: receipt.payload.remedy.owner_mode,
|
|
642
|
+
owner_digest: receipt.payload.remedy.owner_digest,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function configureSigner(options: DataRecord, context: DataRecord) {
|
|
647
|
+
const hasPrivateKey = options.privateKey !== undefined;
|
|
648
|
+
const hasSigner = options.signer !== undefined;
|
|
649
|
+
if (hasPrivateKey === hasSigner) throw new TypeError('configure exactly one remedy receipt signer');
|
|
650
|
+
const allowEphemeralState = options.allowEphemeralState === true;
|
|
651
|
+
|
|
652
|
+
if (hasPrivateKey) {
|
|
653
|
+
if (!allowEphemeralState) {
|
|
654
|
+
throw new TypeError('production remedy receipt issuance requires an external KMS/HSM signer');
|
|
655
|
+
}
|
|
656
|
+
const key = privateKey(options.privateKey);
|
|
657
|
+
return Object.freeze({
|
|
658
|
+
keyId: context.key_id,
|
|
659
|
+
publicKey: publicKeyB64u(key),
|
|
660
|
+
sign: async (bytes: Buffer) => crypto.sign(null, bytes, key),
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const signer = options.signer;
|
|
665
|
+
if (!isDataRecord(signer)) throw new TypeError('remedy receipt signer must be a data object');
|
|
666
|
+
if (!validContext(signer.keyId)
|
|
667
|
+
|| typeof signer.publicKey !== 'string'
|
|
668
|
+
|| typeof signer.sign !== 'function') {
|
|
669
|
+
throw new TypeError('remedy receipt signer requires keyId, publicKey, and sign(bytes)');
|
|
670
|
+
}
|
|
671
|
+
if (!allowEphemeralState && !['kms', 'hsm'].includes(signer.custody)) {
|
|
672
|
+
throw new TypeError('production remedy receipt signer custody must be kms or hsm');
|
|
673
|
+
}
|
|
674
|
+
if (!publicKey(signer.publicKey)) {
|
|
675
|
+
throw new TypeError('remedy receipt signer public key must be Ed25519 SPKI base64url');
|
|
676
|
+
}
|
|
677
|
+
return Object.freeze({
|
|
678
|
+
keyId: signer.keyId,
|
|
679
|
+
publicKey: signer.publicKey,
|
|
680
|
+
sign: signer.sign,
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Issue one receipt. Local private keys require an explicit ephemeral/test
|
|
686
|
+
* opt-in; production issuance requires an external signer declaring KMS/HSM
|
|
687
|
+
* custody. Every returned signature is verified against the configured public
|
|
688
|
+
* key before the receipt leaves this function.
|
|
689
|
+
*/
|
|
690
|
+
export async function issueRemedyProgramReceipt(
|
|
691
|
+
input: {
|
|
692
|
+
state?: unknown;
|
|
693
|
+
remedyOperationId?: string;
|
|
694
|
+
} = {},
|
|
695
|
+
options: {
|
|
696
|
+
context?: unknown;
|
|
697
|
+
privateKey?: unknown;
|
|
698
|
+
signer?: unknown;
|
|
699
|
+
allowEphemeralState?: boolean;
|
|
700
|
+
} = {},
|
|
701
|
+
) {
|
|
702
|
+
if (!exactKeys(input, new Set(['state', 'remedyOperationId']))) {
|
|
703
|
+
throw new TypeError('remedy receipt input must contain exactly state and remedyOperationId');
|
|
704
|
+
}
|
|
705
|
+
const selected = snapshotAndAttempt(input.state, input.remedyOperationId);
|
|
706
|
+
const context = canonicalCopy(options.context, 'remedy receipt context') as unknown;
|
|
707
|
+
if (!validIssuer(context)) {
|
|
708
|
+
throw new TypeError('remedy receipt context must contain exact pinned issuer fields');
|
|
709
|
+
}
|
|
710
|
+
if (context.tenant !== selected.state.tenant_id
|
|
711
|
+
|| context.environment !== selected.state.environment
|
|
712
|
+
|| context.audience !== selected.state.audience) {
|
|
713
|
+
throw new TypeError('remedy receipt context does not match state tenant/environment/audience');
|
|
714
|
+
}
|
|
715
|
+
const signer = configureSigner(options as DataRecord, context);
|
|
716
|
+
if (signer.keyId !== context.key_id) {
|
|
717
|
+
throw new TypeError('remedy receipt context key_id does not match signer keyId');
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const unsigned = {
|
|
721
|
+
version: ACTION_REMEDY_RECEIPT_VERSION,
|
|
722
|
+
issuer: context,
|
|
723
|
+
payload: payloadFor(selected.state, selected.attempt),
|
|
724
|
+
};
|
|
725
|
+
const signedBody = {
|
|
726
|
+
...unsigned,
|
|
727
|
+
content_digest: canonicalDigest(unsigned),
|
|
728
|
+
};
|
|
729
|
+
const signingBytes = remedyProgramReceiptSigningBytes(signedBody);
|
|
730
|
+
const signature = canonicalSignature(await signer.sign(Buffer.from(signingBytes)));
|
|
731
|
+
const verificationKey = publicKey(signer.publicKey)!;
|
|
732
|
+
if (!crypto.verify(null, signingBytes, verificationKey, signature.bytes)) {
|
|
733
|
+
throw new TypeError('remedy receipt signer self-verification failed');
|
|
734
|
+
}
|
|
735
|
+
const receipt = canonicalCopy({
|
|
736
|
+
...signedBody,
|
|
737
|
+
signature: { algorithm: 'Ed25519', value: signature.encoded },
|
|
738
|
+
}, 'remedy receipt');
|
|
739
|
+
const expected = expectedBindings(selected.state, selected.attempt);
|
|
740
|
+
const selfCheck = verifyRemedyProgramReceipt(receipt, {
|
|
741
|
+
trustedKeys: { [context.key_id]: signer.publicKey },
|
|
742
|
+
expectedIssuer: context,
|
|
743
|
+
state: selected.state,
|
|
744
|
+
expected,
|
|
745
|
+
});
|
|
746
|
+
if (!selfCheck.valid) {
|
|
747
|
+
throw new TypeError(`remedy receipt self-verification failed: ${selfCheck.reason}`);
|
|
748
|
+
}
|
|
749
|
+
return deepFreeze(receipt);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
export const signRemedyProgramReceipt = issueRemedyProgramReceipt;
|
|
753
|
+
export const createRemedyProgramReceipt = issueRemedyProgramReceipt;
|
|
754
|
+
|
|
755
|
+
function refusal(reason: string, checks: Record<string, boolean>) {
|
|
756
|
+
return Object.freeze({
|
|
757
|
+
valid: false,
|
|
758
|
+
reason,
|
|
759
|
+
checks: Object.freeze({ ...checks }),
|
|
760
|
+
content_digest: null,
|
|
761
|
+
payload: null,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Verify a receipt without network access. Trust keys, all issuer fields, the
|
|
767
|
+
* exact current state snapshot, and every material original/remedy binding are
|
|
768
|
+
* relying-party inputs; none are accepted from the receipt itself.
|
|
769
|
+
*/
|
|
770
|
+
export function verifyRemedyProgramReceipt(receipt: unknown, {
|
|
771
|
+
trustedKeys,
|
|
772
|
+
expectedIssuer,
|
|
773
|
+
state,
|
|
774
|
+
expected,
|
|
775
|
+
}: {
|
|
776
|
+
trustedKeys?: unknown;
|
|
777
|
+
expectedIssuer?: unknown;
|
|
778
|
+
state?: unknown;
|
|
779
|
+
expected?: unknown;
|
|
780
|
+
} = {}) {
|
|
781
|
+
const checks = {
|
|
782
|
+
structure: false,
|
|
783
|
+
payload: false,
|
|
784
|
+
content_digest: false,
|
|
785
|
+
issuer_pin: false,
|
|
786
|
+
key: false,
|
|
787
|
+
signature: false,
|
|
788
|
+
state_snapshot: false,
|
|
789
|
+
expected_bindings: false,
|
|
790
|
+
};
|
|
791
|
+
try {
|
|
792
|
+
const snapshot = canonicalCopy(receipt, 'remedy receipt') as unknown;
|
|
793
|
+
if (!exactKeys(snapshot, TOP_KEYS)
|
|
794
|
+
|| snapshot.version !== ACTION_REMEDY_RECEIPT_VERSION
|
|
795
|
+
|| !validIssuer(snapshot.issuer)
|
|
796
|
+
|| !exactKeys(snapshot.signature, SIGNATURE_KEYS)) {
|
|
797
|
+
return refusal('receipt_structure_invalid', checks);
|
|
798
|
+
}
|
|
799
|
+
checks.structure = true;
|
|
800
|
+
if (!validPayload(snapshot.payload)) {
|
|
801
|
+
return refusal('receipt_structure_invalid', checks);
|
|
802
|
+
}
|
|
803
|
+
checks.payload = true;
|
|
804
|
+
if (typeof snapshot.content_digest !== 'string'
|
|
805
|
+
|| !DIGEST.test(snapshot.content_digest)
|
|
806
|
+
|| canonicalDigest(contentBody(snapshot)) !== snapshot.content_digest) {
|
|
807
|
+
return refusal('receipt_content_digest_mismatch', checks);
|
|
808
|
+
}
|
|
809
|
+
checks.content_digest = true;
|
|
810
|
+
|
|
811
|
+
let issuerPin: unknown;
|
|
812
|
+
try {
|
|
813
|
+
issuerPin = canonicalCopy(expectedIssuer, 'expected remedy receipt issuer');
|
|
814
|
+
} catch {
|
|
815
|
+
return refusal('receipt_expected_issuer_mismatch', checks);
|
|
816
|
+
}
|
|
817
|
+
checks.issuer_pin = validIssuer(issuerPin) && sameCanonical(snapshot.issuer, issuerPin);
|
|
818
|
+
if (!checks.issuer_pin) return refusal('receipt_expected_issuer_mismatch', checks);
|
|
819
|
+
|
|
820
|
+
const trusted = isDataRecord(trustedKeys)
|
|
821
|
+
&& Object.hasOwn(trustedKeys, snapshot.issuer.key_id)
|
|
822
|
+
? trustedKeys[snapshot.issuer.key_id] : null;
|
|
823
|
+
const key = publicKey(trusted);
|
|
824
|
+
checks.key = key !== null;
|
|
825
|
+
if (!checks.key) return refusal('receipt_key_untrusted', checks);
|
|
826
|
+
|
|
827
|
+
const signatureBytes = snapshot.signature.algorithm === 'Ed25519'
|
|
828
|
+
? strictBase64url(snapshot.signature.value, 64) : null;
|
|
829
|
+
checks.signature = signatureBytes !== null
|
|
830
|
+
&& crypto.verify(null, remedyProgramReceiptSigningBytes(snapshot), key!, signatureBytes);
|
|
831
|
+
if (!checks.signature) return refusal('receipt_signature_invalid', checks);
|
|
832
|
+
|
|
833
|
+
let stateCopy: unknown;
|
|
834
|
+
try {
|
|
835
|
+
stateCopy = canonicalCopy(state, 'expected remedy state snapshot');
|
|
836
|
+
} catch {
|
|
837
|
+
return refusal('receipt_state_snapshot_mismatch', checks);
|
|
838
|
+
}
|
|
839
|
+
checks.state_snapshot = validState(stateCopy)
|
|
840
|
+
&& canonicalDigest(stateCopy) === snapshot.payload.case.state_snapshot_digest
|
|
841
|
+
&& stateCopy.instance_id === snapshot.payload.case.instance_id
|
|
842
|
+
&& stateCopy.revision === snapshot.payload.case.revision
|
|
843
|
+
&& stateCopy.status === snapshot.payload.case.status
|
|
844
|
+
&& stateCopy.updated_at === snapshot.payload.case.updated_at;
|
|
845
|
+
if (!checks.state_snapshot) return refusal('receipt_state_snapshot_mismatch', checks);
|
|
846
|
+
|
|
847
|
+
let expectedCopy: unknown;
|
|
848
|
+
try {
|
|
849
|
+
expectedCopy = canonicalCopy(expected, 'expected remedy bindings');
|
|
850
|
+
} catch {
|
|
851
|
+
return refusal('receipt_expected_binding_mismatch', checks);
|
|
852
|
+
}
|
|
853
|
+
let derivedAttempt: DataRecord;
|
|
854
|
+
try {
|
|
855
|
+
if (!validExpected(expectedCopy)) {
|
|
856
|
+
return refusal('receipt_expected_binding_mismatch', checks);
|
|
857
|
+
}
|
|
858
|
+
derivedAttempt = selectAttempt(stateCopy as DataRecord, expectedCopy.remedy_operation_id);
|
|
859
|
+
} catch {
|
|
860
|
+
return refusal('receipt_expected_binding_mismatch', checks);
|
|
861
|
+
}
|
|
862
|
+
checks.expected_bindings = sameCanonical(receiptExpected(snapshot), expectedCopy)
|
|
863
|
+
&& sameCanonical(expectedBindings(stateCopy as DataRecord, derivedAttempt), expectedCopy)
|
|
864
|
+
&& sameCanonical(payloadFor(stateCopy as DataRecord, derivedAttempt), snapshot.payload);
|
|
865
|
+
if (!checks.expected_bindings) {
|
|
866
|
+
return refusal('receipt_expected_binding_mismatch', checks);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
return deepFreeze({
|
|
870
|
+
valid: true,
|
|
871
|
+
reason: 'verified',
|
|
872
|
+
checks,
|
|
873
|
+
content_digest: snapshot.content_digest,
|
|
874
|
+
payload: canonicalCopy(snapshot.payload, 'verified remedy receipt payload'),
|
|
875
|
+
});
|
|
876
|
+
} catch {
|
|
877
|
+
return refusal('receipt_structure_invalid', checks);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
export default {
|
|
882
|
+
ACTION_REMEDY_RECEIPT_VERSION,
|
|
883
|
+
REMEDY_PROGRAM_RECEIPT_VERSION,
|
|
884
|
+
ACTION_REMEDY_RECEIPT_DOMAIN,
|
|
885
|
+
expectedRemedyProgramReceiptBindings,
|
|
886
|
+
remedyProgramReceiptSigningBytes,
|
|
887
|
+
issueRemedyProgramReceipt,
|
|
888
|
+
signRemedyProgramReceipt,
|
|
889
|
+
createRemedyProgramReceipt,
|
|
890
|
+
verifyRemedyProgramReceipt,
|
|
891
|
+
};
|