@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
package/index.js
CHANGED
|
@@ -1,1249 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
* Deny-by-default enforcement for consequential machine actions. A guarded
|
|
6
|
-
* action runs ONLY if it arrives with a receipt that is:
|
|
7
|
-
* 1. valid — Ed25519 over canonical JSON, signed by a pinned issuer;
|
|
8
|
-
* 2. in-scope — bound to the exact action the manifest guards;
|
|
9
|
-
* 3. sufficiently — meets the action's required assurance tier, and the
|
|
10
|
-
* assured credited tier is CRYPTOGRAPHICALLY VERIFIED, not read
|
|
11
|
-
* from self-asserted payload fields: class_a requires a
|
|
12
|
-
* valid WebAuthn device signoff, quorum requires a valid
|
|
13
|
-
* EP-QUORUM-v1 (distinct humans + distinct keys +
|
|
14
|
-
* threshold + per-signer assertions);
|
|
15
|
-
* 4. fresh — within max age; and
|
|
16
|
-
* 5. unused — not a replay (one-time consumption).
|
|
17
|
-
* Otherwise it is refused with a machine-readable Receipt-Required challenge
|
|
18
|
-
* (HTTP 428). Every decision is appended to a tamper-evident evidence log.
|
|
19
|
-
*
|
|
20
|
-
* It is NOT authentication ("who are you") and NOT permissions ("are you
|
|
21
|
-
* allowed here"). It is a policy-enforcement point that requires portable proof
|
|
22
|
-
* a named human authorized THIS exact action before the world is mutated.
|
|
23
|
-
*
|
|
24
|
-
* Composes @emilia-protocol/require-receipt (manifest + verify + challenge) and
|
|
25
|
-
* adds the three things a firewall needs over a bare verifier: assurance-tier
|
|
26
|
-
* enforcement, replay defense, and the evidence log. Fails closed.
|
|
27
|
-
*/
|
|
28
|
-
import crypto from 'node:crypto';
|
|
29
|
-
import {
|
|
30
|
-
verifyEmiliaReceipt,
|
|
31
|
-
receiptChallenge,
|
|
32
|
-
receiptRequiredHeader,
|
|
33
|
-
validateActionRiskManifest,
|
|
34
|
-
findActionRequirement,
|
|
35
|
-
evaluateReceiptAssurance,
|
|
36
|
-
validatePinnedQuorumPolicy,
|
|
37
|
-
receiptAssuranceTier as receiptAssuranceTierFromProof,
|
|
38
|
-
parseReceiptCarrier,
|
|
39
|
-
RECEIPT_REQUIRED_STATUS,
|
|
40
|
-
RECEIPT_REQUIRED_HEADER,
|
|
41
|
-
} from '@emilia-protocol/require-receipt';
|
|
42
|
-
import { verifyWebAuthnSignoff, verifyQuorum } from '@emilia-protocol/verify';
|
|
43
|
-
import { MemoryConsumptionStore, isSecureConsumptionStore } from './store.js';
|
|
44
|
-
import {
|
|
45
|
-
canonicalEvidenceJson,
|
|
46
|
-
createAtomicEvidenceLog,
|
|
47
|
-
createEvidenceLog,
|
|
48
|
-
createMemoryAtomicEvidenceBackend,
|
|
49
|
-
} from './evidence.js';
|
|
50
|
-
import { DEFAULT_GATE_MANIFEST, HIGH_RISK_ACTION_PACKS, createDefaultActionRiskManifest } from './action-packs.js';
|
|
51
|
-
import { hashCanonical, verifyExecutionBinding } from './execution-binding.js';
|
|
52
|
-
import { buildReliancePacket, ADMISSIBILITY_VERDICTS } from './reliance-packet.js';
|
|
53
|
-
import { createEg1Harness, makeGateInvoke, runEg1, EG1_DEFAULT_SELECTOR, mintDeviceSignoff, mintQuorumEvidence } from './eg1-conformance.js';
|
|
54
|
-
import { CF1_VERSION, CF1_CHECKS, runCf1 } from './cf1-conformance.js';
|
|
55
|
-
import { createKeyRegistry, asKeyRegistry } from './key-registry.js';
|
|
56
|
-
import { classifyRetention, buildRetentionExport } from './retention.js';
|
|
57
|
-
import { createDefaultActionControlManifest, findActionControl, validateActionControlManifest } from './action-control-manifest.js';
|
|
58
|
-
import {
|
|
59
|
-
mintBreakGlassAuthorization,
|
|
60
|
-
verifyBreakGlass,
|
|
61
|
-
consumeBreakGlass,
|
|
62
|
-
buildBreakGlassEvidence,
|
|
63
|
-
runBreakGlass,
|
|
64
|
-
BREAKGLASS_VERSION,
|
|
65
|
-
BREAKGLASS_EVIDENCE_KIND,
|
|
66
|
-
} from './breakglass.js';
|
|
67
|
-
|
|
68
|
-
export {
|
|
69
|
-
MemoryConsumptionStore,
|
|
70
|
-
canonicalEvidenceJson,
|
|
71
|
-
createEvidenceLog,
|
|
72
|
-
createAtomicEvidenceLog,
|
|
73
|
-
createMemoryAtomicEvidenceBackend,
|
|
74
|
-
};
|
|
75
|
-
export {
|
|
76
|
-
createDurableConsumptionStore,
|
|
77
|
-
createMemoryBackend,
|
|
78
|
-
isSecureConsumptionStore,
|
|
79
|
-
DURABLE_CONSUMPTION_VERSION,
|
|
80
|
-
} from './store.js';
|
|
81
|
-
export { createDurableChallengeStore, challengeStorageKey, challengeBodyDigest, DURABLE_CHALLENGE_STORE_VERSION } from './challenge-store.js';
|
|
82
|
-
export { createKeyRegistry, asKeyRegistry } from './key-registry.js';
|
|
83
|
-
export { classifyRetention, buildRetentionExport, RETENTION_EXPORT_VERSION } from './retention.js';
|
|
84
|
-
export { DEFAULT_GATE_MANIFEST, HIGH_RISK_ACTION_PACKS, createDefaultActionRiskManifest };
|
|
85
|
-
export {
|
|
86
|
-
ACTION_CONTROL_MANIFEST_VERSION,
|
|
87
|
-
ACTION_CONTROL_SCHEMA_URL,
|
|
88
|
-
ACTION_CONTROL_CONFORMANCE_LEVEL,
|
|
89
|
-
ACTION_CONTROL_DEFAULTS,
|
|
90
|
-
ACTION_CONTROL_EVIDENCE_PROFILES,
|
|
91
|
-
ACTION_CONTROL_CONFORMANCE_CHECKS,
|
|
92
|
-
toActionControl,
|
|
93
|
-
createDefaultActionControlManifest,
|
|
94
|
-
findActionControl,
|
|
95
|
-
validateActionControlManifest,
|
|
96
|
-
} from './action-control-manifest.js';
|
|
97
|
-
export { EXECUTION_BINDING_VERSION, canonicalize, hashCanonical, materialFieldsFor, verifyExecutionBinding } from './execution-binding.js';
|
|
98
|
-
export { RELIANCE_PACKET_VERSION, ADMISSIBILITY_VERDICTS, buildReliancePacket } from './reliance-packet.js';
|
|
99
|
-
export {
|
|
100
|
-
EXTERNAL_VERIFICATION_STATEMENT_VERSION,
|
|
101
|
-
EXTERNAL_VERIFICATION_DOMAIN,
|
|
102
|
-
externalVerificationDigest,
|
|
103
|
-
signExternalVerificationStatement,
|
|
104
|
-
verifyExternalVerificationStatement,
|
|
105
|
-
} from './reports/external-verification.js';
|
|
106
|
-
export {
|
|
107
|
-
EG1_VERSION, EG1_CHECKS, EG1_DEFAULT_ACTION, EG1_DEFAULT_SELECTOR,
|
|
108
|
-
createEg1Harness, makeGateInvoke, runEg1, mintDeviceSignoff, mintQuorumEvidence,
|
|
109
|
-
} from './eg1-conformance.js';
|
|
110
|
-
export { CF1_VERSION, CF1_CHECKS, runCf1 } from './cf1-conformance.js';
|
|
111
|
-
export {
|
|
112
|
-
mintBreakGlassAuthorization,
|
|
113
|
-
verifyBreakGlass,
|
|
114
|
-
consumeBreakGlass,
|
|
115
|
-
buildBreakGlassEvidence,
|
|
116
|
-
runBreakGlass,
|
|
117
|
-
BREAKGLASS_VERSION,
|
|
118
|
-
BREAKGLASS_EVIDENCE_KIND,
|
|
119
|
-
};
|
|
120
|
-
export const ASSURANCE_TIERS = ['software', 'class_a', 'quorum'];
|
|
121
|
-
const TIER_RANK = { software: 0, class_a: 1, quorum: 2 };
|
|
122
|
-
|
|
123
|
-
function safeCanonicalHash(value) {
|
|
124
|
-
try { return hashCanonical(value); } catch { return null; }
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Structurally compare a PRE-COMPUTED admissibility block with a profile hash.
|
|
129
|
-
* This helper does NOT authenticate the block or establish evaluator provenance.
|
|
130
|
-
* An execution gate must first verify a signature over the packet or recompute
|
|
131
|
-
* the verdict from trusted evidence. createGate enforces that boundary through
|
|
132
|
-
* its verifyAdmissibilityPacket callback whenever a profile is pinned.
|
|
133
|
-
*
|
|
134
|
-
* @param {{id?:string, profile_hash:string}} pinned the profile the relying party requires
|
|
135
|
-
* @param {object|null} presented a reliance packet, or its `.admissibility` block,
|
|
136
|
-
* as produced by buildReliancePacket / the relying party's evaluator
|
|
137
|
-
* @returns {{ok:boolean, reason:string|null, pinned_hash:string, presented_hash:string|null, verdict:string|null}}
|
|
138
|
-
* ok:true ONLY when the presented profile_hash equals the pinned hash AND the
|
|
139
|
-
* verdict is exactly 'admissible'. Every other case fails closed with a distinct reason.
|
|
140
|
-
*/
|
|
141
|
-
export function verifyAdmissibilityAgainstPinnedProfile(pinned, presented) {
|
|
142
|
-
const pinnedHash = pinned && typeof pinned.profile_hash === 'string' ? pinned.profile_hash : null;
|
|
143
|
-
// A pin with no hash is a misconfiguration: refuse, do not silently pass.
|
|
144
|
-
if (!pinnedHash) {
|
|
145
|
-
return { ok: false, reason: 'pinned_profile_missing_hash', pinned_hash: null, presented_hash: null, verdict: null };
|
|
146
|
-
}
|
|
147
|
-
// Accept either a full reliance packet (has .admissibility) or the block itself.
|
|
148
|
-
const adm = presented && typeof presented === 'object'
|
|
149
|
-
? (presented.admissibility !== undefined ? presented.admissibility : presented)
|
|
150
|
-
: null;
|
|
151
|
-
if (!adm || typeof adm !== 'object') {
|
|
152
|
-
return { ok: false, reason: 'admissibility_profile_pinned_but_absent', pinned_hash: pinnedHash, presented_hash: null, verdict: null };
|
|
153
|
-
}
|
|
154
|
-
const presentedHash = typeof adm.profile_hash === 'string' ? adm.profile_hash : null;
|
|
155
|
-
const verdict = typeof adm.verdict === 'string' ? adm.verdict : null;
|
|
156
|
-
// Constant-work equality is unnecessary (hashes are public), but the mismatch
|
|
157
|
-
// MUST be a distinct, named refusal: a presented verdict for a DIFFERENT bar is
|
|
158
|
-
// not evidence about the pinned bar.
|
|
159
|
-
if (presentedHash === null || presentedHash !== pinnedHash) {
|
|
160
|
-
return { ok: false, reason: 'profile_hash_mismatch', pinned_hash: pinnedHash, presented_hash: presentedHash, verdict };
|
|
161
|
-
}
|
|
162
|
-
// Verdict must be recognized AND exactly 'admissible'. Any other closed-set
|
|
163
|
-
// member (missing_evidence/stale/conflicted/unverifiable), an unrecognized
|
|
164
|
-
// string, or a missing verdict fails closed and names the verdict it saw.
|
|
165
|
-
if (verdict === null || !ADMISSIBILITY_VERDICTS.includes(verdict)) {
|
|
166
|
-
return { ok: false, reason: 'admissibility_verdict_unrecognized', pinned_hash: pinnedHash, presented_hash: presentedHash, verdict };
|
|
167
|
-
}
|
|
168
|
-
if (verdict !== 'admissible') {
|
|
169
|
-
return { ok: false, reason: `admissibility_not_admissible:${verdict}`, pinned_hash: pinnedHash, presented_hash: presentedHash, verdict };
|
|
170
|
-
}
|
|
171
|
-
return { ok: true, reason: null, pinned_hash: pinnedHash, presented_hash: presentedHash, verdict };
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* The assurance tier a receipt has CRYPTOGRAPHICALLY EARNED.
|
|
176
|
-
*
|
|
177
|
-
* SECURITY: the credited tier is NEVER inferred from self-asserted payload
|
|
178
|
-
* fields. A bare `quorum:{signers,threshold}` block or an `outcome:
|
|
179
|
-
* 'allow_with_signoff'` string with no verifiable signature earns only
|
|
180
|
-
* `software` — it will be refused `assurance_too_low` by any guard that needs
|
|
181
|
-
* more. Fail-closed by construction.
|
|
182
|
-
*
|
|
183
|
-
* Two independent cryptographic proof shapes are accepted; a receipt earns the
|
|
184
|
-
* HIGHEST tier any of them proves:
|
|
185
|
-
*
|
|
186
|
-
* (a) Pinned assurance proof (`payload.assurance_proof`, EP-ASSURANCE-PROOF-v1):
|
|
187
|
-
* per-signer signatures verified against PINNED approver keys (opts.approverKeys)
|
|
188
|
-
* or a caller-supplied verifier (opts.verifyAssurance). This is the primary,
|
|
189
|
-
* strongest model — the verifier never trusts a key that travels inside the
|
|
190
|
-
* receipt. Delegated to require-receipt's receiptAssuranceTierFromProof.
|
|
191
|
-
*
|
|
192
|
-
* (b) Embedded evidence (DoD audit fix): a full EP-QUORUM-v1
|
|
193
|
-
* document (payload.quorum) whose per-signer WebAuthn assertions verify via
|
|
194
|
-
* verifyQuorum (distinct humans + distinct keys + threshold + action-binding
|
|
195
|
-
* + window) earns `quorum`; a WebAuthn device signoff (payload.signoff =
|
|
196
|
-
* {context, webauthn}) that verifies against the approver's own key via
|
|
197
|
-
* verifyWebAuthnSignoff earns `class_a`. Quorum additionally requires an
|
|
198
|
-
* out-of-band organizational policy and identity-bound approver directory.
|
|
199
|
-
*
|
|
200
|
-
* TRUST-LAUNDERING GUARD: an approver key carried INSIDE the receipt proves
|
|
201
|
-
* only that whoever minted the receipt also holds that key — it is NOT proof
|
|
202
|
-
* the relying party trusts that human. Crediting an elevated tier off such a
|
|
203
|
-
* key would collapse VERIFIED into ACCEPTED (any party can mint a fresh
|
|
204
|
-
* keypair, self-sign a signoff, and embed both). So path (b) elevates the
|
|
205
|
-
* tier ONLY when either: (i) the caller explicitly opts in with
|
|
206
|
-
* `allowEmbeddedApproverKeys:true` for a single Class-A integrity demo
|
|
207
|
-
* (DEFAULT OFF); or (ii) the embedded approver key that would earn the
|
|
208
|
-
* credit is present in the relying party's PINNED approver key set
|
|
209
|
-
* (opts.approverKeys). With no pin and no opt-in, path (b) may still VERIFY
|
|
210
|
-
* the signoff/quorum, but it does NOT elevate above `software`. Fail-closed.
|
|
211
|
-
*
|
|
212
|
-
* @param {object} doc the EP-RECEIPT-v1 document
|
|
213
|
-
* @param {object} [opts]
|
|
214
|
-
* @param {object} [opts.approverKeys] pinned approver keys for path (a) and the
|
|
215
|
-
* path-(b) fallback: a receipt-embedded approver key elevates the tier only if
|
|
216
|
-
* it is one of these pinned keys (unless allowEmbeddedApproverKeys is set)
|
|
217
|
-
* @param {boolean} [opts.allowEmbeddedApproverKeys=false] explicit opt-in where
|
|
218
|
-
* one unpinned embedded key may earn Class-A integrity. It never earns quorum.
|
|
219
|
-
* @param {object} [opts.quorumPolicy] relying-party-pinned organizational rule
|
|
220
|
-
* @param {function} [opts.verifyAssurance] custom assurance verifier for path (a)
|
|
221
|
-
* @param {string} [opts.rpId] bind embedded device assertions to this WebAuthn RP id (path b)
|
|
222
|
-
* @param {boolean} [opts.detail] return a {tier, quorum, signoff} object instead of the string
|
|
223
|
-
* @returns {'software'|'class_a'|'quorum'|object} the highest tier proven
|
|
224
|
-
*/
|
|
225
|
-
export function receiptAssuranceTier(doc, opts = {}) {
|
|
226
|
-
const detail = { tier: 'software', quorum: null, signoff: null };
|
|
227
|
-
|
|
228
|
-
// --- Path (a): pinned assurance proof / caller-supplied verifier. ---
|
|
229
|
-
// Never inferred from receipt fields without a pinned key or explicit verifier.
|
|
230
|
-
let proofTier = 'software';
|
|
231
|
-
try {
|
|
232
|
-
proofTier = receiptAssuranceTierFromProof(doc, opts) || 'software';
|
|
233
|
-
} catch { proofTier = 'software'; }
|
|
234
|
-
if ((TIER_RANK[proofTier] ?? 0) > (TIER_RANK[detail.tier] ?? 0)) detail.tier = proofTier;
|
|
235
|
-
|
|
236
|
-
// --- Path (b): self-contained embedded per-signer evidence (DoD audit fix). ---
|
|
237
|
-
const p = doc?.payload || {};
|
|
238
|
-
const verifyOpts = {
|
|
239
|
-
...(opts.rpId ? { rpId: opts.rpId } : {}),
|
|
240
|
-
...(Array.isArray(opts.allowedOrigins) ? { allowedOrigins: opts.allowedOrigins } : {}),
|
|
241
|
-
};
|
|
242
|
-
// The relying party's PINNED approver public keys (base64url SPKI-DER strings).
|
|
243
|
-
// An embedded approver key elevates the tier only if it is in this set, unless
|
|
244
|
-
// the caller explicitly opts into the self-contained mode.
|
|
245
|
-
const allowEmbedded = opts.allowEmbeddedApproverKeys === true;
|
|
246
|
-
const keyIsTrusted = (k, approver) => allowEmbedded
|
|
247
|
-
|| Boolean(findPinnedApproverKey(opts.approverKeys, k, approver));
|
|
248
|
-
|
|
249
|
-
// quorum: a real, self-contained EP-QUORUM-v1 evidence document. Accept it
|
|
250
|
-
// under payload.quorum or payload.claim.quorum. It only counts if it is a full
|
|
251
|
-
// quorum document (policy + members with WebAuthn signoffs) AND verifyQuorum
|
|
252
|
-
// returns valid. A bare {signers,threshold} block has no members to verify and
|
|
253
|
-
// therefore CANNOT be credited quorum. The cryptographic verification runs
|
|
254
|
-
// regardless (so `detail.quorum` reports validity), but the tier elevates only
|
|
255
|
-
// when every member's embedded approver key is pinned (or the caller opted in).
|
|
256
|
-
const q = p.quorum || p.claim?.quorum;
|
|
257
|
-
if (detail.tier !== 'quorum' && isQuorumEvidence(q)) {
|
|
258
|
-
const policy = validatePinnedQuorumPolicy(opts.quorumPolicy || opts.quorum_policy);
|
|
259
|
-
const trustedMembers = Array.isArray(q.members) ? q.members.map((member) => {
|
|
260
|
-
const entry = findPinnedApproverKey(
|
|
261
|
-
opts.approverKeys,
|
|
262
|
-
member?.approver_public_key,
|
|
263
|
-
member?.signoff?.context?.approver,
|
|
264
|
-
);
|
|
265
|
-
return entry ? { ...member, approver_public_key: entry.public_key } : null;
|
|
266
|
-
}) : [];
|
|
267
|
-
const membersTrusted = trustedMembers.length > 0 && trustedMembers.every(Boolean);
|
|
268
|
-
const qr = policy.ok && membersTrusted
|
|
269
|
-
? verifyQuorum({ ...q, policy: policy.policy, members: trustedMembers }, verifyOpts)
|
|
270
|
-
: { valid: false, checks: {} };
|
|
271
|
-
detail.quorum = {
|
|
272
|
-
valid: qr.valid,
|
|
273
|
-
checks: qr.checks,
|
|
274
|
-
policy_pinned: policy.ok,
|
|
275
|
-
embedded_keys_trusted: membersTrusted,
|
|
276
|
-
approvers: qr.valid
|
|
277
|
-
? trustedMembers.map((member) => member?.signoff?.context?.approver).filter(nonEmptyString)
|
|
278
|
-
: [],
|
|
279
|
-
roles: qr.valid
|
|
280
|
-
? trustedMembers.map((member) => ({
|
|
281
|
-
subject: member?.signoff?.context?.approver ?? null,
|
|
282
|
-
role: member?.role ?? null,
|
|
283
|
-
}))
|
|
284
|
-
: [],
|
|
285
|
-
refusal: !policy.ok ? policy.reason : (!membersTrusted ? 'quorum_member_key_unpinned' : null),
|
|
286
|
-
};
|
|
287
|
-
if (qr.valid && policy.ok && membersTrusted) detail.tier = 'quorum';
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// class_a: a verifiable WebAuthn device signoff. The signoff evidence is
|
|
291
|
-
// {context, webauthn}; the approver key travels with it (signoff.approver_public_key)
|
|
292
|
-
// or alongside it (payload.approver_public_key). That key elevates the tier only
|
|
293
|
-
// when it is pinned by the relying party (or the caller opted into embedded keys).
|
|
294
|
-
if ((TIER_RANK[detail.tier] ?? 0) < TIER_RANK.class_a) {
|
|
295
|
-
const so = p.signoff || p.claim?.signoff;
|
|
296
|
-
if (isSignoffEvidence(so)) {
|
|
297
|
-
const key = so.approver_public_key || p.approver_public_key || p.claim?.approver_public_key;
|
|
298
|
-
if (key) {
|
|
299
|
-
const sr = verifyWebAuthnSignoff(so, key, verifyOpts);
|
|
300
|
-
const trusted = keyIsTrusted(key, so?.context?.approver);
|
|
301
|
-
detail.signoff = {
|
|
302
|
-
valid: sr.valid,
|
|
303
|
-
checks: sr.checks,
|
|
304
|
-
embedded_key_trusted: trusted,
|
|
305
|
-
approver: so?.context?.approver ?? null,
|
|
306
|
-
};
|
|
307
|
-
if (sr.valid && trusted && (TIER_RANK[detail.tier] ?? 0) < TIER_RANK.class_a) detail.tier = 'class_a';
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return opts.detail ? detail : detail.tier;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* The set of PINNED approver public keys (base64url SPKI-DER strings) a relying
|
|
317
|
-
* party trusts, from the same `approverKeys` map path (a) uses. Accepts either a
|
|
318
|
-
* map of { keyId: { public_key } } (the EP-ASSURANCE-PROOF-v1 shape) or a plain
|
|
319
|
-
* array/set of key strings. Used to decide whether a receipt-embedded approver
|
|
320
|
-
* key may elevate the path-(b) tier. Never throws.
|
|
321
|
-
*/
|
|
322
|
-
function spkiFingerprint(value) {
|
|
323
|
-
try {
|
|
324
|
-
const key = crypto.createPublicKey({ key: Buffer.from(value, 'base64url'), format: 'der', type: 'spki' });
|
|
325
|
-
return crypto.createHash('sha256').update(key.export({ type: 'spki', format: 'der' })).digest('hex');
|
|
326
|
-
} catch { return null; }
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function findPinnedApproverKey(approverKeys, presentedKey, approver) {
|
|
330
|
-
if (!approverKeys || typeof approverKeys !== 'object' || Array.isArray(approverKeys)) return null;
|
|
331
|
-
const presentedFingerprint = spkiFingerprint(presentedKey);
|
|
332
|
-
if (!presentedFingerprint || typeof approver !== 'string' || !approver) return null;
|
|
333
|
-
const matches = Object.values(approverKeys).filter((entry) => entry && typeof entry === 'object'
|
|
334
|
-
&& entry.approver_id === approver
|
|
335
|
-
&& spkiFingerprint(entry.public_key) === presentedFingerprint);
|
|
336
|
-
return matches.length === 1 ? matches[0] : null;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/** A quorum evidence doc must carry members with per-signer signoffs to be verifiable. */
|
|
340
|
-
function isQuorumEvidence(q) {
|
|
341
|
-
return !!q && typeof q === 'object' && q.policy && Array.isArray(q.members) && q.members.length > 0
|
|
342
|
-
&& typeof q.action_hash === 'string' && q.action_hash.length > 0;
|
|
343
|
-
}
|
|
344
|
-
/** A device signoff must carry the WebAuthn assertion material to be verifiable. */
|
|
345
|
-
function isSignoffEvidence(s) {
|
|
346
|
-
return !!s && typeof s === 'object' && s.context && s.webauthn;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function nonEmptyString(value) {
|
|
350
|
-
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function businessAuthorizationSource(requirement) {
|
|
354
|
-
if (!requirement || typeof requirement !== 'object') return null;
|
|
355
|
-
const nested = requirement.business_authorization
|
|
356
|
-
|| requirement.businessAuthorization
|
|
357
|
-
|| requirement.authorization_requirement
|
|
358
|
-
|| requirement.authorizationRequirement
|
|
359
|
-
|| requirement.authorization_policy
|
|
360
|
-
|| requirement.authorizationPolicy
|
|
361
|
-
|| requirement.business_policy
|
|
362
|
-
|| requirement.control?.business_authorization
|
|
363
|
-
|| requirement.control?.authorization_requirement
|
|
364
|
-
|| requirement.control?.authorization_policy;
|
|
365
|
-
if (nested && typeof nested === 'object' && !Array.isArray(nested)) return nested;
|
|
366
|
-
const hasDirect = [
|
|
367
|
-
'policy', 'business_policy', 'policy_id', 'policy_hash', 'tenant_id', 'allowed_approvers',
|
|
368
|
-
'allowed_approver_subjects', 'allowed_approver_roles',
|
|
369
|
-
].some((field) => Object.prototype.hasOwnProperty.call(requirement, field));
|
|
370
|
-
return hasDirect ? requirement : null;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Normalize the relying party's business-authorization pin for one action.
|
|
375
|
-
*
|
|
376
|
-
* Canonical manifest shape:
|
|
377
|
-
* business_authorization: {
|
|
378
|
-
* policy: { id, hash }, tenant_id,
|
|
379
|
-
* allowed_approvers: [{ subject, role }]
|
|
380
|
-
* }
|
|
381
|
-
*
|
|
382
|
-
* Flat policy_id/policy_hash and approver aliases are accepted so an existing
|
|
383
|
-
* manifest can add the control without changing its surrounding schema. Once
|
|
384
|
-
* any part is configured, every part is required; a partial pin is invalid.
|
|
385
|
-
*/
|
|
386
|
-
export function businessAuthorizationRequirement(requirement) {
|
|
387
|
-
const source = businessAuthorizationSource(requirement);
|
|
388
|
-
if (!source) {
|
|
389
|
-
return {
|
|
390
|
-
configured: false, ok: true, reason: null,
|
|
391
|
-
policy_id: null, policy_hash: null, tenant_id: null, allowed_approvers: [],
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
const policy = source.policy && typeof source.policy === 'object' && !Array.isArray(source.policy)
|
|
395
|
-
? source.policy
|
|
396
|
-
: (source.business_policy && typeof source.business_policy === 'object' && !Array.isArray(source.business_policy)
|
|
397
|
-
? source.business_policy : {});
|
|
398
|
-
const root = requirement && typeof requirement === 'object' ? requirement : {};
|
|
399
|
-
const policyId = nonEmptyString(source.policy_id ?? source.id ?? policy.policy_id ?? policy.id ?? root.policy_id);
|
|
400
|
-
const policyHash = nonEmptyString(source.policy_hash ?? source.hash ?? policy.policy_hash ?? policy.hash ?? root.policy_hash);
|
|
401
|
-
const tenantId = nonEmptyString(
|
|
402
|
-
source.tenant_id ?? source.tenant ?? source.organization_id
|
|
403
|
-
?? root.tenant_id ?? root.tenant ?? root.organization_id,
|
|
404
|
-
);
|
|
405
|
-
const rawApprovers = source.allowed_approvers ?? source.approvers ?? source.approver_roster
|
|
406
|
-
?? root.allowed_approvers ?? root.approvers ?? root.approver_roster;
|
|
407
|
-
const subjects = source.allowed_approver_subjects ?? source.approver_subjects
|
|
408
|
-
?? root.allowed_approver_subjects ?? root.approver_subjects;
|
|
409
|
-
const roles = source.allowed_approver_roles ?? source.approver_roles
|
|
410
|
-
?? root.allowed_approver_roles ?? root.approver_roles;
|
|
411
|
-
let allowed = [];
|
|
412
|
-
if (Array.isArray(rawApprovers)) {
|
|
413
|
-
allowed = rawApprovers.map((entry) => {
|
|
414
|
-
if (typeof entry === 'string') return { subject: entry, role: null };
|
|
415
|
-
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return { subject: null, role: null };
|
|
416
|
-
return {
|
|
417
|
-
subject: nonEmptyString(entry.subject ?? entry.approver ?? entry.principal_id),
|
|
418
|
-
role: nonEmptyString(entry.role ?? entry.approver_role),
|
|
419
|
-
};
|
|
420
|
-
});
|
|
421
|
-
} else if (Array.isArray(subjects)) {
|
|
422
|
-
const validRoles = Array.isArray(roles) ? roles.filter((role) => nonEmptyString(role)) : [];
|
|
423
|
-
allowed = subjects.flatMap((subject) => {
|
|
424
|
-
if (!validRoles.length) return [{ subject: nonEmptyString(subject), role: null }];
|
|
425
|
-
return validRoles.map((role) => ({ subject: nonEmptyString(subject), role }));
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
const malformed = !policyId || !policyHash || !tenantId || allowed.length === 0
|
|
429
|
-
|| allowed.some((entry) => !entry.subject || !entry.role);
|
|
430
|
-
const duplicate = new Set(allowed.map((entry) => `${entry.subject}\u0000${entry.role}`)).size !== allowed.length;
|
|
431
|
-
return {
|
|
432
|
-
configured: true,
|
|
433
|
-
ok: !malformed && !duplicate,
|
|
434
|
-
reason: malformed ? 'business_authorization_incomplete'
|
|
435
|
-
: (duplicate ? 'business_authorization_duplicate_approver' : null),
|
|
436
|
-
policy_id: policyId,
|
|
437
|
-
policy_hash: policyHash,
|
|
438
|
-
tenant_id: tenantId,
|
|
439
|
-
allowed_approvers: allowed,
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function signedString(candidates) {
|
|
444
|
-
const present = candidates.filter((value) => value !== undefined && value !== null);
|
|
445
|
-
if (!present.length) return { ok: true, value: null };
|
|
446
|
-
if (present.some((value) => !nonEmptyString(value))) return { ok: false, value: null };
|
|
447
|
-
const distinct = [...new Set(present)];
|
|
448
|
-
return distinct.length === 1 ? { ok: true, value: distinct[0] } : { ok: false, value: null };
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
function receiptRoleAssertions(receipt, tierResult) {
|
|
452
|
-
const claim = receipt?.payload?.claim || {};
|
|
453
|
-
const assertions = [];
|
|
454
|
-
const arrays = [claim.approvers, claim.approver_authorizations, claim.approver_roles];
|
|
455
|
-
for (const entries of arrays) {
|
|
456
|
-
if (!Array.isArray(entries)) continue;
|
|
457
|
-
for (const entry of entries) {
|
|
458
|
-
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
|
|
459
|
-
const subject = nonEmptyString(entry.subject ?? entry.approver ?? entry.principal_id);
|
|
460
|
-
const role = nonEmptyString(entry.role ?? entry.approver_role);
|
|
461
|
-
if (subject && role) assertions.push({ subject, role });
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
const singleSubject = nonEmptyString(claim.approver ?? claim.approver_subject);
|
|
465
|
-
const singleRole = nonEmptyString(claim.approver_role ?? claim.role);
|
|
466
|
-
if (singleSubject && singleRole) assertions.push({ subject: singleSubject, role: singleRole });
|
|
467
|
-
|
|
468
|
-
if (tierResult?.quorum?.valid === true) {
|
|
469
|
-
for (const member of (receipt?.payload?.quorum?.members || [])) {
|
|
470
|
-
const subject = nonEmptyString(member?.signoff?.context?.approver);
|
|
471
|
-
const role = nonEmptyString(member?.role);
|
|
472
|
-
if (subject && role) assertions.push({ subject, role });
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
return assertions;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
function verifiedApproverSubjects(assurance, tierResult) {
|
|
479
|
-
const subjects = new Set(Array.isArray(assurance?.approvers) ? assurance.approvers.filter(nonEmptyString) : []);
|
|
480
|
-
if (tierResult?.signoff?.valid === true && nonEmptyString(tierResult.signoff.approver)) {
|
|
481
|
-
subjects.add(tierResult.signoff.approver);
|
|
482
|
-
}
|
|
483
|
-
if (tierResult?.quorum?.valid === true) {
|
|
484
|
-
for (const subject of (tierResult.quorum.approvers || [])) {
|
|
485
|
-
if (nonEmptyString(subject)) subjects.add(subject);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
return [...subjects].sort();
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
* Verify signed business-policy and tenant fields plus the cryptographically
|
|
493
|
-
* credited human approvers against one action's relying-party pins.
|
|
494
|
-
*/
|
|
495
|
-
export function verifyBusinessAuthorization({ requirement, receipt, assurance, tierResult } = {}) {
|
|
496
|
-
const expected = businessAuthorizationRequirement(requirement);
|
|
497
|
-
const claim = receipt?.payload?.claim || {};
|
|
498
|
-
const payload = receipt?.payload || {};
|
|
499
|
-
const policyId = signedString([claim.policy_id, payload.policy_id]);
|
|
500
|
-
const policyHash = signedString([claim.policy_hash, payload.policy_hash]);
|
|
501
|
-
const tenantId = signedString([
|
|
502
|
-
claim.tenant_id, claim.organization_id, payload.tenant_id, payload.organization_id,
|
|
503
|
-
]);
|
|
504
|
-
const subjects = verifiedApproverSubjects(assurance, tierResult);
|
|
505
|
-
const roleAssertions = receiptRoleAssertions(receipt, tierResult);
|
|
506
|
-
const evaluatedApprovers = subjects.map((subject) => {
|
|
507
|
-
const asserted = [...new Set(roleAssertions.filter((entry) => entry.subject === subject).map((entry) => entry.role))];
|
|
508
|
-
return { subject, roles: asserted };
|
|
509
|
-
});
|
|
510
|
-
const evaluated = {
|
|
511
|
-
policy_id: policyId.value,
|
|
512
|
-
policy_hash: policyHash.value,
|
|
513
|
-
tenant_id: tenantId.value,
|
|
514
|
-
approvers: evaluatedApprovers,
|
|
515
|
-
};
|
|
516
|
-
const base = { required: expected.configured, ok: true, reason: null, expected, evaluated };
|
|
517
|
-
if (!expected.configured) return base;
|
|
518
|
-
if (!expected.ok) return { ...base, ok: false, reason: expected.reason };
|
|
519
|
-
if (!policyId.ok) return { ...base, ok: false, reason: 'business_policy_id_ambiguous' };
|
|
520
|
-
if (!policyHash.ok) return { ...base, ok: false, reason: 'business_policy_hash_ambiguous' };
|
|
521
|
-
if (!tenantId.ok) return { ...base, ok: false, reason: 'business_tenant_ambiguous' };
|
|
522
|
-
if (policyId.value !== expected.policy_id) return { ...base, ok: false, reason: 'business_policy_id_mismatch' };
|
|
523
|
-
if (policyHash.value !== expected.policy_hash) return { ...base, ok: false, reason: 'business_policy_hash_mismatch' };
|
|
524
|
-
if (tenantId.value !== expected.tenant_id) return { ...base, ok: false, reason: 'business_tenant_mismatch' };
|
|
525
|
-
if (!subjects.length) return { ...base, ok: false, reason: 'business_approver_required' };
|
|
526
|
-
|
|
527
|
-
for (const approver of evaluatedApprovers) {
|
|
528
|
-
const allowedForSubject = expected.allowed_approvers.filter((entry) => entry.subject === approver.subject);
|
|
529
|
-
if (!allowedForSubject.length) return { ...base, ok: false, reason: 'business_approver_not_allowed' };
|
|
530
|
-
const allowedRoles = new Set(allowedForSubject.map((entry) => entry.role));
|
|
531
|
-
if (approver.roles.length && !approver.roles.some((role) => allowedRoles.has(role))) {
|
|
532
|
-
return { ...base, ok: false, reason: 'business_approver_role_not_allowed' };
|
|
533
|
-
}
|
|
534
|
-
// The manifest is the authoritative subject-to-role assignment. When the
|
|
535
|
-
// receipt does not repeat a role, record the pinned role rather than trusting
|
|
536
|
-
// an unsigned/free-text role label from elsewhere.
|
|
537
|
-
if (!approver.roles.length) approver.roles = [...allowedRoles].sort();
|
|
538
|
-
}
|
|
539
|
-
return base;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
/**
|
|
543
|
-
* Create a gate.
|
|
544
|
-
* @param {object} opts
|
|
545
|
-
* @param {object} [opts.manifest] EP-ACTION-RISK-MANIFEST-v0.1 (which actions are guarded, their tier)
|
|
546
|
-
* @param {string[]} [opts.trustedKeys] base64url SPKI-DER issuer keys you trust
|
|
547
|
-
* @param {number} [opts.maxAgeSec=900] reject receipts older than this
|
|
548
|
-
* @param {object} [opts.store] durable, ownership-fenced, permanent consumption store
|
|
549
|
-
* @param {boolean} [opts.allowEphemeralStore=false] explicit test/demo opt-in for in-memory state
|
|
550
|
-
* @param {object} [opts.log] evidence log (default in-memory, hash-chained)
|
|
551
|
-
* @param {boolean} [opts.allowInlineKey=false] accept the receipt's own key (integrity, NOT trust)
|
|
552
|
-
* @param {object} [opts.keyRegistry] a key registry (createKeyRegistry) for rotation + revocation;
|
|
553
|
-
* if given it supersedes trustedKeys — a receipt is verified only against keys valid (and not
|
|
554
|
-
* revoked) at its issuance time.
|
|
555
|
-
* @param {object} [opts.approverKeys] PINNED approver keys ({ keyId: { public_key, key_class } }).
|
|
556
|
-
* Used both for the pinned assurance-proof path and to authorize receipt-embedded
|
|
557
|
-
* approver keys under the self-contained embedded-evidence path.
|
|
558
|
-
* @param {boolean} [opts.allowEmbeddedApproverKeys=false] allow one embedded key
|
|
559
|
-
* to earn Class-A integrity in demos. Embedded keys never establish quorum.
|
|
560
|
-
* @param {object} [opts.quorumPolicy] global relying-party-pinned quorum rule
|
|
561
|
-
* @param {object} [opts.quorumPolicies] action_type -> pinned quorum rule
|
|
562
|
-
* @param {string} [opts.rpId] WebAuthn relying-party identifier. Required for
|
|
563
|
-
* built-in Class-A or quorum assurance verification.
|
|
564
|
-
* @param {string[]} [opts.allowedOrigins] exact WebAuthn origins accepted by the
|
|
565
|
-
* relying party. Required for built-in Class-A or quorum verification.
|
|
566
|
-
* @param {function} [opts.verifyAdmissibilityPacket] trusted relying-party hook.
|
|
567
|
-
* Required whenever an admissibility profile is pinned. It must authenticate
|
|
568
|
-
* the presented packet or recompute the verdict and return the trusted block.
|
|
569
|
-
*/
|
|
570
|
-
export function createGate({ manifest = null, trustedKeys = [], maxAgeSec = 900, store, log, allowInlineKey = false, allowEphemeralStore = false, strictEvidence = true, now = Date.now, keyRegistry = null, approverKeys = {}, approver_keys = null, verifyAssurance = null, rpId = null, allowedOrigins = [], quorumPolicy = null, quorumPolicies = {}, requiredAdmissibilityProfile = null, verifyAdmissibilityPacket = null, allowEmbeddedApproverKeys = false } = {}) {
|
|
571
|
-
// Production key custody: a registry (rotation + revocation) supersedes a flat
|
|
572
|
-
// trustedKeys list. A flat list is coerced to an always-valid registry, so
|
|
573
|
-
// existing callers are unchanged.
|
|
574
|
-
const registry = keyRegistry ? asKeyRegistry(keyRegistry) : (trustedKeys.length ? asKeyRegistry(trustedKeys) : null);
|
|
575
|
-
if (manifest) {
|
|
576
|
-
const m = validateActionRiskManifest(manifest);
|
|
577
|
-
if (!m.ok) throw new Error('EMILIA Gate: invalid action-risk manifest: ' + m.errors.join('; '));
|
|
578
|
-
for (const [index, actionRequirement] of (manifest.actions || []).entries()) {
|
|
579
|
-
if (actionRequirement?.receipt_required !== true) continue;
|
|
580
|
-
const business = businessAuthorizationRequirement(actionRequirement);
|
|
581
|
-
if (business.configured && !business.ok) {
|
|
582
|
-
throw new Error(`EMILIA Gate: invalid action-risk manifest: actions[${index}].business_authorization ${business.reason}`);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
if (allowInlineKey) {
|
|
587
|
-
// eslint-disable-next-line no-console
|
|
588
|
-
console.warn('EMILIA Gate: allowInlineKey=true accepts a receipt\'s OWN key. This proves INTEGRITY (the receipt was not tampered with) but NOT issuer TRUST (anyone can mint a receipt with their own key). Use for demos only; pin trustedKeys in production.');
|
|
589
|
-
}
|
|
590
|
-
// Replay defense is only sound if the store is shared, ownership-fenced, and
|
|
591
|
-
// permanent. This is a security property in every environment; NODE_ENV must
|
|
592
|
-
// never silently decide whether a receipt can be replayed.
|
|
593
|
-
let consumption = store;
|
|
594
|
-
if (!consumption) {
|
|
595
|
-
if (!allowEphemeralStore) throw new Error(
|
|
596
|
-
'EMILIA Gate requires a durable, ownership-fenced, permanent consumption store. '
|
|
597
|
-
+ 'Pass allowEphemeralStore:true only for an explicit test/demo gate.',
|
|
598
|
-
);
|
|
599
|
-
consumption = new MemoryConsumptionStore();
|
|
600
|
-
}
|
|
601
|
-
for (const method of ['consume', 'reserve', 'commit']) {
|
|
602
|
-
if (typeof consumption?.[method] !== 'function') {
|
|
603
|
-
throw new Error(`EMILIA Gate consumption store must implement ${method}()`);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
if (!allowEphemeralStore && !isSecureConsumptionStore(consumption)) {
|
|
607
|
-
throw new Error(
|
|
608
|
-
'EMILIA Gate requires a durable, ownership-fenced, permanent consumption store. '
|
|
609
|
-
+ 'Pass allowEphemeralStore:true only for an explicit test/demo gate.',
|
|
610
|
-
);
|
|
611
|
-
}
|
|
612
|
-
const evidence = log || createEvidenceLog({ strict: strictEvidence });
|
|
613
|
-
|
|
614
|
-
async function check({ selector = {}, receipt = null, observedAction = null, consumptionMode = 'consume', admissibilityProfile = null, reliancePacket: presentedPacket = null, admissibility = null } = {}) {
|
|
615
|
-
const requirement = manifest ? findActionRequirement(manifest, selector) : null;
|
|
616
|
-
const action = requirement?.action_type || selector.action_type || selector.action || null;
|
|
617
|
-
// Assurance tier the action requires (cryptographically checked below). For a
|
|
618
|
-
// manifest-guarded action the tier MUST be declared explicitly: never fall
|
|
619
|
-
// back to the weakest 'software' tier because assurance_class was omitted —
|
|
620
|
-
// that would let a guarded, possibly critical, action accept a bare
|
|
621
|
-
// machine-signed receipt (a fail-open). A guarded requirement with no tier
|
|
622
|
-
// is a misconfiguration and fails closed just below. Only selector-only
|
|
623
|
-
// checks (no manifest requirement) use the documented 'software' default.
|
|
624
|
-
const requiredTier = requirement
|
|
625
|
-
? requirement.assurance_class
|
|
626
|
-
: (selector.assurance_class || 'software');
|
|
627
|
-
const pinnedQuorumPolicy = requirement?.quorum_policy
|
|
628
|
-
|| (quorumPolicies && typeof quorumPolicies === 'object' ? quorumPolicies[action] : null)
|
|
629
|
-
|| quorumPolicy;
|
|
630
|
-
const observed = observedAction || selector.observedAction || selector.actionDetails || null;
|
|
631
|
-
const businessExpected = businessAuthorizationRequirement(requirement);
|
|
632
|
-
let businessEvaluation = {
|
|
633
|
-
required: businessExpected.configured,
|
|
634
|
-
ok: !businessExpected.configured,
|
|
635
|
-
reason: null,
|
|
636
|
-
expected: businessExpected,
|
|
637
|
-
evaluated: { policy_id: null, policy_hash: null, tenant_id: null, approvers: [] },
|
|
638
|
-
};
|
|
639
|
-
|
|
640
|
-
async function decide(allow, status, reason, extra = {}) {
|
|
641
|
-
const entry = {
|
|
642
|
-
kind: 'decision',
|
|
643
|
-
at: new Date(typeof now === 'function' ? now() : now).toISOString(),
|
|
644
|
-
action,
|
|
645
|
-
allow,
|
|
646
|
-
status,
|
|
647
|
-
reason,
|
|
648
|
-
selector: { ...selector },
|
|
649
|
-
required_tier: requiredTier,
|
|
650
|
-
receipt_id: receipt?.payload?.receipt_id ?? null,
|
|
651
|
-
subject: receipt?.payload?.subject ?? null,
|
|
652
|
-
observed_action_hash: observed ? safeCanonicalHash(observed) : null,
|
|
653
|
-
business_authorization: businessEvaluation,
|
|
654
|
-
evaluated_policy_id: businessEvaluation.evaluated.policy_id,
|
|
655
|
-
evaluated_policy_hash: businessEvaluation.evaluated.policy_hash,
|
|
656
|
-
evaluated_tenant_id: businessEvaluation.evaluated.tenant_id,
|
|
657
|
-
evaluated_approvers: businessEvaluation.evaluated.approvers,
|
|
658
|
-
...extra,
|
|
659
|
-
};
|
|
660
|
-
let record;
|
|
661
|
-
try {
|
|
662
|
-
record = await evidence.record(entry);
|
|
663
|
-
} catch (e) {
|
|
664
|
-
// The decision could not be durably recorded. Fail CLOSED: never
|
|
665
|
-
// authorize an action we cannot account for. Downgrade any allow to a
|
|
666
|
-
// refusal and best-effort note the downgrade (non-fatal if that fails too).
|
|
667
|
-
allow = false;
|
|
668
|
-
status = RECEIPT_REQUIRED_STATUS;
|
|
669
|
-
reason = 'evidence_log_failed';
|
|
670
|
-
try {
|
|
671
|
-
record = await evidence.record({ ...entry, allow: false, status, reason, evidence_error: String(e?.message ?? e) });
|
|
672
|
-
} catch {
|
|
673
|
-
record = null;
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
const out = { allow, status, reason, action, requirement, evidence: record };
|
|
677
|
-
if (!allow) {
|
|
678
|
-
out.challenge = receiptChallenge(action, reason, {
|
|
679
|
-
status: RECEIPT_REQUIRED_STATUS,
|
|
680
|
-
assuranceClass: requiredTier,
|
|
681
|
-
maxAgeSec,
|
|
682
|
-
manifest: selector.manifestUrl,
|
|
683
|
-
});
|
|
684
|
-
out.header = receiptRequiredHeader({ action, assuranceClass: requiredTier, maxAgeSec });
|
|
685
|
-
}
|
|
686
|
-
return out;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// Manifest present and this selector is not guarded (or explicitly not required): pass through.
|
|
690
|
-
if (manifest && (!requirement || requirement.receipt_required === false)) {
|
|
691
|
-
return decide(true, 200, 'not_guarded');
|
|
692
|
-
}
|
|
693
|
-
// Guarded, but no receipt was presented.
|
|
694
|
-
if (!receipt) {
|
|
695
|
-
return decide(false, RECEIPT_REQUIRED_STATUS, 'receipt_required');
|
|
696
|
-
}
|
|
697
|
-
// A manifest-guarded action that declares no assurance_class is a
|
|
698
|
-
// misconfiguration. Fail CLOSED rather than defaulting to the weakest tier
|
|
699
|
-
// (which would accept a bare machine-signed receipt for a guarded action).
|
|
700
|
-
// validateActionRiskManifest also rejects such a manifest at author time;
|
|
701
|
-
// this is defense in depth for a manifest loaded without re-validation.
|
|
702
|
-
if (requirement && requirement.receipt_required !== false && !requiredTier) {
|
|
703
|
-
return decide(false, RECEIPT_REQUIRED_STATUS, 'manifest_missing_assurance_class');
|
|
704
|
-
}
|
|
705
|
-
// Signature / freshness / action-binding / outcome. Production key custody:
|
|
706
|
-
// resolve the issuer keys valid (and not revoked) at THIS receipt's issuance
|
|
707
|
-
// time. A revoked or out-of-window key is excluded, so its signature does not
|
|
708
|
-
// verify and the action is refused (fail closed).
|
|
709
|
-
const effectiveKeys = registry
|
|
710
|
-
? registry.keysValidAt(receipt?.payload?.created_at)
|
|
711
|
-
: trustedKeys;
|
|
712
|
-
const v = verifyEmiliaReceipt(receipt, { trustedKeys: effectiveKeys, allowInlineKey, action, maxAgeSec });
|
|
713
|
-
if (!v.ok) {
|
|
714
|
-
return decide(false, RECEIPT_REQUIRED_STATUS, `receipt_rejected:${v.reason}`, { rejected: v });
|
|
715
|
-
}
|
|
716
|
-
// Assurance tier. CRYPTOGRAPHICALLY VERIFIED — never inferred from
|
|
717
|
-
// self-asserted payload fields. The credited tier is the HIGHER of two
|
|
718
|
-
// independent proof paths:
|
|
719
|
-
// (a) pinned assurance proof (payload.assurance_proof verified against
|
|
720
|
-
// pinned approverKeys) or a caller-supplied verifyAssurance hook;
|
|
721
|
-
// (b) self-contained embedded per-signer evidence (EP-QUORUM-v1 /
|
|
722
|
-
// WebAuthn device signoff) re-verified via verifyQuorum /
|
|
723
|
-
// verifyWebAuthnSignoff (DoD audit fix).
|
|
724
|
-
// A receipt that only CLAIMS a higher tier earns 'software' and is refused.
|
|
725
|
-
const needRank = TIER_RANK[requiredTier];
|
|
726
|
-
if (needRank === undefined) {
|
|
727
|
-
return decide(false, RECEIPT_REQUIRED_STATUS, 'unknown_required_tier', { have_tier: 'software', need_tier: requiredTier, assurance_tier_source: 'cryptographic_verification' });
|
|
728
|
-
}
|
|
729
|
-
if (needRank >= TIER_RANK.class_a && typeof verifyAssurance !== 'function'
|
|
730
|
-
&& (typeof rpId !== 'string' || !rpId
|
|
731
|
-
|| !Array.isArray(allowedOrigins) || allowedOrigins.length === 0
|
|
732
|
-
|| allowedOrigins.some((origin) => typeof origin !== 'string' || !origin))) {
|
|
733
|
-
return decide(false, RECEIPT_REQUIRED_STATUS, 'assurance_context_unpinned', {
|
|
734
|
-
have_tier: 'software', need_tier: requiredTier,
|
|
735
|
-
assurance_tier_source: 'cryptographic_verification',
|
|
736
|
-
});
|
|
737
|
-
}
|
|
738
|
-
if (requiredTier === 'quorum' && typeof verifyAssurance !== 'function') {
|
|
739
|
-
const policy = validatePinnedQuorumPolicy(pinnedQuorumPolicy);
|
|
740
|
-
if (!policy.ok) {
|
|
741
|
-
return decide(false, RECEIPT_REQUIRED_STATUS, policy.reason, {
|
|
742
|
-
have_tier: 'software', need_tier: requiredTier,
|
|
743
|
-
assurance_tier_source: 'cryptographic_verification',
|
|
744
|
-
});
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
const assurance = evaluateReceiptAssurance(receipt, requiredTier, {
|
|
748
|
-
approverKeys: approver_keys || approverKeys,
|
|
749
|
-
verifyAssurance,
|
|
750
|
-
rpId,
|
|
751
|
-
allowedOrigins,
|
|
752
|
-
quorumPolicy: pinnedQuorumPolicy,
|
|
753
|
-
});
|
|
754
|
-
const tierResult = receiptAssuranceTier(receipt, {
|
|
755
|
-
rpId, allowedOrigins, detail: true, approverKeys: approver_keys || approverKeys,
|
|
756
|
-
verifyAssurance, quorumPolicy: pinnedQuorumPolicy,
|
|
757
|
-
// Trust-laundering guard: a receipt-embedded approver key does NOT elevate
|
|
758
|
-
// the tier unless it is in the pinned approverKeys set, or the operator
|
|
759
|
-
// explicitly opted into the self-contained embedded-evidence mode. DEFAULT OFF.
|
|
760
|
-
allowEmbeddedApproverKeys,
|
|
761
|
-
});
|
|
762
|
-
// Take the strongest tier either path proves.
|
|
763
|
-
const have = (TIER_RANK[assurance.have] ?? 0) >= (TIER_RANK[tierResult.tier] ?? 0)
|
|
764
|
-
? assurance.have : tierResult.tier;
|
|
765
|
-
if ((TIER_RANK[have] ?? 0) < needRank) {
|
|
766
|
-
// The credited tier (from either proof path) is below what the action
|
|
767
|
-
// requires. The canonical machine-readable reason is 'assurance_too_low';
|
|
768
|
-
// main's proof-path detail (e.g. 'assurance_proof_required') is surfaced
|
|
769
|
-
// separately so callers keep the diagnostic without changing the contract.
|
|
770
|
-
return decide(false, RECEIPT_REQUIRED_STATUS, 'assurance_too_low', {
|
|
771
|
-
have_tier: have, need_tier: requiredTier,
|
|
772
|
-
assurance_tier_source: 'cryptographic_verification',
|
|
773
|
-
assurance_detail: assurance.reason || null,
|
|
774
|
-
tier_evidence: { quorum: tierResult.quorum, signoff: tierResult.signoff },
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
// Business authorization is a distinct trust root from receipt issuer
|
|
778
|
-
// integrity and assurance tier. The signed claim must name the exact policy
|
|
779
|
-
// id+hash and tenant this action requirement pins, and the humans who
|
|
780
|
-
// cryptographically earned the tier must belong to its subject/role roster.
|
|
781
|
-
// This runs BEFORE execution binding, admissibility, and receipt reservation.
|
|
782
|
-
businessEvaluation = verifyBusinessAuthorization({
|
|
783
|
-
requirement,
|
|
784
|
-
receipt,
|
|
785
|
-
assurance,
|
|
786
|
-
tierResult,
|
|
787
|
-
});
|
|
788
|
-
if (!businessEvaluation.ok) {
|
|
789
|
-
return decide(false, RECEIPT_REQUIRED_STATUS, businessEvaluation.reason, {
|
|
790
|
-
have_tier: have,
|
|
791
|
-
assurance_tier_source: 'cryptographic_verification',
|
|
792
|
-
});
|
|
793
|
-
}
|
|
794
|
-
// The high-risk action packs define material fields that must be observed
|
|
795
|
-
// by the executor from the system of record. A signed, harmless-looking
|
|
796
|
-
// claim cannot authorize a different real mutation.
|
|
797
|
-
const executionBinding = verifyExecutionBinding({ requirement, receipt, observedAction: observed });
|
|
798
|
-
if (!executionBinding.ok) {
|
|
799
|
-
return decide(false, RECEIPT_REQUIRED_STATUS, 'execution_binding_failed', { execution_binding: executionBinding, have_tier: have, assurance_tier_source: 'cryptographic_verification' });
|
|
800
|
-
}
|
|
801
|
-
// OPT-IN admissibility pinning. When the caller pins a required admissibility
|
|
802
|
-
// profile {id, profile_hash} (gate-level requiredAdmissibilityProfile, a
|
|
803
|
-
// per-call admissibilityProfile, or selector.admissibilityProfile), the gate
|
|
804
|
-
// REFUSES unless a presented reliance packet's admissibility block was computed
|
|
805
|
-
// against the SAME pinned profile_hash AND carries an 'admissible' verdict. The
|
|
806
|
-
// gate does NOT re-evaluate raw evidence and does NOT define the bar — the
|
|
807
|
-
// relying party's own evaluator produced the verdict OFFLINE against its pinned
|
|
808
|
-
// profile. Checked BEFORE consumption so a mismatch never burns the receipt.
|
|
809
|
-
// When no profile is pinned, this whole block is inert — behavior is
|
|
810
|
-
// byte-for-byte unchanged from the pre-admissibility gate.
|
|
811
|
-
const pinnedProfile = admissibilityProfile || selector.admissibilityProfile || requiredAdmissibilityProfile;
|
|
812
|
-
let trustedAdmissibility = null;
|
|
813
|
-
if (pinnedProfile) {
|
|
814
|
-
const presentedAdmissibility = admissibility ?? presentedPacket ?? selector.reliancePacket ?? selector.admissibility ?? null;
|
|
815
|
-
if (typeof verifyAdmissibilityPacket !== 'function') {
|
|
816
|
-
return decide(false, RECEIPT_REQUIRED_STATUS, 'admissibility_verifier_required', {
|
|
817
|
-
pinned_profile: { id: pinnedProfile.id ?? null, profile_hash: pinnedProfile.profile_hash ?? null },
|
|
818
|
-
have_tier: have,
|
|
819
|
-
assurance_tier_source: 'cryptographic_verification',
|
|
820
|
-
});
|
|
821
|
-
}
|
|
822
|
-
try {
|
|
823
|
-
trustedAdmissibility = await verifyAdmissibilityPacket({
|
|
824
|
-
pinned_profile: structuredClone(pinnedProfile),
|
|
825
|
-
presented: structuredClone(presentedAdmissibility),
|
|
826
|
-
receipt: structuredClone(receipt),
|
|
827
|
-
selector: structuredClone(selector),
|
|
828
|
-
observed_action: observed === null ? null : structuredClone(observed),
|
|
829
|
-
});
|
|
830
|
-
} catch {
|
|
831
|
-
return decide(false, RECEIPT_REQUIRED_STATUS, 'admissibility_verification_failed', {
|
|
832
|
-
pinned_profile: { id: pinnedProfile.id ?? null, profile_hash: pinnedProfile.profile_hash ?? null },
|
|
833
|
-
have_tier: have,
|
|
834
|
-
assurance_tier_source: 'cryptographic_verification',
|
|
835
|
-
});
|
|
836
|
-
}
|
|
837
|
-
const adm = verifyAdmissibilityAgainstPinnedProfile(pinnedProfile, trustedAdmissibility);
|
|
838
|
-
if (!adm.ok) {
|
|
839
|
-
return decide(false, RECEIPT_REQUIRED_STATUS, adm.reason, {
|
|
840
|
-
admissibility_check: adm,
|
|
841
|
-
pinned_profile: { id: pinnedProfile.id ?? null, profile_hash: pinnedProfile.profile_hash ?? null },
|
|
842
|
-
have_tier: have,
|
|
843
|
-
assurance_tier_source: 'cryptographic_verification',
|
|
844
|
-
});
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
// One-time consumption (replay defense). Require a stable, issuer-generated
|
|
848
|
-
// receipt_id — never fall back to a content hash, whose canonicalization can
|
|
849
|
-
// differ across language implementations and silently break replay detection
|
|
850
|
-
// when services of different languages share a store.
|
|
851
|
-
const receiptId = receipt?.payload?.receipt_id;
|
|
852
|
-
if (!receiptId) {
|
|
853
|
-
return decide(false, RECEIPT_REQUIRED_STATUS, 'receipt_rejected:missing_receipt_id');
|
|
854
|
-
}
|
|
855
|
-
let fresh;
|
|
856
|
-
if (consumptionMode === 'reserve') {
|
|
857
|
-
if (typeof consumption.reserve !== 'function') {
|
|
858
|
-
return decide(false, RECEIPT_REQUIRED_STATUS, 'consumption_store_lacks_reserve', { consumption_key: receiptId });
|
|
859
|
-
}
|
|
860
|
-
fresh = await consumption.reserve(receiptId);
|
|
861
|
-
} else if (consumptionMode === 'none') {
|
|
862
|
-
fresh = true;
|
|
863
|
-
} else {
|
|
864
|
-
fresh = await consumption.consume(receiptId);
|
|
865
|
-
}
|
|
866
|
-
if (!fresh) {
|
|
867
|
-
return decide(false, RECEIPT_REQUIRED_STATUS, 'replay_refused', { consumption_key: receiptId });
|
|
868
|
-
}
|
|
869
|
-
const allowExtra = { signer: v.signer, outcome: v.outcome, have_tier: have, assurance_tier_source: 'cryptographic_verification', execution_binding: executionBinding, consumption_mode: consumptionMode };
|
|
870
|
-
// Carry the admissibility block (from the presented packet) onto the decision
|
|
871
|
-
// so a reliance packet built from this decision embeds the verdict the relying
|
|
872
|
-
// party's evaluator computed. Only when something was actually presented.
|
|
873
|
-
const presentedAdmForAllow = pinnedProfile
|
|
874
|
-
? trustedAdmissibility
|
|
875
|
-
: (admissibility ?? presentedPacket ?? selector.reliancePacket ?? selector.admissibility ?? null);
|
|
876
|
-
if (presentedAdmForAllow) {
|
|
877
|
-
const admBlock = presentedAdmForAllow.admissibility !== undefined ? presentedAdmForAllow.admissibility : presentedAdmForAllow;
|
|
878
|
-
if (admBlock) allowExtra.admissibility = admBlock;
|
|
879
|
-
}
|
|
880
|
-
return decide(true, 200, 'allow', allowExtra);
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
async function requestGateInput(req, opts = {}) {
|
|
884
|
-
let selector = typeof opts.selector === 'function'
|
|
885
|
-
? await opts.selector(req)
|
|
886
|
-
: { ...(opts.selector || {}) };
|
|
887
|
-
if (!selector || typeof selector !== 'object' || Array.isArray(selector)) selector = {};
|
|
888
|
-
if (opts.action && !selector.action_type) {
|
|
889
|
-
selector.action_type = typeof opts.action === 'function' ? await opts.action(req) : opts.action;
|
|
890
|
-
}
|
|
891
|
-
let receipt = typeof opts.receipt === 'function' ? await opts.receipt(req) : (opts.receipt ?? null);
|
|
892
|
-
if (!receipt) {
|
|
893
|
-
const hdr = req.headers?.['x-emilia-receipt'];
|
|
894
|
-
if (hdr) receipt = parseReceiptCarrier(hdr);
|
|
895
|
-
if (!receipt && req.body?.emilia_receipt) receipt = req.body.emilia_receipt;
|
|
896
|
-
}
|
|
897
|
-
const observedAction = typeof opts.observedAction === 'function'
|
|
898
|
-
? await opts.observedAction(req)
|
|
899
|
-
: (opts.observedAction || req.emiliaObservedAction || null);
|
|
900
|
-
const admissibilityProfile = typeof opts.admissibilityProfile === 'function'
|
|
901
|
-
? await opts.admissibilityProfile(req)
|
|
902
|
-
: (opts.admissibilityProfile ?? null);
|
|
903
|
-
const presentedPacket = typeof opts.reliancePacket === 'function'
|
|
904
|
-
? await opts.reliancePacket(req)
|
|
905
|
-
: (opts.reliancePacket ?? opts.admissibility ?? null);
|
|
906
|
-
return { selector, receipt, observedAction, admissibilityProfile, reliancePacket: presentedPacket };
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
function sendRefusal(res, authorization) {
|
|
910
|
-
if (typeof res?.setHeader === 'function' && authorization.header) {
|
|
911
|
-
res.setHeader(RECEIPT_REQUIRED_HEADER, authorization.header);
|
|
912
|
-
}
|
|
913
|
-
if (typeof res?.status === 'function' && typeof res?.json === 'function') {
|
|
914
|
-
return res.status(authorization.status).json(authorization.challenge);
|
|
915
|
-
}
|
|
916
|
-
if (res) res.statusCode = authorization.status;
|
|
917
|
-
if (typeof res?.end === 'function') {
|
|
918
|
-
if (typeof res?.setHeader === 'function') res.setHeader('content-type', 'application/json');
|
|
919
|
-
return res.end(JSON.stringify(authorization.challenge));
|
|
920
|
-
}
|
|
921
|
-
return authorization;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
/**
|
|
925
|
-
* Express/Connect route wrapper. The route handler itself is the effect
|
|
926
|
-
* callback owned by gate.run(), so authorization, receipt reservation,
|
|
927
|
-
* execution, consumption, and evidence form one lifecycle.
|
|
928
|
-
*/
|
|
929
|
-
function route(handler, opts = {}) {
|
|
930
|
-
if (typeof handler !== 'function') throw new Error('EMILIA Gate route(): handler is required');
|
|
931
|
-
return async function emiliaGateRoute(req, res) {
|
|
932
|
-
const input = await requestGateInput(req, opts);
|
|
933
|
-
const out = await run(input, async (authorization) => {
|
|
934
|
-
req.emiliaGate = authorization;
|
|
935
|
-
return handler(req, res, authorization);
|
|
936
|
-
});
|
|
937
|
-
if (!out.ok) return sendRefusal(res, out.authorization);
|
|
938
|
-
req.emiliaGate = out.authorization;
|
|
939
|
-
req.emiliaGateExecution = out.execution;
|
|
940
|
-
req.emiliaReliancePacket = out.packet;
|
|
941
|
-
return out.result;
|
|
942
|
-
};
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
/**
|
|
946
|
-
* @deprecated Middleware cannot prove that code after next() actually ran.
|
|
947
|
-
* It therefore fails closed without parsing or consuming a presented receipt.
|
|
948
|
-
* Use gate.route(handler, opts), gate.guard(), or gate.run().
|
|
949
|
-
*/
|
|
950
|
-
function middleware(opts = {}) {
|
|
951
|
-
return async function emiliaGateDeprecatedMiddleware(req, res) {
|
|
952
|
-
const selector = typeof opts.selector === 'function'
|
|
953
|
-
? await opts.selector(req)
|
|
954
|
-
: { ...(opts.selector || {}) };
|
|
955
|
-
const action = (selector && (selector.action_type || selector.action))
|
|
956
|
-
|| (typeof opts.action === 'function' ? await opts.action(req) : opts.action)
|
|
957
|
-
|| null;
|
|
958
|
-
const reason = 'unsafe_middleware_deprecated';
|
|
959
|
-
const challenge = receiptChallenge(action, reason, {
|
|
960
|
-
status: RECEIPT_REQUIRED_STATUS,
|
|
961
|
-
assuranceClass: selector?.assurance_class || null,
|
|
962
|
-
maxAgeSec,
|
|
963
|
-
manifest: selector?.manifestUrl,
|
|
964
|
-
});
|
|
965
|
-
const authorization = {
|
|
966
|
-
allow: false,
|
|
967
|
-
status: RECEIPT_REQUIRED_STATUS,
|
|
968
|
-
reason,
|
|
969
|
-
action,
|
|
970
|
-
challenge,
|
|
971
|
-
header: receiptRequiredHeader({ action, assuranceClass: selector?.assurance_class || null, maxAgeSec }),
|
|
972
|
-
};
|
|
973
|
-
return sendRefusal(res, authorization);
|
|
974
|
-
};
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
/**
|
|
978
|
-
* Emit a post-execution receipt bound to a prior authorization decision — the
|
|
979
|
-
* "execution emits proof" half of the loop (maps to the EP Commit seal). It
|
|
980
|
-
* commits to the exact authorization decision (`authorizes_decision` = that
|
|
981
|
-
* decision's evidence hash), so authorization and execution are one chain.
|
|
982
|
-
*/
|
|
983
|
-
async function recordExecution({ authorization, outcome = 'executed', detail, observedAction = null, executionBinding = null } = {}) {
|
|
984
|
-
const auth = authorization?.evidence || authorization || {};
|
|
985
|
-
return evidence.record({
|
|
986
|
-
kind: 'execution',
|
|
987
|
-
at: new Date(typeof now === 'function' ? now() : now).toISOString(),
|
|
988
|
-
authorizes_decision: auth.hash ?? null,
|
|
989
|
-
action: authorization?.action ?? auth.action ?? null,
|
|
990
|
-
receipt_id: auth.receipt_id ?? null,
|
|
991
|
-
outcome, // 'executed' | 'failed'
|
|
992
|
-
observed_action_hash: observedAction ? safeCanonicalHash(observedAction) : null,
|
|
993
|
-
execution_binding: executionBinding || authorization?.evidence?.execution_binding || authorization?.execution_binding || null,
|
|
994
|
-
...(detail !== undefined ? { detail } : {}),
|
|
995
|
-
});
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
/**
|
|
999
|
-
* Recommended end-to-end path. Reserves the receipt, runs the side effect,
|
|
1000
|
-
* commits one-time consumption after the effect attempt, and records execution.
|
|
1001
|
-
* Once the executor is invoked, an exception is an INDETERMINATE outcome: the
|
|
1002
|
-
* external effect may have happened before its response was lost. The receipt
|
|
1003
|
-
* is therefore committed (or left reserved if the store is unavailable),
|
|
1004
|
-
* never released automatically. Callers that need retries must make the
|
|
1005
|
-
* downstream effect idempotent under the receipt id and reconcile its result.
|
|
1006
|
-
*/
|
|
1007
|
-
async function run({ selector = {}, receipt = null, observedAction = null, admissibilityProfile = null, reliancePacket: presentedPacket = null, admissibility = null } = {}, fn, opts = {}) {
|
|
1008
|
-
if (typeof fn !== 'function') throw new Error('EMILIA Gate run(): fn is required');
|
|
1009
|
-
const authorization = await check({ selector, receipt, observedAction, consumptionMode: 'reserve', admissibilityProfile, reliancePacket: presentedPacket, admissibility });
|
|
1010
|
-
if (!authorization.allow) {
|
|
1011
|
-
return { ok: false, status: authorization.status, body: authorization.challenge, authorization };
|
|
1012
|
-
}
|
|
1013
|
-
const receiptId = authorization.evidence?.receipt_id;
|
|
1014
|
-
let phase = 'reserved';
|
|
1015
|
-
let consumptionCommitted = false;
|
|
1016
|
-
try {
|
|
1017
|
-
phase = 'effect_attempted';
|
|
1018
|
-
const result = await fn(authorization);
|
|
1019
|
-
phase = 'effect_returned';
|
|
1020
|
-
if (typeof consumption.commit === 'function') await consumption.commit(receiptId);
|
|
1021
|
-
consumptionCommitted = true;
|
|
1022
|
-
phase = 'consumed';
|
|
1023
|
-
if (opts.recordExecution === false) return { ok: true, result, authorization, execution: null, packet: null };
|
|
1024
|
-
phase = 'recording_execution';
|
|
1025
|
-
const execution = await recordExecution({ authorization, outcome: 'executed', observedAction });
|
|
1026
|
-
const packet = await reliancePacket({ authorization, execution });
|
|
1027
|
-
return { ok: true, result, authorization, execution, packet };
|
|
1028
|
-
} catch (e) {
|
|
1029
|
-
// An exception after invoking fn() cannot establish that no external
|
|
1030
|
-
// effect occurred. Burn the approval if possible; if storage is down, the
|
|
1031
|
-
// ownership-fenced reservation remains and still blocks replay.
|
|
1032
|
-
let consumptionError = null;
|
|
1033
|
-
if (!consumptionCommitted && phase !== 'reserved' && typeof consumption.commit === 'function') {
|
|
1034
|
-
try {
|
|
1035
|
-
await consumption.commit(receiptId);
|
|
1036
|
-
consumptionCommitted = true;
|
|
1037
|
-
} catch (commitError) {
|
|
1038
|
-
consumptionError = commitError;
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
if (opts.recordExecution !== false && phase !== 'recording_execution') {
|
|
1042
|
-
try {
|
|
1043
|
-
await recordExecution({
|
|
1044
|
-
authorization,
|
|
1045
|
-
outcome: 'indeterminate',
|
|
1046
|
-
// Exception text frequently contains provider payloads, record IDs,
|
|
1047
|
-
// or secrets. The caller still receives the original exception;
|
|
1048
|
-
// the portable evidence record carries only the closed outcome.
|
|
1049
|
-
detail: { code: 'effect_attempted_outcome_unknown' },
|
|
1050
|
-
observedAction,
|
|
1051
|
-
});
|
|
1052
|
-
} catch (recordError) {
|
|
1053
|
-
if (!consumptionError) consumptionError = recordError;
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
if (consumptionError && e && typeof e === 'object') {
|
|
1057
|
-
e.consumption_error = String(consumptionError?.message ?? consumptionError);
|
|
1058
|
-
}
|
|
1059
|
-
throw e;
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
/**
|
|
1064
|
-
* Wrap any function so it runs only behind a passing gate check, and (unless
|
|
1065
|
-
* disabled) emits an execution receipt after it runs — the full firewall loop:
|
|
1066
|
-
* request -> check -> execute -> execution receipt. Framework-agnostic.
|
|
1067
|
-
*/
|
|
1068
|
-
function guard(fn, opts = {}) {
|
|
1069
|
-
return async function guarded(...args) {
|
|
1070
|
-
const selector = typeof opts.selector === 'function' ? opts.selector(...args) : (opts.selector || {});
|
|
1071
|
-
const receipt = typeof opts.receipt === 'function' ? opts.receipt(...args) : (opts.receipt ?? null);
|
|
1072
|
-
const observedAction = typeof opts.observedAction === 'function'
|
|
1073
|
-
? opts.observedAction(...args)
|
|
1074
|
-
: (opts.observedAction || selector.observedAction || null);
|
|
1075
|
-
const admissibilityProfile = typeof opts.admissibilityProfile === 'function'
|
|
1076
|
-
? opts.admissibilityProfile(...args)
|
|
1077
|
-
: (opts.admissibilityProfile ?? null);
|
|
1078
|
-
const presentedPacket = typeof opts.reliancePacket === 'function'
|
|
1079
|
-
? opts.reliancePacket(...args)
|
|
1080
|
-
: (opts.reliancePacket ?? opts.admissibility ?? null);
|
|
1081
|
-
const out = await run({ selector, receipt, observedAction, admissibilityProfile, reliancePacket: presentedPacket }, () => fn(...args), { recordExecution: opts.recordExecution });
|
|
1082
|
-
if (!out.ok) {
|
|
1083
|
-
const e = new Error(`EMILIA Gate refused (${out.authorization.reason})`);
|
|
1084
|
-
e.code = 'EMILIA_RECEIPT_REQUIRED';
|
|
1085
|
-
e.gate = out.authorization;
|
|
1086
|
-
throw e;
|
|
1087
|
-
}
|
|
1088
|
-
return out.result;
|
|
1089
|
-
};
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
async function reliancePacket({ authorization, execution = null, binding = null, admissibility = null } = {}) {
|
|
1093
|
-
// The admissibility block rides on the authorization decision's evidence when
|
|
1094
|
-
// a reliance packet was presented at check() time; an explicit `admissibility`
|
|
1095
|
-
// arg overrides it. buildReliancePacket fails closed on a non-'admissible'
|
|
1096
|
-
// block, so a do_not_rely verdict can never be laundered into rely here.
|
|
1097
|
-
const adm = admissibility
|
|
1098
|
-
?? authorization?.evidence?.admissibility
|
|
1099
|
-
?? authorization?.admissibility
|
|
1100
|
-
?? null;
|
|
1101
|
-
return buildReliancePacket({
|
|
1102
|
-
decision: authorization,
|
|
1103
|
-
execution,
|
|
1104
|
-
evidence,
|
|
1105
|
-
manifest,
|
|
1106
|
-
binding,
|
|
1107
|
-
admissibility: adm,
|
|
1108
|
-
});
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
/** Retention classification over this gate's evidence log (hot/cold/expired/legal-hold). */
|
|
1112
|
-
function retention(opts = {}) {
|
|
1113
|
-
return classifyRetention(evidence.all(), opts);
|
|
1114
|
-
}
|
|
1115
|
-
/** The auditor/SIEM export manifest for this gate's evidence log. */
|
|
1116
|
-
function retentionExport(opts = {}) {
|
|
1117
|
-
return buildRetentionExport(evidence.all(), opts);
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
return {
|
|
1121
|
-
check, run, recordExecution, route, wrapRoute: route, middleware, guard, reliancePacket, evidence,
|
|
1122
|
-
store: consumption, keyRegistry: registry, retention, retentionExport,
|
|
1123
|
-
};
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
export function createTrustedActionFirewall(opts = {}) {
|
|
1127
|
-
const { manifest = createDefaultActionRiskManifest(), ...rest } = opts;
|
|
1128
|
-
return createGate({ ...rest, manifest });
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
/**
|
|
1132
|
-
* EG-1 conformance for an existing gate. The gate MUST have been built trusting
|
|
1133
|
-
* `harness.publicKey` (otherwise every valid receipt is rejected and the gate
|
|
1134
|
-
* cannot earn EG-1). Returns the EG-1 JSON report.
|
|
1135
|
-
* @param {object} o
|
|
1136
|
-
* @param {object} o.gate an EMILIA Gate (createGate/createTrustedActionFirewall)
|
|
1137
|
-
* @param {object} o.harness the harness whose key the gate trusts (createEg1Harness)
|
|
1138
|
-
* @param {object} [o.action] the high-risk action to exercise
|
|
1139
|
-
* @param {object} [o.selector] the manifest selector for that action
|
|
1140
|
-
*/
|
|
1141
|
-
export async function gateConformance({ gate, harness, action, selector = EG1_DEFAULT_SELECTOR } = {}) {
|
|
1142
|
-
if (!gate || typeof gate.run !== 'function') {
|
|
1143
|
-
throw new Error('gateConformance requires a gate built trusting harness.publicKey');
|
|
1144
|
-
}
|
|
1145
|
-
if (!harness) throw new Error('gateConformance requires the harness whose key the gate trusts');
|
|
1146
|
-
const act = action || harness.action;
|
|
1147
|
-
const invoke = makeGateInvoke(gate, { selector, action: act });
|
|
1148
|
-
return runEg1({ invoke, harness, action: act });
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
/**
|
|
1152
|
-
* Self-certify the reference gate: build a default Consequence Firewall that
|
|
1153
|
-
* trusts a fresh EG-1 harness key, then run all eight checks. This is the
|
|
1154
|
-
* canonical "EMILIA Gate earns EG-1" proof — runnable as a CLI (`eg1.mjs`),
|
|
1155
|
-
* shown on /gate, and the template an adopter copies for their integration.
|
|
1156
|
-
*/
|
|
1157
|
-
export async function gateConformanceSelfTest({ now } = {}) {
|
|
1158
|
-
const harness = createEg1Harness({ now });
|
|
1159
|
-
const gate = createTrustedActionFirewall({
|
|
1160
|
-
trustedKeys: [harness.publicKey], approverKeys: harness.approverKeys,
|
|
1161
|
-
rpId: harness.rpId, allowedOrigins: harness.allowedOrigins,
|
|
1162
|
-
now, allowEphemeralStore: true,
|
|
1163
|
-
});
|
|
1164
|
-
return gateConformance({ gate, harness });
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
/**
|
|
1168
|
-
* CF-1 (Consequence Firewall) conformance for an existing gate. Runs the eight
|
|
1169
|
-
* EG-1 runtime checks plus the three CF-1 category checks: the action is
|
|
1170
|
-
* declared consequential by the manifest, a gate pinned to the WRONG issuer key
|
|
1171
|
-
* refuses a valid receipt, and the allowed run emits offline-verifiable reliance
|
|
1172
|
-
* evidence. The `gate` MUST trust `harness.publicKey`; `wrongGate` MUST trust a
|
|
1173
|
-
* DIFFERENT key (otherwise wrong_authority_refused cannot be demonstrated).
|
|
1174
|
-
* @param {object} o
|
|
1175
|
-
* @param {object} o.gate an EMILIA Gate trusting harness.publicKey
|
|
1176
|
-
* @param {object} [o.wrongGate] a sibling gate trusting a different (wrong) key
|
|
1177
|
-
* @param {object} o.harness from createEg1Harness()
|
|
1178
|
-
* @param {object} [o.manifest] the action-risk manifest (to resolve the requirement)
|
|
1179
|
-
* @param {object} [o.selector] the manifest selector for the action
|
|
1180
|
-
* @param {object} [o.action] the high-risk action to exercise
|
|
1181
|
-
*/
|
|
1182
|
-
export async function cf1Conformance({ gate, wrongGate, harness, manifest = null, selector = EG1_DEFAULT_SELECTOR, action } = {}) {
|
|
1183
|
-
if (!gate || typeof gate.run !== 'function') throw new Error('cf1Conformance requires a gate built trusting harness.publicKey');
|
|
1184
|
-
if (!harness) throw new Error('cf1Conformance requires the harness whose key the gate trusts');
|
|
1185
|
-
const act = action || harness.action;
|
|
1186
|
-
const invoke = makeGateInvoke(gate, { selector, action: act });
|
|
1187
|
-
const wrongInvoke = (wrongGate && typeof wrongGate.run === 'function')
|
|
1188
|
-
? makeGateInvoke(wrongGate, { selector, action: act }) : undefined;
|
|
1189
|
-
const requirement = manifest ? findActionRequirement(manifest, selector) : null;
|
|
1190
|
-
return runCf1({ invoke, wrongInvoke, harness, action: act, requirement });
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
/**
|
|
1194
|
-
* Self-certify the reference gate against CF-1: a default Trusted Action
|
|
1195
|
-
* Firewall trusting a fresh harness key, a sibling firewall trusting a DIFFERENT
|
|
1196
|
-
* key (for wrong_authority_refused), and the default action-risk manifest (for
|
|
1197
|
-
* consequential_action_declared). The canonical "reference gate earns CF-1"
|
|
1198
|
-
* proof — runnable as a CLI (`cf1.mjs`).
|
|
1199
|
-
*/
|
|
1200
|
-
export async function cf1ConformanceSelfTest({ now } = {}) {
|
|
1201
|
-
const harness = createEg1Harness({ now });
|
|
1202
|
-
const manifest = createDefaultActionRiskManifest();
|
|
1203
|
-
const gate = createTrustedActionFirewall({
|
|
1204
|
-
trustedKeys: [harness.publicKey], approverKeys: harness.approverKeys,
|
|
1205
|
-
rpId: harness.rpId, allowedOrigins: harness.allowedOrigins,
|
|
1206
|
-
now, allowEphemeralStore: true,
|
|
1207
|
-
});
|
|
1208
|
-
const wrongHarness = createEg1Harness({ now });
|
|
1209
|
-
const wrongGate = createTrustedActionFirewall({
|
|
1210
|
-
trustedKeys: [wrongHarness.publicKey], approverKeys: wrongHarness.approverKeys,
|
|
1211
|
-
rpId: wrongHarness.rpId, allowedOrigins: wrongHarness.allowedOrigins,
|
|
1212
|
-
now, allowEphemeralStore: true,
|
|
1213
|
-
});
|
|
1214
|
-
return cf1Conformance({ gate, wrongGate, harness, manifest, selector: EG1_DEFAULT_SELECTOR, action: harness.action });
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
export default {
|
|
1218
|
-
createGate,
|
|
1219
|
-
createTrustedActionFirewall,
|
|
1220
|
-
runBreakGlass,
|
|
1221
|
-
receiptAssuranceTier,
|
|
1222
|
-
businessAuthorizationRequirement,
|
|
1223
|
-
verifyBusinessAuthorization,
|
|
1224
|
-
verifyAdmissibilityAgainstPinnedProfile,
|
|
1225
|
-
ADMISSIBILITY_VERDICTS,
|
|
1226
|
-
MemoryConsumptionStore,
|
|
1227
|
-
createEvidenceLog,
|
|
1228
|
-
createAtomicEvidenceLog,
|
|
1229
|
-
createMemoryAtomicEvidenceBackend,
|
|
1230
|
-
ASSURANCE_TIERS,
|
|
1231
|
-
DEFAULT_GATE_MANIFEST,
|
|
1232
|
-
HIGH_RISK_ACTION_PACKS,
|
|
1233
|
-
gateConformance,
|
|
1234
|
-
gateConformanceSelfTest,
|
|
1235
|
-
cf1Conformance,
|
|
1236
|
-
cf1ConformanceSelfTest,
|
|
1237
|
-
CF1_VERSION,
|
|
1238
|
-
CF1_CHECKS,
|
|
1239
|
-
runCf1,
|
|
1240
|
-
createEg1Harness,
|
|
1241
|
-
runEg1,
|
|
1242
|
-
createKeyRegistry,
|
|
1243
|
-
asKeyRegistry,
|
|
1244
|
-
classifyRetention,
|
|
1245
|
-
buildRetentionExport,
|
|
1246
|
-
createDefaultActionControlManifest,
|
|
1247
|
-
findActionControl,
|
|
1248
|
-
validateActionControlManifest,
|
|
1249
|
-
};
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Compatibility entry point for the TypeScript index module.
|
|
3
|
+
export * from './dist/index.js';
|
|
4
|
+
export { default } from './dist/index.js';
|