@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,1351 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* EMILIA Gate Trust Program Profile v1.
|
|
5
|
+
*
|
|
6
|
+
* A relying-party-controlled, fail-closed authorization DAG for consequential
|
|
7
|
+
* actions. This module composes evidence verifiers; it does not redefine the
|
|
8
|
+
* Handshake, Quorum, AEC, capability, or Action Escrow wire formats.
|
|
9
|
+
*/
|
|
10
|
+
import crypto from 'node:crypto';
|
|
11
|
+
import { canonicalize, hashCanonical } from './execution-binding.js';
|
|
12
|
+
export const TRUST_PROGRAM_VERSION = 'EP-GATE-TRUST-PROGRAM-PROFILE-v1';
|
|
13
|
+
export const TRUST_STAGE_RECEIPT_VERSION = 'EP-GATE-TRUST-STAGE-RECEIPT-v1';
|
|
14
|
+
const STAGE_RECEIPT_DOMAIN = `${TRUST_STAGE_RECEIPT_VERSION}\0`;
|
|
15
|
+
const DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
16
|
+
const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
17
|
+
const CAID = /^caid:1:[a-z][a-z0-9.-]*\.[1-9][0-9]*:jcs-sha256:[A-Za-z0-9_-]{43}$/;
|
|
18
|
+
const MAX_STAGES = 64;
|
|
19
|
+
const MAX_REQUIREMENTS_PER_STAGE = 64;
|
|
20
|
+
const MAX_TOTAL_REQUIREMENTS = 1024;
|
|
21
|
+
const PROGRAM_KEYS = new Set([
|
|
22
|
+
'@version', 'program_id', 'version', 'root_caid', 'action_digest',
|
|
23
|
+
'valid_from', 'expires_at', 'stages', 'execution',
|
|
24
|
+
]);
|
|
25
|
+
const STAGE_KEYS = new Set(['stage_id', 'depends_on', 'rule', 'requirements']);
|
|
26
|
+
const REQUIREMENT_KEYS = new Set([
|
|
27
|
+
'requirement_id', 'evidence_type', 'verifier_profile', 'policy_digest',
|
|
28
|
+
'max_age_sec', 'revocation_required',
|
|
29
|
+
]);
|
|
30
|
+
const EXECUTION_KEYS = new Set([
|
|
31
|
+
'depends_on', 'consequence_mode', 'capability_template_digest', 'escrow_profile_digest',
|
|
32
|
+
]);
|
|
33
|
+
const STAGE_RECEIPT_KEYS = new Set([
|
|
34
|
+
'version', 'issuer', 'payload', 'receipt_digest', 'signature',
|
|
35
|
+
]);
|
|
36
|
+
const STAGE_RECEIPT_ISSUER_KEYS = new Set([
|
|
37
|
+
'issuer', 'tenant', 'environment', 'audience', 'key_id',
|
|
38
|
+
]);
|
|
39
|
+
const STAGE_RECEIPT_PAYLOAD_KEYS = new Set([
|
|
40
|
+
'instance_id', 'program_id', 'program_version', 'program_digest', 'root_caid',
|
|
41
|
+
'action_digest', 'stage_id', 'stage_policy_digest', 'predecessor_receipt_digests',
|
|
42
|
+
'evidence_digests', 'subjects', 'key_fingerprints', 'satisfied_at',
|
|
43
|
+
]);
|
|
44
|
+
const STAGE_RECEIPT_SIGNATURE_KEYS = new Set(['algorithm', 'value']);
|
|
45
|
+
const STATE_KEYS = new Set([
|
|
46
|
+
'version', 'tenant_id', 'instance_id', 'program_id', 'program_version', 'program_digest',
|
|
47
|
+
'root_caid', 'action_digest', 'status', 'revision', 'created_at', 'updated_at',
|
|
48
|
+
'stages', 'used_evidence_ids', 'execution', 'invalidation_reason',
|
|
49
|
+
]);
|
|
50
|
+
const STAGE_STATE_KEYS = new Set([
|
|
51
|
+
'status', 'predecessor_receipt_digests', 'evidence', 'receipt',
|
|
52
|
+
]);
|
|
53
|
+
const EVIDENCE_STATE_KEYS = new Set([
|
|
54
|
+
'evidence_id', 'evidence_digest', 'policy_digest', 'binding_digest', 'subjects',
|
|
55
|
+
'key_fingerprints', 'issued_at', 'expires_at', 'revocation_checked_at',
|
|
56
|
+
]);
|
|
57
|
+
const EXECUTION_STATE_KEYS = new Set([
|
|
58
|
+
'status', 'claim_token_digest', 'evidence_digest', 'outcome', 'operation_id',
|
|
59
|
+
'claimed_at', 'authorization_binding', 'finalized_at', 'reconciled_at',
|
|
60
|
+
]);
|
|
61
|
+
const AUTHORIZATION_BINDING_KEYS = new Set([
|
|
62
|
+
'instance_id', 'operation_id', 'program_digest', 'root_caid', 'action_digest',
|
|
63
|
+
'receipt_context_digest', 'terminal_stage_receipt_digests', 'consequence_mode',
|
|
64
|
+
'capability_template_digest', 'escrow_profile_digest',
|
|
65
|
+
]);
|
|
66
|
+
function fail(reason) {
|
|
67
|
+
return { ok: false, reason };
|
|
68
|
+
}
|
|
69
|
+
function isRecord(value) {
|
|
70
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
71
|
+
return false;
|
|
72
|
+
const prototype = Object.getPrototypeOf(value);
|
|
73
|
+
return prototype === Object.prototype || prototype === null;
|
|
74
|
+
}
|
|
75
|
+
function isDataRecord(value) {
|
|
76
|
+
return isRecord(value) && Reflect.ownKeys(value).every((key) => {
|
|
77
|
+
if (typeof key !== 'string')
|
|
78
|
+
return false;
|
|
79
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
80
|
+
return descriptor?.enumerable === true && Object.hasOwn(descriptor, 'value');
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function exactKeys(value, allowed) {
|
|
84
|
+
return isDataRecord(value)
|
|
85
|
+
&& Reflect.ownKeys(value).length === allowed.size
|
|
86
|
+
&& Object.keys(value).every((key) => allowed.has(key));
|
|
87
|
+
}
|
|
88
|
+
function clone(value) {
|
|
89
|
+
return structuredClone(value);
|
|
90
|
+
}
|
|
91
|
+
function digest(value) {
|
|
92
|
+
return `sha256:${hashCanonical(value)}`;
|
|
93
|
+
}
|
|
94
|
+
function strictInstant(value) {
|
|
95
|
+
if (typeof value !== 'string'
|
|
96
|
+
|| !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/.test(value))
|
|
97
|
+
return NaN;
|
|
98
|
+
const parsed = Date.parse(value);
|
|
99
|
+
return Number.isFinite(parsed) ? parsed : NaN;
|
|
100
|
+
}
|
|
101
|
+
function exactUniqueStrings(value, maximum) {
|
|
102
|
+
return Array.isArray(value)
|
|
103
|
+
&& value.length <= maximum
|
|
104
|
+
&& value.every((entry) => typeof entry === 'string' && ID.test(entry))
|
|
105
|
+
&& new Set(value).size === value.length;
|
|
106
|
+
}
|
|
107
|
+
function boundedProjection(value, required) {
|
|
108
|
+
return Array.isArray(value)
|
|
109
|
+
&& (!required || value.length > 0)
|
|
110
|
+
&& value.length <= 256
|
|
111
|
+
&& value.every((entry) => typeof entry === 'string'
|
|
112
|
+
&& entry.length > 0 && entry.length <= 512
|
|
113
|
+
&& !/[\u0000-\u001f\u007f]/.test(entry))
|
|
114
|
+
&& new Set(value).size === value.length;
|
|
115
|
+
}
|
|
116
|
+
function sortedUniqueDigests(value, maximum) {
|
|
117
|
+
return Array.isArray(value)
|
|
118
|
+
&& value.length <= maximum
|
|
119
|
+
&& value.every((entry) => typeof entry === 'string' && DIGEST.test(entry))
|
|
120
|
+
&& new Set(value).size === value.length
|
|
121
|
+
&& value.every((entry, index) => index === 0 || value[index - 1] < entry);
|
|
122
|
+
}
|
|
123
|
+
function boundedContextString(value) {
|
|
124
|
+
return typeof value === 'string' && value.length > 0 && value.length <= 256
|
|
125
|
+
&& !/[\u0000-\u001f\u007f]/.test(value);
|
|
126
|
+
}
|
|
127
|
+
function effectiveRequired(stage) {
|
|
128
|
+
if (stage.rule.mode === 'all')
|
|
129
|
+
return stage.requirements.length;
|
|
130
|
+
if (stage.rule.mode === 'any')
|
|
131
|
+
return 1;
|
|
132
|
+
return stage.rule.required;
|
|
133
|
+
}
|
|
134
|
+
function validationFailure(reason) {
|
|
135
|
+
return { valid: false, reason, digest: null };
|
|
136
|
+
}
|
|
137
|
+
/** Validate the closed, bounded DAG before any state is created. */
|
|
138
|
+
export function validateTrustProgram(program) {
|
|
139
|
+
try {
|
|
140
|
+
canonicalize(program);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return validationFailure('program_not_canonical');
|
|
144
|
+
}
|
|
145
|
+
if (!exactKeys(program, PROGRAM_KEYS) || program['@version'] !== TRUST_PROGRAM_VERSION) {
|
|
146
|
+
return validationFailure('program_version_unsupported');
|
|
147
|
+
}
|
|
148
|
+
if (typeof program.program_id !== 'string' || !ID.test(program.program_id)
|
|
149
|
+
|| !Number.isSafeInteger(program.version) || program.version < 1
|
|
150
|
+
|| typeof program.root_caid !== 'string' || !CAID.test(program.root_caid)
|
|
151
|
+
|| typeof program.action_digest !== 'string' || !DIGEST.test(program.action_digest)) {
|
|
152
|
+
return validationFailure('program_binding_invalid');
|
|
153
|
+
}
|
|
154
|
+
const validFrom = strictInstant(program.valid_from);
|
|
155
|
+
const expiresAt = strictInstant(program.expires_at);
|
|
156
|
+
if (!Number.isFinite(validFrom) || !Number.isFinite(expiresAt) || expiresAt <= validFrom) {
|
|
157
|
+
return validationFailure('program_time_window_invalid');
|
|
158
|
+
}
|
|
159
|
+
if (!Array.isArray(program.stages) || program.stages.length === 0 || program.stages.length > MAX_STAGES) {
|
|
160
|
+
return validationFailure('program_stage_count_invalid');
|
|
161
|
+
}
|
|
162
|
+
if (!exactKeys(program.execution, EXECUTION_KEYS)
|
|
163
|
+
|| !exactUniqueStrings(program.execution.depends_on, MAX_STAGES)
|
|
164
|
+
|| program.execution.depends_on.length === 0
|
|
165
|
+
|| !['receipt-program', 'action-escrow'].includes(program.execution.consequence_mode)
|
|
166
|
+
|| (program.execution.consequence_mode === 'receipt-program'
|
|
167
|
+
&& (!DIGEST.test(program.execution.capability_template_digest)
|
|
168
|
+
|| program.execution.escrow_profile_digest !== null))
|
|
169
|
+
|| (program.execution.consequence_mode === 'action-escrow'
|
|
170
|
+
&& (!DIGEST.test(program.execution.escrow_profile_digest)
|
|
171
|
+
|| program.execution.capability_template_digest !== null))) {
|
|
172
|
+
return validationFailure('program_execution_invalid');
|
|
173
|
+
}
|
|
174
|
+
const stageIds = new Set();
|
|
175
|
+
let totalRequirements = 0;
|
|
176
|
+
for (const stage of program.stages) {
|
|
177
|
+
if (!exactKeys(stage, STAGE_KEYS) || typeof stage.stage_id !== 'string' || !ID.test(stage.stage_id)
|
|
178
|
+
|| stageIds.has(stage.stage_id))
|
|
179
|
+
return validationFailure('stage_id_invalid');
|
|
180
|
+
stageIds.add(stage.stage_id);
|
|
181
|
+
if (!exactUniqueStrings(stage.depends_on, MAX_STAGES) || stage.depends_on.includes(stage.stage_id)) {
|
|
182
|
+
return validationFailure('stage_dependency_invalid');
|
|
183
|
+
}
|
|
184
|
+
if (!Array.isArray(stage.requirements) || stage.requirements.length === 0
|
|
185
|
+
|| stage.requirements.length > MAX_REQUIREMENTS_PER_STAGE) {
|
|
186
|
+
return validationFailure('stage_requirement_count_invalid');
|
|
187
|
+
}
|
|
188
|
+
totalRequirements += stage.requirements.length;
|
|
189
|
+
if (totalRequirements > MAX_TOTAL_REQUIREMENTS)
|
|
190
|
+
return validationFailure('program_requirement_limit');
|
|
191
|
+
const requirementIds = new Set();
|
|
192
|
+
for (const requirement of stage.requirements) {
|
|
193
|
+
if (!exactKeys(requirement, REQUIREMENT_KEYS)
|
|
194
|
+
|| typeof requirement.requirement_id !== 'string' || !ID.test(requirement.requirement_id)
|
|
195
|
+
|| requirementIds.has(requirement.requirement_id)
|
|
196
|
+
|| typeof requirement.evidence_type !== 'string' || !ID.test(requirement.evidence_type)
|
|
197
|
+
|| typeof requirement.verifier_profile !== 'string' || !ID.test(requirement.verifier_profile)
|
|
198
|
+
|| typeof requirement.policy_digest !== 'string' || !DIGEST.test(requirement.policy_digest)
|
|
199
|
+
|| !Number.isSafeInteger(requirement.max_age_sec) || requirement.max_age_sec < 1
|
|
200
|
+
|| requirement.max_age_sec > 31_536_000
|
|
201
|
+
|| typeof requirement.revocation_required !== 'boolean') {
|
|
202
|
+
return validationFailure('stage_requirement_invalid');
|
|
203
|
+
}
|
|
204
|
+
requirementIds.add(requirement.requirement_id);
|
|
205
|
+
}
|
|
206
|
+
const ruleKeys = stage.rule?.mode === 'threshold'
|
|
207
|
+
? new Set(['mode', 'required', 'distinct_subjects', 'distinct_keys'])
|
|
208
|
+
: new Set(['mode', 'distinct_subjects', 'distinct_keys']);
|
|
209
|
+
if (!exactKeys(stage.rule, ruleKeys)
|
|
210
|
+
|| !['all', 'any', 'threshold'].includes(stage.rule.mode)
|
|
211
|
+
|| typeof stage.rule.distinct_subjects !== 'boolean'
|
|
212
|
+
|| typeof stage.rule.distinct_keys !== 'boolean') {
|
|
213
|
+
return validationFailure('stage_rule_invalid');
|
|
214
|
+
}
|
|
215
|
+
const required = effectiveRequired(stage);
|
|
216
|
+
if (!Number.isSafeInteger(required) || required < 1 || required > stage.requirements.length) {
|
|
217
|
+
return validationFailure('stage_threshold_invalid');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
for (const stage of program.stages) {
|
|
221
|
+
if (stage.depends_on.some((dependency) => !stageIds.has(dependency))) {
|
|
222
|
+
return validationFailure('stage_dependency_unknown');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (program.execution.depends_on.some((dependency) => !stageIds.has(dependency))) {
|
|
226
|
+
return validationFailure('execution_dependency_unknown');
|
|
227
|
+
}
|
|
228
|
+
const byId = new Map(program.stages.map((stage) => [stage.stage_id, stage]));
|
|
229
|
+
const visiting = new Set();
|
|
230
|
+
const visited = new Set();
|
|
231
|
+
const visit = (stageId) => {
|
|
232
|
+
if (visiting.has(stageId))
|
|
233
|
+
return false;
|
|
234
|
+
if (visited.has(stageId))
|
|
235
|
+
return true;
|
|
236
|
+
visiting.add(stageId);
|
|
237
|
+
for (const dependency of byId.get(stageId).depends_on) {
|
|
238
|
+
if (!visit(dependency))
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
visiting.delete(stageId);
|
|
242
|
+
visited.add(stageId);
|
|
243
|
+
return true;
|
|
244
|
+
};
|
|
245
|
+
for (const stageId of stageIds) {
|
|
246
|
+
if (!visit(stageId))
|
|
247
|
+
return validationFailure('program_cycle');
|
|
248
|
+
}
|
|
249
|
+
const executionRelevant = new Set();
|
|
250
|
+
const markRelevant = (stageId) => {
|
|
251
|
+
if (executionRelevant.has(stageId))
|
|
252
|
+
return;
|
|
253
|
+
executionRelevant.add(stageId);
|
|
254
|
+
for (const dependency of byId.get(stageId).depends_on)
|
|
255
|
+
markRelevant(dependency);
|
|
256
|
+
};
|
|
257
|
+
for (const stageId of program.execution.depends_on)
|
|
258
|
+
markRelevant(stageId);
|
|
259
|
+
if ([...stageIds].some((stageId) => !executionRelevant.has(stageId))) {
|
|
260
|
+
return validationFailure('stage_not_execution_relevant');
|
|
261
|
+
}
|
|
262
|
+
return { valid: true, reason: null, digest: digest(program) };
|
|
263
|
+
}
|
|
264
|
+
export function trustProgramDigest(program) {
|
|
265
|
+
const result = validateTrustProgram(program);
|
|
266
|
+
if (!result.valid)
|
|
267
|
+
throw new TypeError(`invalid trust program: ${result.reason}`);
|
|
268
|
+
return result.digest;
|
|
269
|
+
}
|
|
270
|
+
function publicKey(value) {
|
|
271
|
+
try {
|
|
272
|
+
if (value instanceof crypto.KeyObject)
|
|
273
|
+
return value;
|
|
274
|
+
if (typeof value !== 'string' || !/^[A-Za-z0-9_-]+$/.test(value))
|
|
275
|
+
return null;
|
|
276
|
+
const bytes = Buffer.from(value, 'base64url');
|
|
277
|
+
if (bytes.toString('base64url') !== value)
|
|
278
|
+
return null;
|
|
279
|
+
const key = crypto.createPublicKey({ key: bytes, type: 'spki', format: 'der' });
|
|
280
|
+
return key.asymmetricKeyType === 'ed25519' ? key : null;
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function stageReceiptBody(receipt) {
|
|
287
|
+
return { version: receipt.version, issuer: receipt.issuer, payload: receipt.payload };
|
|
288
|
+
}
|
|
289
|
+
function validStageReceiptShape(receipt) {
|
|
290
|
+
if (!exactKeys(receipt, STAGE_RECEIPT_KEYS)
|
|
291
|
+
|| receipt.version !== TRUST_STAGE_RECEIPT_VERSION
|
|
292
|
+
|| !exactKeys(receipt.issuer, STAGE_RECEIPT_ISSUER_KEYS)
|
|
293
|
+
|| !Object.values(receipt.issuer).every(boundedContextString)
|
|
294
|
+
|| !exactKeys(receipt.payload, STAGE_RECEIPT_PAYLOAD_KEYS)
|
|
295
|
+
|| typeof receipt.payload.instance_id !== 'string' || !ID.test(receipt.payload.instance_id)
|
|
296
|
+
|| typeof receipt.payload.program_id !== 'string' || !ID.test(receipt.payload.program_id)
|
|
297
|
+
|| !Number.isSafeInteger(receipt.payload.program_version) || receipt.payload.program_version < 1
|
|
298
|
+
|| typeof receipt.payload.program_digest !== 'string' || !DIGEST.test(receipt.payload.program_digest)
|
|
299
|
+
|| typeof receipt.payload.root_caid !== 'string' || !CAID.test(receipt.payload.root_caid)
|
|
300
|
+
|| typeof receipt.payload.action_digest !== 'string' || !DIGEST.test(receipt.payload.action_digest)
|
|
301
|
+
|| typeof receipt.payload.stage_id !== 'string' || !ID.test(receipt.payload.stage_id)
|
|
302
|
+
|| typeof receipt.payload.stage_policy_digest !== 'string' || !DIGEST.test(receipt.payload.stage_policy_digest)
|
|
303
|
+
|| !sortedUniqueDigests(receipt.payload.predecessor_receipt_digests, MAX_STAGES)
|
|
304
|
+
|| !sortedUniqueDigests(receipt.payload.evidence_digests, MAX_REQUIREMENTS_PER_STAGE)
|
|
305
|
+
|| !boundedProjection(receipt.payload.subjects, false)
|
|
306
|
+
|| !boundedProjection(receipt.payload.key_fingerprints, false)
|
|
307
|
+
|| !receipt.payload.subjects.every((entry, index) => index === 0 || receipt.payload.subjects[index - 1] < entry)
|
|
308
|
+
|| !receipt.payload.key_fingerprints.every((entry, index) => index === 0 || receipt.payload.key_fingerprints[index - 1] < entry)
|
|
309
|
+
|| !Number.isFinite(strictInstant(receipt.payload.satisfied_at))
|
|
310
|
+
|| typeof receipt.receipt_digest !== 'string' || !DIGEST.test(receipt.receipt_digest)
|
|
311
|
+
|| !exactKeys(receipt.signature, STAGE_RECEIPT_SIGNATURE_KEYS)
|
|
312
|
+
|| receipt.signature.algorithm !== 'Ed25519'
|
|
313
|
+
|| typeof receipt.signature.value !== 'string'
|
|
314
|
+
|| !/^[A-Za-z0-9_-]+$/.test(receipt.signature.value)
|
|
315
|
+
|| Buffer.from(receipt.signature.value, 'base64url').length !== 64
|
|
316
|
+
|| Buffer.from(receipt.signature.value, 'base64url').toString('base64url') !== receipt.signature.value) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
async function signStageReceipt(payload, context, privateKey, signer) {
|
|
322
|
+
const body = {
|
|
323
|
+
version: TRUST_STAGE_RECEIPT_VERSION,
|
|
324
|
+
issuer: {
|
|
325
|
+
issuer: context.issuer,
|
|
326
|
+
tenant: context.tenant,
|
|
327
|
+
environment: context.environment,
|
|
328
|
+
audience: context.audience,
|
|
329
|
+
key_id: context.key_id,
|
|
330
|
+
},
|
|
331
|
+
payload,
|
|
332
|
+
};
|
|
333
|
+
const receiptDigest = digest(body);
|
|
334
|
+
const signingBytes = Buffer.from(STAGE_RECEIPT_DOMAIN + canonicalize(body), 'utf8');
|
|
335
|
+
const signature = signer
|
|
336
|
+
? await signer({ signingBytes, body: clone(body), receiptDigest })
|
|
337
|
+
: crypto.sign(null, signingBytes, privateKey).toString('base64url');
|
|
338
|
+
if (typeof signature !== 'string' || !/^[A-Za-z0-9_-]+$/.test(signature)
|
|
339
|
+
|| Buffer.from(signature, 'base64url').length !== 64
|
|
340
|
+
|| Buffer.from(signature, 'base64url').toString('base64url') !== signature) {
|
|
341
|
+
throw new TypeError('stage receipt signer returned a malformed Ed25519 signature');
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
...body,
|
|
345
|
+
receipt_digest: receiptDigest,
|
|
346
|
+
signature: { algorithm: 'Ed25519', value: signature },
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
/** Independently verify one stage receipt and optional relying-party bindings. */
|
|
350
|
+
export function verifyTrustStageReceipt(receipt, options = {}) {
|
|
351
|
+
const checks = {
|
|
352
|
+
structure: false, digest: false, key: false, signature: false, issuer: false, expected: false,
|
|
353
|
+
};
|
|
354
|
+
if (!validStageReceiptShape(receipt)) {
|
|
355
|
+
return { valid: false, reason: 'receipt_structure_invalid', checks };
|
|
356
|
+
}
|
|
357
|
+
checks.structure = true;
|
|
358
|
+
let bodyDigest;
|
|
359
|
+
try {
|
|
360
|
+
bodyDigest = digest(stageReceiptBody(receipt));
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return { valid: false, reason: 'receipt_not_canonical', checks };
|
|
364
|
+
}
|
|
365
|
+
checks.digest = bodyDigest === receipt.receipt_digest;
|
|
366
|
+
if (!checks.digest)
|
|
367
|
+
return { valid: false, reason: 'receipt_digest_mismatch', checks };
|
|
368
|
+
const key = publicKey(options.trustedKeys?.[receipt.issuer.key_id]);
|
|
369
|
+
checks.key = key !== null;
|
|
370
|
+
if (!key)
|
|
371
|
+
return { valid: false, reason: 'receipt_key_untrusted', checks };
|
|
372
|
+
try {
|
|
373
|
+
checks.signature = crypto.verify(null, Buffer.from(STAGE_RECEIPT_DOMAIN + canonicalize(stageReceiptBody(receipt)), 'utf8'), key, Buffer.from(receipt.signature.value, 'base64url'));
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
checks.signature = false;
|
|
377
|
+
}
|
|
378
|
+
if (!checks.signature)
|
|
379
|
+
return { valid: false, reason: 'receipt_signature_invalid', checks };
|
|
380
|
+
try {
|
|
381
|
+
checks.issuer = options.expectedIssuer === undefined
|
|
382
|
+
|| canonicalize(receipt.issuer) === canonicalize(options.expectedIssuer);
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
checks.issuer = false;
|
|
386
|
+
}
|
|
387
|
+
if (!checks.issuer)
|
|
388
|
+
return { valid: false, reason: 'receipt_expected_issuer_mismatch', checks };
|
|
389
|
+
const expected = options.expected ?? {};
|
|
390
|
+
checks.expected = Object.entries(expected).every(([field, value]) => {
|
|
391
|
+
try {
|
|
392
|
+
return canonicalize(receipt.payload[field]) === canonicalize(value);
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
if (!checks.expected)
|
|
399
|
+
return { valid: false, reason: 'receipt_expected_binding_mismatch', checks };
|
|
400
|
+
return { valid: true, reason: null, checks, receipt_digest: receipt.receipt_digest, payload: clone(receipt.payload) };
|
|
401
|
+
}
|
|
402
|
+
function initialState(program, programDigest, tenantId, instanceId, now) {
|
|
403
|
+
const stages = {};
|
|
404
|
+
for (const stage of program.stages) {
|
|
405
|
+
stages[stage.stage_id] = {
|
|
406
|
+
status: stage.depends_on.length === 0 ? 'collecting' : 'locked',
|
|
407
|
+
predecessor_receipt_digests: [],
|
|
408
|
+
evidence: {},
|
|
409
|
+
receipt: null,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
version: TRUST_PROGRAM_VERSION,
|
|
414
|
+
tenant_id: tenantId,
|
|
415
|
+
instance_id: instanceId,
|
|
416
|
+
program_id: program.program_id,
|
|
417
|
+
program_version: program.version,
|
|
418
|
+
program_digest: programDigest,
|
|
419
|
+
root_caid: program.root_caid,
|
|
420
|
+
action_digest: program.action_digest,
|
|
421
|
+
status: 'active',
|
|
422
|
+
revision: 0,
|
|
423
|
+
created_at: new Date(now).toISOString(),
|
|
424
|
+
updated_at: new Date(now).toISOString(),
|
|
425
|
+
stages,
|
|
426
|
+
used_evidence_ids: [],
|
|
427
|
+
execution: { status: 'locked', claim_token_digest: null, evidence_digest: null, outcome: null },
|
|
428
|
+
invalidation_reason: null,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function invalidateState(state, reason, at) {
|
|
432
|
+
const next = clone(state);
|
|
433
|
+
next.status = 'invalidated';
|
|
434
|
+
next.invalidation_reason = reason;
|
|
435
|
+
next.revision += 1;
|
|
436
|
+
next.updated_at = new Date(at).toISOString();
|
|
437
|
+
for (const stage of Object.values(next.stages))
|
|
438
|
+
stage.status = 'invalidated';
|
|
439
|
+
if (['locked', 'ready'].includes(next.execution.status))
|
|
440
|
+
next.execution.status = 'invalidated';
|
|
441
|
+
return next;
|
|
442
|
+
}
|
|
443
|
+
function storedStateValid(state, program, programDigest, tenantId, instanceId, receiptContext, receiptVerificationKey) {
|
|
444
|
+
if (!exactKeys(state, STATE_KEYS)
|
|
445
|
+
|| state.version !== TRUST_PROGRAM_VERSION
|
|
446
|
+
|| state.tenant_id !== tenantId
|
|
447
|
+
|| state.instance_id !== instanceId
|
|
448
|
+
|| state.program_id !== program.program_id
|
|
449
|
+
|| state.program_version !== program.version
|
|
450
|
+
|| state.program_digest !== programDigest
|
|
451
|
+
|| state.root_caid !== program.root_caid
|
|
452
|
+
|| state.action_digest !== program.action_digest
|
|
453
|
+
|| !['active', 'invalidated'].includes(state.status)
|
|
454
|
+
|| !Number.isSafeInteger(state.revision) || state.revision < 0
|
|
455
|
+
|| !Number.isFinite(strictInstant(state.created_at))
|
|
456
|
+
|| !Number.isFinite(strictInstant(state.updated_at))
|
|
457
|
+
|| strictInstant(state.updated_at) < strictInstant(state.created_at)
|
|
458
|
+
|| (state.status === 'active' && state.invalidation_reason !== null)
|
|
459
|
+
|| (state.status === 'invalidated'
|
|
460
|
+
&& (typeof state.invalidation_reason !== 'string'
|
|
461
|
+
|| state.invalidation_reason.length < 1 || state.invalidation_reason.length > 256))
|
|
462
|
+
|| !isRecord(state.stages)
|
|
463
|
+
|| Object.keys(state.stages).length !== program.stages.length
|
|
464
|
+
|| !Array.isArray(state.used_evidence_ids)
|
|
465
|
+
|| state.used_evidence_ids.length > MAX_TOTAL_REQUIREMENTS
|
|
466
|
+
|| !state.used_evidence_ids.every((entry) => typeof entry === 'string' && ID.test(entry))
|
|
467
|
+
|| new Set(state.used_evidence_ids).size !== state.used_evidence_ids.length
|
|
468
|
+
|| !state.used_evidence_ids.every((entry, index) => index === 0 || state.used_evidence_ids[index - 1] < entry)
|
|
469
|
+
|| !isRecord(state.execution)
|
|
470
|
+
|| !['status', 'claim_token_digest', 'evidence_digest', 'outcome'].every((key) => Object.hasOwn(state.execution, key))
|
|
471
|
+
|| Object.keys(state.execution).some((key) => !EXECUTION_STATE_KEYS.has(key))) {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
const usedEvidenceIds = [];
|
|
475
|
+
for (const definition of program.stages) {
|
|
476
|
+
const stage = state.stages[definition.stage_id];
|
|
477
|
+
if (!exactKeys(stage, STAGE_STATE_KEYS)
|
|
478
|
+
|| !['locked', 'collecting', 'satisfied', 'invalidated'].includes(stage.status)
|
|
479
|
+
|| !sortedUniqueDigests(stage.predecessor_receipt_digests, MAX_STAGES)
|
|
480
|
+
|| !isRecord(stage.evidence)
|
|
481
|
+
|| Object.keys(stage.evidence).length > definition.requirements.length)
|
|
482
|
+
return false;
|
|
483
|
+
const dependencyReceipts = definition.depends_on.map((dependency) => state.stages[dependency]?.receipt);
|
|
484
|
+
const expectedPredecessors = dependencyReceipts.every((entry) => entry !== null)
|
|
485
|
+
? dependencyReceipts.map((entry) => entry.receipt_digest).sort()
|
|
486
|
+
: [];
|
|
487
|
+
if (canonicalize(stage.predecessor_receipt_digests) !== canonicalize(expectedPredecessors))
|
|
488
|
+
return false;
|
|
489
|
+
for (const [requirementId, accepted] of Object.entries(stage.evidence)) {
|
|
490
|
+
const requirement = definition.requirements.find((candidate) => candidate.requirement_id === requirementId);
|
|
491
|
+
if (!requirement || !exactKeys(accepted, EVIDENCE_STATE_KEYS)
|
|
492
|
+
|| typeof accepted.evidence_id !== 'string' || !ID.test(accepted.evidence_id)
|
|
493
|
+
|| typeof accepted.evidence_digest !== 'string' || !DIGEST.test(accepted.evidence_digest)
|
|
494
|
+
|| accepted.policy_digest !== requirement.policy_digest
|
|
495
|
+
|| typeof accepted.binding_digest !== 'string' || !DIGEST.test(accepted.binding_digest)
|
|
496
|
+
|| !boundedProjection(accepted.subjects, definition.rule.distinct_subjects)
|
|
497
|
+
|| !boundedProjection(accepted.key_fingerprints, definition.rule.distinct_keys)
|
|
498
|
+
|| !Number.isFinite(strictInstant(accepted.issued_at))
|
|
499
|
+
|| !Number.isFinite(strictInstant(accepted.expires_at))
|
|
500
|
+
|| strictInstant(accepted.expires_at) <= strictInstant(accepted.issued_at)
|
|
501
|
+
|| (accepted.revocation_checked_at !== null
|
|
502
|
+
&& !Number.isFinite(strictInstant(accepted.revocation_checked_at)))
|
|
503
|
+
|| (requirement.revocation_required && accepted.revocation_checked_at === null))
|
|
504
|
+
return false;
|
|
505
|
+
usedEvidenceIds.push(accepted.evidence_id);
|
|
506
|
+
}
|
|
507
|
+
const thresholdMet = stageIsSatisfied(definition, stage);
|
|
508
|
+
if (state.status === 'active') {
|
|
509
|
+
const dependenciesSatisfied = definition.depends_on.every((dependency) => state.stages[dependency].status === 'satisfied');
|
|
510
|
+
if ((!dependenciesSatisfied && stage.status !== 'locked')
|
|
511
|
+
|| (dependenciesSatisfied && stage.status === 'locked')
|
|
512
|
+
|| (stage.status === 'satisfied' && !thresholdMet)
|
|
513
|
+
|| (stage.status === 'collecting' && thresholdMet)
|
|
514
|
+
|| stage.status === 'invalidated')
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
else if (stage.status !== 'invalidated')
|
|
518
|
+
return false;
|
|
519
|
+
if (stage.receipt !== null) {
|
|
520
|
+
if (!validStageReceiptShape(stage.receipt)
|
|
521
|
+
|| digest(stageReceiptBody(stage.receipt)) !== stage.receipt.receipt_digest)
|
|
522
|
+
return false;
|
|
523
|
+
const evidenceEntries = Object.values(stage.evidence);
|
|
524
|
+
const expectedPayload = {
|
|
525
|
+
instance_id: instanceId,
|
|
526
|
+
program_id: program.program_id,
|
|
527
|
+
program_version: program.version,
|
|
528
|
+
program_digest: programDigest,
|
|
529
|
+
root_caid: program.root_caid,
|
|
530
|
+
action_digest: program.action_digest,
|
|
531
|
+
stage_id: definition.stage_id,
|
|
532
|
+
stage_policy_digest: digest(definition),
|
|
533
|
+
predecessor_receipt_digests: [...stage.predecessor_receipt_digests],
|
|
534
|
+
evidence_digests: evidenceEntries.map((entry) => entry.evidence_digest).sort(),
|
|
535
|
+
subjects: [...new Set(evidenceEntries.flatMap((entry) => entry.subjects))].sort(),
|
|
536
|
+
key_fingerprints: [...new Set(evidenceEntries.flatMap((entry) => entry.key_fingerprints))].sort(),
|
|
537
|
+
};
|
|
538
|
+
if (!Object.entries(expectedPayload).every(([key, value]) => canonicalize(stage.receipt.payload[key]) === canonicalize(value)))
|
|
539
|
+
return false;
|
|
540
|
+
if (receiptVerificationKey) {
|
|
541
|
+
const verified = verifyTrustStageReceipt(stage.receipt, {
|
|
542
|
+
trustedKeys: { [receiptContext.key_id]: receiptVerificationKey },
|
|
543
|
+
expectedIssuer: receiptContext,
|
|
544
|
+
expected: expectedPayload,
|
|
545
|
+
});
|
|
546
|
+
if (!verified.valid)
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (stage.status === 'satisfied' && stage.receipt === null)
|
|
551
|
+
return false;
|
|
552
|
+
if (state.status === 'active' && stage.status !== 'satisfied' && stage.receipt !== null)
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
if (canonicalize([...usedEvidenceIds].sort()) !== canonicalize(state.used_evidence_ids))
|
|
556
|
+
return false;
|
|
557
|
+
const execution = state.execution;
|
|
558
|
+
if (!['locked', 'ready', 'claimed', 'executed', 'refused', 'indeterminate',
|
|
559
|
+
'proved_no_effect', 'invalidated'].includes(execution.status))
|
|
560
|
+
return false;
|
|
561
|
+
const terminalReceipts = program.execution.depends_on.map((stageId) => state.stages[stageId].receipt?.receipt_digest);
|
|
562
|
+
const terminalSatisfied = terminalReceipts.every((entry) => typeof entry === 'string');
|
|
563
|
+
if (state.status === 'active'
|
|
564
|
+
&& ((terminalSatisfied && execution.status === 'locked')
|
|
565
|
+
|| (!terminalSatisfied && execution.status !== 'locked')
|
|
566
|
+
|| execution.status === 'invalidated'))
|
|
567
|
+
return false;
|
|
568
|
+
if (state.status === 'invalidated'
|
|
569
|
+
&& !['invalidated', 'claimed', 'executed', 'refused', 'indeterminate', 'proved_no_effect']
|
|
570
|
+
.includes(execution.status))
|
|
571
|
+
return false;
|
|
572
|
+
const claimedOrTerminal = ['claimed', 'executed', 'refused', 'indeterminate', 'proved_no_effect']
|
|
573
|
+
.includes(execution.status);
|
|
574
|
+
const createdAt = strictInstant(state.created_at);
|
|
575
|
+
const updatedAt = strictInstant(state.updated_at);
|
|
576
|
+
let claimedAt = NaN;
|
|
577
|
+
if (claimedOrTerminal) {
|
|
578
|
+
const binding = execution.authorization_binding;
|
|
579
|
+
claimedAt = strictInstant(execution.claimed_at);
|
|
580
|
+
if (!exactKeys(binding, AUTHORIZATION_BINDING_KEYS)
|
|
581
|
+
|| binding.instance_id !== instanceId
|
|
582
|
+
|| typeof binding.operation_id !== 'string' || !ID.test(binding.operation_id)
|
|
583
|
+
|| binding.program_digest !== programDigest
|
|
584
|
+
|| binding.root_caid !== program.root_caid
|
|
585
|
+
|| binding.action_digest !== program.action_digest
|
|
586
|
+
|| typeof binding.receipt_context_digest !== 'string'
|
|
587
|
+
|| !DIGEST.test(binding.receipt_context_digest)
|
|
588
|
+
|| binding.receipt_context_digest !== digest(receiptContext)
|
|
589
|
+
|| canonicalize(binding.terminal_stage_receipt_digests)
|
|
590
|
+
!== canonicalize([...terminalReceipts].sort())
|
|
591
|
+
|| binding.consequence_mode !== program.execution.consequence_mode
|
|
592
|
+
|| binding.capability_template_digest !== program.execution.capability_template_digest
|
|
593
|
+
|| binding.escrow_profile_digest !== program.execution.escrow_profile_digest
|
|
594
|
+
|| !Number.isFinite(claimedAt) || claimedAt < createdAt || claimedAt > updatedAt)
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
else if (Object.hasOwn(execution, 'authorization_binding')
|
|
598
|
+
|| Object.hasOwn(execution, 'operation_id') || Object.hasOwn(execution, 'claimed_at'))
|
|
599
|
+
return false;
|
|
600
|
+
if (execution.status === 'claimed') {
|
|
601
|
+
if (typeof execution.claim_token_digest !== 'string' || !DIGEST.test(execution.claim_token_digest)
|
|
602
|
+
|| execution.evidence_digest !== null || execution.outcome !== null)
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
else if (execution.claim_token_digest !== null)
|
|
606
|
+
return false;
|
|
607
|
+
const outcomeStatus = ['executed', 'refused', 'indeterminate', 'proved_no_effect']
|
|
608
|
+
.includes(execution.status);
|
|
609
|
+
if (outcomeStatus) {
|
|
610
|
+
if (execution.outcome !== execution.status
|
|
611
|
+
|| typeof execution.evidence_digest !== 'string' || !DIGEST.test(execution.evidence_digest))
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
else if (execution.evidence_digest !== null || execution.outcome !== null)
|
|
615
|
+
return false;
|
|
616
|
+
const hasFinalizedAt = Object.hasOwn(execution, 'finalized_at');
|
|
617
|
+
const hasReconciledAt = Object.hasOwn(execution, 'reconciled_at');
|
|
618
|
+
const finalizedAt = hasFinalizedAt ? strictInstant(execution.finalized_at) : NaN;
|
|
619
|
+
const reconciledAt = hasReconciledAt ? strictInstant(execution.reconciled_at) : NaN;
|
|
620
|
+
if (outcomeStatus) {
|
|
621
|
+
if (!hasFinalizedAt || !Number.isFinite(finalizedAt)
|
|
622
|
+
|| finalizedAt < claimedAt || finalizedAt > updatedAt)
|
|
623
|
+
return false;
|
|
624
|
+
if (execution.status === 'proved_no_effect') {
|
|
625
|
+
if (!hasReconciledAt || !Number.isFinite(reconciledAt)
|
|
626
|
+
|| reconciledAt < finalizedAt || reconciledAt > updatedAt)
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
else if (execution.status === 'executed' && hasReconciledAt) {
|
|
630
|
+
if (!Number.isFinite(reconciledAt)
|
|
631
|
+
|| reconciledAt < finalizedAt || reconciledAt > updatedAt)
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
else if (execution.status !== 'executed' && hasReconciledAt)
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
else if (hasFinalizedAt || hasReconciledAt)
|
|
638
|
+
return false;
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
function assertStoreIdentity(tenantId, instanceId) {
|
|
642
|
+
if (typeof tenantId !== 'string'
|
|
643
|
+
|| Buffer.byteLength(tenantId, 'utf8') < 1
|
|
644
|
+
|| Buffer.byteLength(tenantId, 'utf8') > 512
|
|
645
|
+
|| /[\u0000-\u001f\u007f]/.test(tenantId)) {
|
|
646
|
+
throw new TypeError('trust-program tenantId is invalid');
|
|
647
|
+
}
|
|
648
|
+
if (typeof instanceId !== 'string' || !ID.test(instanceId)) {
|
|
649
|
+
throw new TypeError('trust-program instanceId is invalid');
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* In-process compare-and-swap store. Deliberately rejected by the kernel unless
|
|
654
|
+
* allowEphemeralState is explicit; production must use a durable atomic store.
|
|
655
|
+
*/
|
|
656
|
+
export function createMemoryTrustProgramStore() {
|
|
657
|
+
const records = new Map();
|
|
658
|
+
const storageKey = (tenantId, instanceId) => `${tenantId.length}:${tenantId}${instanceId}`;
|
|
659
|
+
return {
|
|
660
|
+
durable: false,
|
|
661
|
+
async create({ tenantId, state }) {
|
|
662
|
+
assertStoreIdentity(tenantId, state?.instance_id);
|
|
663
|
+
if (state?.tenant_id !== tenantId)
|
|
664
|
+
return fail('state_binding_invalid');
|
|
665
|
+
const key = storageKey(state.tenant_id, state.instance_id);
|
|
666
|
+
if (records.has(key))
|
|
667
|
+
return fail('instance_exists');
|
|
668
|
+
records.set(key, clone(state));
|
|
669
|
+
return { ok: true, state: clone(state) };
|
|
670
|
+
},
|
|
671
|
+
async get({ tenantId, instanceId }) {
|
|
672
|
+
assertStoreIdentity(tenantId, instanceId);
|
|
673
|
+
const state = records.get(storageKey(tenantId, instanceId));
|
|
674
|
+
return state ? { ok: true, state: clone(state) } : fail('instance_not_found');
|
|
675
|
+
},
|
|
676
|
+
async compareAndSwap({ tenantId, instanceId, expectedRevision, state }) {
|
|
677
|
+
assertStoreIdentity(tenantId, instanceId);
|
|
678
|
+
const key = storageKey(tenantId, instanceId);
|
|
679
|
+
const current = records.get(key);
|
|
680
|
+
if (!current)
|
|
681
|
+
return fail('instance_not_found');
|
|
682
|
+
if (state?.tenant_id !== tenantId || state?.instance_id !== instanceId) {
|
|
683
|
+
return fail('state_binding_invalid');
|
|
684
|
+
}
|
|
685
|
+
if (current.revision !== expectedRevision)
|
|
686
|
+
return fail('revision_conflict');
|
|
687
|
+
if (strictInstant(state?.updated_at) < strictInstant(current.updated_at)) {
|
|
688
|
+
return fail('clock_regression');
|
|
689
|
+
}
|
|
690
|
+
records.set(key, clone(state));
|
|
691
|
+
return { ok: true, state: clone(state) };
|
|
692
|
+
},
|
|
693
|
+
async invalidate({ tenantId, instanceId, expectedRevision, reason, at }) {
|
|
694
|
+
assertStoreIdentity(tenantId, instanceId);
|
|
695
|
+
const key = storageKey(tenantId, instanceId);
|
|
696
|
+
const current = records.get(key);
|
|
697
|
+
if (!current)
|
|
698
|
+
return fail('instance_not_found');
|
|
699
|
+
if (current.revision !== expectedRevision)
|
|
700
|
+
return fail('revision_conflict');
|
|
701
|
+
if (current.status === 'invalidated')
|
|
702
|
+
return fail('program_instance_invalidated');
|
|
703
|
+
if (!Number.isSafeInteger(at) || at < strictInstant(current.updated_at)) {
|
|
704
|
+
return fail('clock_regression');
|
|
705
|
+
}
|
|
706
|
+
const next = invalidateState(current, reason, at);
|
|
707
|
+
records.set(key, clone(next));
|
|
708
|
+
return { ok: true, state: clone(next) };
|
|
709
|
+
},
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
function findStage(program, stageId) {
|
|
713
|
+
return program.stages.find((stage) => stage.stage_id === stageId);
|
|
714
|
+
}
|
|
715
|
+
function findRequirement(stage, requirementId) {
|
|
716
|
+
return stage.requirements.find((requirement) => requirement.requirement_id === requirementId);
|
|
717
|
+
}
|
|
718
|
+
function updateUnlocks(state, program) {
|
|
719
|
+
for (const definition of program.stages) {
|
|
720
|
+
const stage = state.stages[definition.stage_id];
|
|
721
|
+
if (stage.status !== 'locked')
|
|
722
|
+
continue;
|
|
723
|
+
const predecessors = definition.depends_on.map((stageId) => state.stages[stageId]);
|
|
724
|
+
if (predecessors.every((entry) => entry.status === 'satisfied')) {
|
|
725
|
+
stage.status = 'collecting';
|
|
726
|
+
stage.predecessor_receipt_digests = predecessors
|
|
727
|
+
.map((entry) => entry.receipt.receipt_digest)
|
|
728
|
+
.sort();
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (program.execution.depends_on.every((stageId) => state.stages[stageId].status === 'satisfied')) {
|
|
732
|
+
state.execution.status = 'ready';
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
function stageIsSatisfied(definition, state) {
|
|
736
|
+
return Object.keys(state.evidence).length >= effectiveRequired(definition);
|
|
737
|
+
}
|
|
738
|
+
function tokenDigest(value) {
|
|
739
|
+
return digest({ token: value });
|
|
740
|
+
}
|
|
741
|
+
export function createTrustProgramKernel(options) {
|
|
742
|
+
const checked = validateTrustProgram(options?.program);
|
|
743
|
+
if (!checked.valid)
|
|
744
|
+
throw new TypeError(`invalid trust program: ${checked.reason}`);
|
|
745
|
+
if (!options.store || typeof options.store.create !== 'function'
|
|
746
|
+
|| typeof options.store.get !== 'function'
|
|
747
|
+
|| typeof options.store.compareAndSwap !== 'function'
|
|
748
|
+
|| typeof options.store.invalidate !== 'function')
|
|
749
|
+
throw new TypeError('trust program store required');
|
|
750
|
+
if (options.store.durable !== true && options.allowEphemeralState !== true) {
|
|
751
|
+
throw new TypeError('durable trust program store required');
|
|
752
|
+
}
|
|
753
|
+
if (!isDataRecord(options.verifiers)
|
|
754
|
+
|| Object.values(options.verifiers).some((verifier) => typeof verifier !== 'function')) {
|
|
755
|
+
throw new TypeError('trust program verifier registry required');
|
|
756
|
+
}
|
|
757
|
+
if ((!options.receiptPrivateKey && typeof options.receiptSigner !== 'function')
|
|
758
|
+
|| !isDataRecord(options.receiptContext)
|
|
759
|
+
|| !exactKeys(options.receiptContext, STAGE_RECEIPT_ISSUER_KEYS)
|
|
760
|
+
|| !Object.values(options.receiptContext).every(boundedContextString)
|
|
761
|
+
|| Buffer.byteLength(options.receiptContext.tenant, 'utf8') > 512) {
|
|
762
|
+
throw new TypeError('stage receipt signer required');
|
|
763
|
+
}
|
|
764
|
+
if (options.allowEphemeralState !== true
|
|
765
|
+
&& (!options.receiptVerificationKey
|
|
766
|
+
|| typeof options.actionBindingVerifier !== 'function'
|
|
767
|
+
|| typeof options.executionBindingVerifier !== 'function'
|
|
768
|
+
|| typeof options.executionEvidenceRevalidator !== 'function'
|
|
769
|
+
|| typeof options.executionOutcomeVerifier !== 'function'
|
|
770
|
+
|| typeof options.reconciliationVerifier !== 'function')) {
|
|
771
|
+
throw new TypeError('production trust verifiers required');
|
|
772
|
+
}
|
|
773
|
+
const program = clone(options.program);
|
|
774
|
+
const programDigest = checked.digest;
|
|
775
|
+
const receiptContext = Object.freeze(clone(options.receiptContext));
|
|
776
|
+
const tenantId = receiptContext.tenant;
|
|
777
|
+
const sourceStore = options.store;
|
|
778
|
+
const store = Object.freeze({
|
|
779
|
+
durable: sourceStore.durable === true,
|
|
780
|
+
create: sourceStore.create.bind(sourceStore),
|
|
781
|
+
get: sourceStore.get.bind(sourceStore),
|
|
782
|
+
compareAndSwap: sourceStore.compareAndSwap.bind(sourceStore),
|
|
783
|
+
invalidate: sourceStore.invalidate.bind(sourceStore),
|
|
784
|
+
});
|
|
785
|
+
const verifiers = Object.freeze({ ...options.verifiers });
|
|
786
|
+
const receiptPrivateKey = options.receiptPrivateKey;
|
|
787
|
+
const receiptVerificationKey = options.receiptVerificationKey;
|
|
788
|
+
const receiptSigner = options.receiptSigner;
|
|
789
|
+
const allowEphemeralState = options.allowEphemeralState === true;
|
|
790
|
+
const actionBindingVerifier = options.actionBindingVerifier;
|
|
791
|
+
const executionBindingVerifier = options.executionBindingVerifier;
|
|
792
|
+
const executionEvidenceRevalidator = options.executionEvidenceRevalidator;
|
|
793
|
+
const executionOutcomeVerifier = options.executionOutcomeVerifier;
|
|
794
|
+
const reconciliationVerifier = options.reconciliationVerifier;
|
|
795
|
+
const now = typeof options.now === 'function' ? options.now : Date.now;
|
|
796
|
+
const clockInstant = () => {
|
|
797
|
+
const current = now();
|
|
798
|
+
return Number.isSafeInteger(current) && current >= 0 && current <= 8_640_000_000_000_000
|
|
799
|
+
? current : NaN;
|
|
800
|
+
};
|
|
801
|
+
const checkedStoreResult = (result, instanceId) => {
|
|
802
|
+
if (!isRecord(result) || typeof result.ok !== 'boolean')
|
|
803
|
+
return fail('store_response_invalid');
|
|
804
|
+
if (result.ok !== true)
|
|
805
|
+
return typeof result.reason === 'string'
|
|
806
|
+
? result : fail('store_response_invalid');
|
|
807
|
+
if (!storedStateValid(result.state, program, programDigest, tenantId, instanceId, receiptContext, receiptVerificationKey))
|
|
808
|
+
return fail('store_state_invalid');
|
|
809
|
+
return result;
|
|
810
|
+
};
|
|
811
|
+
const load = async (instanceId) => {
|
|
812
|
+
try {
|
|
813
|
+
const result = await store.get({ tenantId, instanceId });
|
|
814
|
+
return checkedStoreResult(result, instanceId);
|
|
815
|
+
}
|
|
816
|
+
catch {
|
|
817
|
+
return fail('store_unavailable');
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
const compareAndSwap = async (input) => {
|
|
821
|
+
try {
|
|
822
|
+
const result = await store.compareAndSwap({ ...input, tenantId });
|
|
823
|
+
return checkedStoreResult(result, input.instanceId);
|
|
824
|
+
}
|
|
825
|
+
catch {
|
|
826
|
+
return fail('store_unavailable');
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
const active = (state) => state.status === 'invalidated'
|
|
830
|
+
? fail('program_instance_invalidated') : null;
|
|
831
|
+
const inWindow = (current = clockInstant()) => {
|
|
832
|
+
if (!Number.isFinite(current))
|
|
833
|
+
return fail('clock_invalid');
|
|
834
|
+
if (current < strictInstant(program.valid_from))
|
|
835
|
+
return fail('program_not_yet_valid');
|
|
836
|
+
if (current >= strictInstant(program.expires_at))
|
|
837
|
+
return fail('program_expired');
|
|
838
|
+
return null;
|
|
839
|
+
};
|
|
840
|
+
const transitionInstant = (state) => {
|
|
841
|
+
const current = clockInstant();
|
|
842
|
+
if (!Number.isFinite(current))
|
|
843
|
+
return { ok: false, reason: 'clock_invalid' };
|
|
844
|
+
if (current < strictInstant(state.updated_at)) {
|
|
845
|
+
return { ok: false, reason: 'clock_regression' };
|
|
846
|
+
}
|
|
847
|
+
return { ok: true, at: current };
|
|
848
|
+
};
|
|
849
|
+
async function start({ instanceId, action }) {
|
|
850
|
+
if (typeof instanceId !== 'string' || !ID.test(instanceId))
|
|
851
|
+
return fail('instance_id_invalid');
|
|
852
|
+
const current = clockInstant();
|
|
853
|
+
const windowFailure = inWindow(current);
|
|
854
|
+
if (windowFailure)
|
|
855
|
+
return windowFailure;
|
|
856
|
+
if (typeof actionBindingVerifier === 'function') {
|
|
857
|
+
let actionBound = false;
|
|
858
|
+
try {
|
|
859
|
+
actionBound = await actionBindingVerifier({
|
|
860
|
+
action: clone(action),
|
|
861
|
+
expectedCaid: program.root_caid,
|
|
862
|
+
expectedActionDigest: program.action_digest,
|
|
863
|
+
program: clone(program),
|
|
864
|
+
}) === true;
|
|
865
|
+
}
|
|
866
|
+
catch {
|
|
867
|
+
actionBound = false;
|
|
868
|
+
}
|
|
869
|
+
if (!actionBound)
|
|
870
|
+
return fail('action_binding_invalid');
|
|
871
|
+
}
|
|
872
|
+
else if (!allowEphemeralState) {
|
|
873
|
+
return fail('action_binding_verifier_unavailable');
|
|
874
|
+
}
|
|
875
|
+
try {
|
|
876
|
+
const result = await store.create({
|
|
877
|
+
tenantId,
|
|
878
|
+
state: initialState(program, programDigest, tenantId, instanceId, current),
|
|
879
|
+
});
|
|
880
|
+
return checkedStoreResult(result, instanceId);
|
|
881
|
+
}
|
|
882
|
+
catch {
|
|
883
|
+
return fail('store_unavailable');
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
async function status(instanceId) {
|
|
887
|
+
return load(instanceId);
|
|
888
|
+
}
|
|
889
|
+
async function challenge({ instanceId, stageId, requirementId }) {
|
|
890
|
+
const loaded = await load(instanceId);
|
|
891
|
+
if (!loaded.ok)
|
|
892
|
+
return loaded;
|
|
893
|
+
const stateFailure = active(loaded.state);
|
|
894
|
+
if (stateFailure)
|
|
895
|
+
return stateFailure;
|
|
896
|
+
const windowFailure = inWindow();
|
|
897
|
+
if (windowFailure)
|
|
898
|
+
return windowFailure;
|
|
899
|
+
const definition = findStage(program, stageId);
|
|
900
|
+
if (!definition)
|
|
901
|
+
return fail('stage_unknown');
|
|
902
|
+
const requirement = findRequirement(definition, requirementId);
|
|
903
|
+
if (!requirement)
|
|
904
|
+
return fail('requirement_unknown');
|
|
905
|
+
const stage = loaded.state.stages[stageId];
|
|
906
|
+
if (stage.status === 'locked')
|
|
907
|
+
return fail('stage_locked');
|
|
908
|
+
if (stage.status !== 'collecting')
|
|
909
|
+
return fail('stage_not_collecting');
|
|
910
|
+
if (stage.evidence[requirementId])
|
|
911
|
+
return fail('requirement_already_satisfied');
|
|
912
|
+
const binding = {
|
|
913
|
+
instance_id: instanceId,
|
|
914
|
+
program_digest: programDigest,
|
|
915
|
+
program_version: program.version,
|
|
916
|
+
root_caid: program.root_caid,
|
|
917
|
+
action_digest: program.action_digest,
|
|
918
|
+
stage_id: stageId,
|
|
919
|
+
requirement_id: requirementId,
|
|
920
|
+
policy_digest: requirement.policy_digest,
|
|
921
|
+
predecessor_receipt_digests: [...stage.predecessor_receipt_digests],
|
|
922
|
+
};
|
|
923
|
+
return { ok: true, binding, binding_digest: digest(binding), verifier_profile: requirement.verifier_profile };
|
|
924
|
+
}
|
|
925
|
+
async function admit({ instanceId, stageId, requirementId, artifact }) {
|
|
926
|
+
const loaded = await load(instanceId);
|
|
927
|
+
if (!loaded.ok)
|
|
928
|
+
return loaded;
|
|
929
|
+
const stateFailure = active(loaded.state);
|
|
930
|
+
if (stateFailure)
|
|
931
|
+
return stateFailure;
|
|
932
|
+
const transition = transitionInstant(loaded.state);
|
|
933
|
+
if (!transition.ok)
|
|
934
|
+
return fail(transition.reason);
|
|
935
|
+
const current = transition.at;
|
|
936
|
+
const windowFailure = inWindow(current);
|
|
937
|
+
if (windowFailure)
|
|
938
|
+
return windowFailure;
|
|
939
|
+
const definition = findStage(program, stageId);
|
|
940
|
+
if (!definition)
|
|
941
|
+
return fail('stage_unknown');
|
|
942
|
+
const requirement = findRequirement(definition, requirementId);
|
|
943
|
+
if (!requirement)
|
|
944
|
+
return fail('requirement_unknown');
|
|
945
|
+
const stageState = loaded.state.stages[stageId];
|
|
946
|
+
if (stageState.status === 'locked')
|
|
947
|
+
return fail('stage_locked');
|
|
948
|
+
if (stageState.status !== 'collecting')
|
|
949
|
+
return fail('stage_not_collecting');
|
|
950
|
+
if (!isRecord(artifact) || typeof artifact.evidence_id !== 'string' || !ID.test(artifact.evidence_id)) {
|
|
951
|
+
return fail('evidence_id_invalid');
|
|
952
|
+
}
|
|
953
|
+
if (loaded.state.used_evidence_ids.includes(artifact.evidence_id))
|
|
954
|
+
return fail('evidence_replayed');
|
|
955
|
+
if (stageState.evidence[requirementId])
|
|
956
|
+
return fail('requirement_already_satisfied');
|
|
957
|
+
const verifier = verifiers[requirement.verifier_profile];
|
|
958
|
+
if (typeof verifier !== 'function')
|
|
959
|
+
return fail('verifier_unavailable');
|
|
960
|
+
let verified;
|
|
961
|
+
try {
|
|
962
|
+
verified = await verifier({ artifact: clone(artifact), requirement: clone(requirement), program: clone(program) });
|
|
963
|
+
}
|
|
964
|
+
catch {
|
|
965
|
+
return fail('evidence_verification_failed');
|
|
966
|
+
}
|
|
967
|
+
if (!isRecord(verified) || verified.valid !== true)
|
|
968
|
+
return fail(verified?.reason ?? 'evidence_verification_failed');
|
|
969
|
+
const expectedChallenge = await challenge({ instanceId, stageId, requirementId });
|
|
970
|
+
if (!expectedChallenge.ok)
|
|
971
|
+
return expectedChallenge;
|
|
972
|
+
if (verified.binding_digest !== expectedChallenge.binding_digest)
|
|
973
|
+
return fail('evidence_binding_mismatch');
|
|
974
|
+
if (verified.policy_digest !== requirement.policy_digest)
|
|
975
|
+
return fail('evidence_policy_mismatch');
|
|
976
|
+
const issuedAt = strictInstant(verified.issued_at);
|
|
977
|
+
const expiresAt = strictInstant(verified.expires_at);
|
|
978
|
+
if (!Number.isFinite(issuedAt) || issuedAt > current || current - issuedAt > requirement.max_age_sec * 1000) {
|
|
979
|
+
return fail('evidence_stale');
|
|
980
|
+
}
|
|
981
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= current)
|
|
982
|
+
return fail('evidence_expired');
|
|
983
|
+
const revocationAt = strictInstant(verified.revocation_checked_at);
|
|
984
|
+
if (requirement.revocation_required
|
|
985
|
+
&& (!Number.isFinite(revocationAt) || revocationAt > current
|
|
986
|
+
|| current - revocationAt > requirement.max_age_sec * 1000)) {
|
|
987
|
+
return fail('revocation_check_required');
|
|
988
|
+
}
|
|
989
|
+
if (!boundedProjection(verified.subjects, definition.rule.distinct_subjects)
|
|
990
|
+
|| !boundedProjection(verified.key_fingerprints, definition.rule.distinct_keys)) {
|
|
991
|
+
return fail('evidence_principal_set_invalid');
|
|
992
|
+
}
|
|
993
|
+
const prior = Object.values(stageState.evidence);
|
|
994
|
+
const priorSubjects = new Set(prior.flatMap((entry) => entry.subjects));
|
|
995
|
+
const priorKeys = new Set(prior.flatMap((entry) => entry.key_fingerprints));
|
|
996
|
+
if (definition.rule.distinct_subjects && verified.subjects.some((entry) => priorSubjects.has(entry))) {
|
|
997
|
+
return fail('stage_subject_not_distinct');
|
|
998
|
+
}
|
|
999
|
+
if (definition.rule.distinct_keys && verified.key_fingerprints.some((entry) => priorKeys.has(entry))) {
|
|
1000
|
+
return fail('stage_key_not_distinct');
|
|
1001
|
+
}
|
|
1002
|
+
const next = clone(loaded.state);
|
|
1003
|
+
const nextStage = next.stages[stageId];
|
|
1004
|
+
nextStage.evidence[requirementId] = {
|
|
1005
|
+
evidence_id: artifact.evidence_id,
|
|
1006
|
+
evidence_digest: digest(artifact),
|
|
1007
|
+
policy_digest: requirement.policy_digest,
|
|
1008
|
+
binding_digest: expectedChallenge.binding_digest,
|
|
1009
|
+
subjects: [...verified.subjects].sort(),
|
|
1010
|
+
key_fingerprints: [...verified.key_fingerprints].sort(),
|
|
1011
|
+
issued_at: verified.issued_at,
|
|
1012
|
+
expires_at: verified.expires_at,
|
|
1013
|
+
revocation_checked_at: verified.revocation_checked_at ?? null,
|
|
1014
|
+
};
|
|
1015
|
+
next.used_evidence_ids.push(artifact.evidence_id);
|
|
1016
|
+
next.used_evidence_ids.sort();
|
|
1017
|
+
next.revision += 1;
|
|
1018
|
+
next.updated_at = new Date(current).toISOString();
|
|
1019
|
+
let stageReceipt = null;
|
|
1020
|
+
if (stageIsSatisfied(definition, nextStage)) {
|
|
1021
|
+
nextStage.status = 'satisfied';
|
|
1022
|
+
const receiptPayload = {
|
|
1023
|
+
instance_id: instanceId,
|
|
1024
|
+
program_id: program.program_id,
|
|
1025
|
+
program_version: program.version,
|
|
1026
|
+
program_digest: programDigest,
|
|
1027
|
+
root_caid: program.root_caid,
|
|
1028
|
+
action_digest: program.action_digest,
|
|
1029
|
+
stage_id: stageId,
|
|
1030
|
+
stage_policy_digest: digest(definition),
|
|
1031
|
+
predecessor_receipt_digests: [...nextStage.predecessor_receipt_digests].sort(),
|
|
1032
|
+
evidence_digests: Object.values(nextStage.evidence)
|
|
1033
|
+
.map((entry) => entry.evidence_digest).sort(),
|
|
1034
|
+
subjects: [...new Set(Object.values(nextStage.evidence).flatMap((entry) => entry.subjects))].sort(),
|
|
1035
|
+
key_fingerprints: [...new Set(Object.values(nextStage.evidence).flatMap((entry) => entry.key_fingerprints))].sort(),
|
|
1036
|
+
satisfied_at: new Date(current).toISOString(),
|
|
1037
|
+
};
|
|
1038
|
+
try {
|
|
1039
|
+
stageReceipt = await signStageReceipt(receiptPayload, receiptContext, receiptPrivateKey, receiptSigner);
|
|
1040
|
+
}
|
|
1041
|
+
catch {
|
|
1042
|
+
return fail('stage_receipt_signing_failed');
|
|
1043
|
+
}
|
|
1044
|
+
if (receiptVerificationKey) {
|
|
1045
|
+
const selfCheck = verifyTrustStageReceipt(stageReceipt, {
|
|
1046
|
+
trustedKeys: { [receiptContext.key_id]: receiptVerificationKey },
|
|
1047
|
+
expectedIssuer: receiptContext,
|
|
1048
|
+
expected: receiptPayload,
|
|
1049
|
+
});
|
|
1050
|
+
if (!selfCheck.valid)
|
|
1051
|
+
return fail('stage_receipt_self_verification_failed');
|
|
1052
|
+
}
|
|
1053
|
+
nextStage.receipt = stageReceipt;
|
|
1054
|
+
updateUnlocks(next, program);
|
|
1055
|
+
}
|
|
1056
|
+
const committed = await compareAndSwap({
|
|
1057
|
+
instanceId,
|
|
1058
|
+
expectedRevision: loaded.state.revision,
|
|
1059
|
+
state: next,
|
|
1060
|
+
});
|
|
1061
|
+
if (!committed.ok)
|
|
1062
|
+
return committed;
|
|
1063
|
+
return {
|
|
1064
|
+
ok: true,
|
|
1065
|
+
stage_completed: stageReceipt !== null,
|
|
1066
|
+
stage_receipt: stageReceipt,
|
|
1067
|
+
state: committed.state,
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
async function claimExecution({ instanceId, operationId, claimToken: suppliedClaimToken }) {
|
|
1071
|
+
const loaded = await load(instanceId);
|
|
1072
|
+
if (!loaded.ok)
|
|
1073
|
+
return loaded;
|
|
1074
|
+
const stateFailure = active(loaded.state);
|
|
1075
|
+
if (stateFailure)
|
|
1076
|
+
return stateFailure;
|
|
1077
|
+
const transition = transitionInstant(loaded.state);
|
|
1078
|
+
if (!transition.ok)
|
|
1079
|
+
return fail(transition.reason);
|
|
1080
|
+
const current = transition.at;
|
|
1081
|
+
const windowFailure = inWindow(current);
|
|
1082
|
+
if (windowFailure)
|
|
1083
|
+
return windowFailure;
|
|
1084
|
+
const statusValue = loaded.state.execution.status;
|
|
1085
|
+
if (statusValue === 'locked')
|
|
1086
|
+
return fail('execution_locked');
|
|
1087
|
+
if (statusValue === 'claimed') {
|
|
1088
|
+
if (typeof operationId === 'string' && operationId === loaded.state.execution.operation_id
|
|
1089
|
+
&& typeof suppliedClaimToken === 'string'
|
|
1090
|
+
&& tokenDigest(suppliedClaimToken) === loaded.state.execution.claim_token_digest) {
|
|
1091
|
+
return {
|
|
1092
|
+
ok: true,
|
|
1093
|
+
idempotent: true,
|
|
1094
|
+
claim_token: suppliedClaimToken,
|
|
1095
|
+
authorization_binding: clone(loaded.state.execution.authorization_binding),
|
|
1096
|
+
state: loaded.state,
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
return fail('execution_already_claimed');
|
|
1100
|
+
}
|
|
1101
|
+
if (statusValue === 'indeterminate')
|
|
1102
|
+
return fail('execution_indeterminate');
|
|
1103
|
+
if (['executed', 'proved_no_effect', 'refused'].includes(statusValue))
|
|
1104
|
+
return fail('execution_already_terminal');
|
|
1105
|
+
if (statusValue !== 'ready')
|
|
1106
|
+
return fail('execution_unavailable');
|
|
1107
|
+
for (const definition of program.stages) {
|
|
1108
|
+
const accepted = Object.entries(loaded.state.stages[definition.stage_id].evidence);
|
|
1109
|
+
for (const [requirementId, entry] of accepted) {
|
|
1110
|
+
const requirement = findRequirement(definition, requirementId);
|
|
1111
|
+
const issuedAt = strictInstant(entry.issued_at);
|
|
1112
|
+
const expiresAt = strictInstant(entry.expires_at);
|
|
1113
|
+
const revocationAt = strictInstant(entry.revocation_checked_at);
|
|
1114
|
+
if (!requirement || !Number.isFinite(issuedAt) || !Number.isFinite(expiresAt)
|
|
1115
|
+
|| issuedAt > current || current - issuedAt > requirement.max_age_sec * 1000
|
|
1116
|
+
|| expiresAt <= current
|
|
1117
|
+
|| (requirement.revocation_required
|
|
1118
|
+
&& (!Number.isFinite(revocationAt)
|
|
1119
|
+
|| revocationAt > current
|
|
1120
|
+
|| current - revocationAt > requirement.max_age_sec * 1000))) {
|
|
1121
|
+
return fail('execution_evidence_stale');
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
if (typeof executionEvidenceRevalidator === 'function') {
|
|
1126
|
+
let evidenceStillValid = false;
|
|
1127
|
+
try {
|
|
1128
|
+
evidenceStillValid = await executionEvidenceRevalidator({
|
|
1129
|
+
state: clone(loaded.state),
|
|
1130
|
+
program: clone(program),
|
|
1131
|
+
checkedAt: new Date(current).toISOString(),
|
|
1132
|
+
}) === true;
|
|
1133
|
+
}
|
|
1134
|
+
catch {
|
|
1135
|
+
evidenceStillValid = false;
|
|
1136
|
+
}
|
|
1137
|
+
if (!evidenceStillValid)
|
|
1138
|
+
return fail('execution_evidence_revalidation_failed');
|
|
1139
|
+
}
|
|
1140
|
+
else if (!allowEphemeralState) {
|
|
1141
|
+
return fail('execution_evidence_revalidator_unavailable');
|
|
1142
|
+
}
|
|
1143
|
+
if (store.durable === true
|
|
1144
|
+
&& (typeof operationId !== 'string' || !ID.test(operationId)
|
|
1145
|
+
|| typeof suppliedClaimToken !== 'string' || suppliedClaimToken.length < 32)) {
|
|
1146
|
+
return fail('durable_execution_identity_required');
|
|
1147
|
+
}
|
|
1148
|
+
const claimToken = suppliedClaimToken ?? crypto.randomBytes(32).toString('base64url');
|
|
1149
|
+
if (typeof claimToken !== 'string' || claimToken.length < 32 || claimToken.length > 256) {
|
|
1150
|
+
return fail('execution_claim_token_invalid');
|
|
1151
|
+
}
|
|
1152
|
+
const stableOperationId = operationId ?? `tpo_${crypto.randomUUID()}`;
|
|
1153
|
+
const proposedBinding = {
|
|
1154
|
+
instance_id: instanceId,
|
|
1155
|
+
operation_id: stableOperationId,
|
|
1156
|
+
program_digest: programDigest,
|
|
1157
|
+
root_caid: program.root_caid,
|
|
1158
|
+
action_digest: program.action_digest,
|
|
1159
|
+
receipt_context_digest: digest(receiptContext),
|
|
1160
|
+
terminal_stage_receipt_digests: program.execution.depends_on
|
|
1161
|
+
.map((stageId) => loaded.state.stages[stageId].receipt.receipt_digest).sort(),
|
|
1162
|
+
consequence_mode: program.execution.consequence_mode,
|
|
1163
|
+
capability_template_digest: program.execution.capability_template_digest,
|
|
1164
|
+
escrow_profile_digest: program.execution.escrow_profile_digest,
|
|
1165
|
+
};
|
|
1166
|
+
if (typeof executionBindingVerifier === 'function') {
|
|
1167
|
+
let bindingValid = false;
|
|
1168
|
+
try {
|
|
1169
|
+
bindingValid = await executionBindingVerifier(clone(proposedBinding)) === true;
|
|
1170
|
+
}
|
|
1171
|
+
catch {
|
|
1172
|
+
bindingValid = false;
|
|
1173
|
+
}
|
|
1174
|
+
if (!bindingValid)
|
|
1175
|
+
return fail('execution_binding_invalid');
|
|
1176
|
+
}
|
|
1177
|
+
else if (!allowEphemeralState) {
|
|
1178
|
+
return fail('execution_binding_verifier_unavailable');
|
|
1179
|
+
}
|
|
1180
|
+
const next = clone(loaded.state);
|
|
1181
|
+
next.execution = {
|
|
1182
|
+
...next.execution,
|
|
1183
|
+
status: 'claimed',
|
|
1184
|
+
operation_id: stableOperationId,
|
|
1185
|
+
claim_token_digest: tokenDigest(claimToken),
|
|
1186
|
+
claimed_at: new Date(current).toISOString(),
|
|
1187
|
+
authorization_binding: proposedBinding,
|
|
1188
|
+
};
|
|
1189
|
+
next.revision += 1;
|
|
1190
|
+
next.updated_at = new Date(current).toISOString();
|
|
1191
|
+
const committed = await compareAndSwap({
|
|
1192
|
+
instanceId, expectedRevision: loaded.state.revision, state: next,
|
|
1193
|
+
});
|
|
1194
|
+
if (!committed.ok)
|
|
1195
|
+
return committed;
|
|
1196
|
+
return {
|
|
1197
|
+
ok: true,
|
|
1198
|
+
claim_token: claimToken,
|
|
1199
|
+
authorization_binding: clone(next.execution.authorization_binding),
|
|
1200
|
+
state: committed.state,
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
async function finalizeExecution({ instanceId, claimToken, outcome, evidenceDigest, evidence }) {
|
|
1204
|
+
const loaded = await load(instanceId);
|
|
1205
|
+
if (!loaded.ok)
|
|
1206
|
+
return loaded;
|
|
1207
|
+
const transition = transitionInstant(loaded.state);
|
|
1208
|
+
if (!transition.ok)
|
|
1209
|
+
return fail(transition.reason);
|
|
1210
|
+
const current = transition.at;
|
|
1211
|
+
if (loaded.state.execution.status !== 'claimed') {
|
|
1212
|
+
if (loaded.state.execution.status === 'indeterminate')
|
|
1213
|
+
return fail('execution_indeterminate');
|
|
1214
|
+
if (['executed', 'proved_no_effect', 'refused'].includes(loaded.state.execution.status)) {
|
|
1215
|
+
return fail('execution_already_terminal');
|
|
1216
|
+
}
|
|
1217
|
+
return fail('execution_not_claimed');
|
|
1218
|
+
}
|
|
1219
|
+
if (typeof claimToken !== 'string'
|
|
1220
|
+
|| tokenDigest(claimToken) !== loaded.state.execution.claim_token_digest) {
|
|
1221
|
+
return fail('execution_claim_mismatch');
|
|
1222
|
+
}
|
|
1223
|
+
if (!['executed', 'refused', 'indeterminate'].includes(outcome) || !DIGEST.test(evidenceDigest)) {
|
|
1224
|
+
return fail('execution_outcome_invalid');
|
|
1225
|
+
}
|
|
1226
|
+
if (typeof executionOutcomeVerifier === 'function') {
|
|
1227
|
+
let verified = false;
|
|
1228
|
+
try {
|
|
1229
|
+
verified = await executionOutcomeVerifier({
|
|
1230
|
+
outcome,
|
|
1231
|
+
evidenceDigest,
|
|
1232
|
+
evidence: clone(evidence),
|
|
1233
|
+
authorizationBinding: clone(loaded.state.execution.authorization_binding),
|
|
1234
|
+
state: clone(loaded.state),
|
|
1235
|
+
}) === true;
|
|
1236
|
+
}
|
|
1237
|
+
catch {
|
|
1238
|
+
verified = false;
|
|
1239
|
+
}
|
|
1240
|
+
if (!verified)
|
|
1241
|
+
return fail('execution_evidence_invalid');
|
|
1242
|
+
}
|
|
1243
|
+
else if (!allowEphemeralState) {
|
|
1244
|
+
return fail('execution_outcome_verifier_unavailable');
|
|
1245
|
+
}
|
|
1246
|
+
const next = clone(loaded.state);
|
|
1247
|
+
next.execution.status = outcome;
|
|
1248
|
+
next.execution.outcome = outcome;
|
|
1249
|
+
next.execution.evidence_digest = evidenceDigest;
|
|
1250
|
+
next.execution.claim_token_digest = null;
|
|
1251
|
+
next.execution.finalized_at = new Date(current).toISOString();
|
|
1252
|
+
next.revision += 1;
|
|
1253
|
+
next.updated_at = new Date(current).toISOString();
|
|
1254
|
+
const committed = await compareAndSwap({
|
|
1255
|
+
instanceId, expectedRevision: loaded.state.revision, state: next,
|
|
1256
|
+
});
|
|
1257
|
+
return committed.ok ? { ok: true, state: committed.state } : committed;
|
|
1258
|
+
}
|
|
1259
|
+
async function reconcileExecution(input) {
|
|
1260
|
+
const loaded = await load(input.instanceId);
|
|
1261
|
+
if (!loaded.ok)
|
|
1262
|
+
return loaded;
|
|
1263
|
+
const transition = transitionInstant(loaded.state);
|
|
1264
|
+
if (!transition.ok)
|
|
1265
|
+
return fail(transition.reason);
|
|
1266
|
+
const current = transition.at;
|
|
1267
|
+
if (loaded.state.execution.status !== 'indeterminate') {
|
|
1268
|
+
if (['executed', 'proved_no_effect', 'refused'].includes(loaded.state.execution.status)) {
|
|
1269
|
+
return fail('execution_already_terminal');
|
|
1270
|
+
}
|
|
1271
|
+
return fail('execution_not_indeterminate');
|
|
1272
|
+
}
|
|
1273
|
+
if (!['executed', 'proved_no_effect'].includes(input.outcome) || !DIGEST.test(input.evidenceDigest)) {
|
|
1274
|
+
return fail('reconciliation_outcome_invalid');
|
|
1275
|
+
}
|
|
1276
|
+
if (typeof reconciliationVerifier === 'function') {
|
|
1277
|
+
let verified = false;
|
|
1278
|
+
try {
|
|
1279
|
+
verified = await reconciliationVerifier({
|
|
1280
|
+
...clone(input),
|
|
1281
|
+
state: clone(loaded.state),
|
|
1282
|
+
authorizationBinding: clone(loaded.state.execution.authorization_binding),
|
|
1283
|
+
}) === true;
|
|
1284
|
+
}
|
|
1285
|
+
catch {
|
|
1286
|
+
verified = false;
|
|
1287
|
+
}
|
|
1288
|
+
if (!verified)
|
|
1289
|
+
return fail('reconciliation_evidence_invalid');
|
|
1290
|
+
}
|
|
1291
|
+
else if (!allowEphemeralState) {
|
|
1292
|
+
return fail('reconciliation_verifier_unavailable');
|
|
1293
|
+
}
|
|
1294
|
+
const next = clone(loaded.state);
|
|
1295
|
+
next.execution.status = input.outcome;
|
|
1296
|
+
next.execution.outcome = input.outcome;
|
|
1297
|
+
next.execution.evidence_digest = input.evidenceDigest;
|
|
1298
|
+
next.execution.reconciled_at = new Date(current).toISOString();
|
|
1299
|
+
next.revision += 1;
|
|
1300
|
+
next.updated_at = new Date(current).toISOString();
|
|
1301
|
+
const committed = await compareAndSwap({
|
|
1302
|
+
instanceId: input.instanceId, expectedRevision: loaded.state.revision, state: next,
|
|
1303
|
+
});
|
|
1304
|
+
return committed.ok ? { ok: true, state: committed.state } : committed;
|
|
1305
|
+
}
|
|
1306
|
+
async function invalidate({ instanceId, expectedRevision, reason }) {
|
|
1307
|
+
if (typeof reason !== 'string' || reason.length < 1 || reason.length > 256)
|
|
1308
|
+
return fail('invalidation_reason_invalid');
|
|
1309
|
+
const loaded = await load(instanceId);
|
|
1310
|
+
if (!loaded.ok)
|
|
1311
|
+
return loaded;
|
|
1312
|
+
if (!Number.isSafeInteger(expectedRevision) || loaded.state.revision !== expectedRevision) {
|
|
1313
|
+
return fail('revision_conflict');
|
|
1314
|
+
}
|
|
1315
|
+
if (loaded.state.status === 'invalidated')
|
|
1316
|
+
return fail('program_instance_invalidated');
|
|
1317
|
+
const transition = transitionInstant(loaded.state);
|
|
1318
|
+
if (!transition.ok)
|
|
1319
|
+
return fail(transition.reason);
|
|
1320
|
+
try {
|
|
1321
|
+
const result = await store.invalidate({
|
|
1322
|
+
tenantId, instanceId, expectedRevision, reason, at: transition.at,
|
|
1323
|
+
});
|
|
1324
|
+
return checkedStoreResult(result, instanceId);
|
|
1325
|
+
}
|
|
1326
|
+
catch {
|
|
1327
|
+
return fail('store_unavailable');
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
return Object.freeze({
|
|
1331
|
+
program_digest: programDigest,
|
|
1332
|
+
start,
|
|
1333
|
+
status,
|
|
1334
|
+
challenge,
|
|
1335
|
+
admit,
|
|
1336
|
+
claimExecution,
|
|
1337
|
+
finalizeExecution,
|
|
1338
|
+
reconcileExecution,
|
|
1339
|
+
invalidate,
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
export default {
|
|
1343
|
+
TRUST_PROGRAM_VERSION,
|
|
1344
|
+
TRUST_STAGE_RECEIPT_VERSION,
|
|
1345
|
+
validateTrustProgram,
|
|
1346
|
+
trustProgramDigest,
|
|
1347
|
+
verifyTrustStageReceipt,
|
|
1348
|
+
createMemoryTrustProgramStore,
|
|
1349
|
+
createTrustProgramKernel,
|
|
1350
|
+
};
|
|
1351
|
+
//# sourceMappingURL=trust-program.js.map
|