@emilia-protocol/gate 0.10.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +91 -0
- package/README.md +272 -0
- package/action-control-manifest.js +3 -268
- package/action-escrow-custodian.js +4 -0
- package/action-escrow-evidence.js +4 -0
- package/action-escrow-package.js +4 -0
- package/action-escrow-postgres.js +4 -0
- package/action-escrow-state.js +4 -0
- package/action-escrow-verifiers.js +4 -0
- package/action-escrow.js +4 -0
- package/action-packs.js +3 -140
- package/adapters/_kit.js +3 -104
- package/adapters/aws.js +3 -108
- package/adapters/cloudflare.js +3 -54
- package/adapters/gcp.js +3 -70
- package/adapters/github.js +3 -109
- package/adapters/jira.js +3 -55
- package/adapters/k8s.js +3 -70
- package/adapters/linear.js +3 -55
- package/adapters/salesforce.js +3 -55
- package/adapters/stripe.js +3 -81
- package/adapters/supabase.js +3 -121
- package/adapters/terraform.js +3 -59
- package/adapters/vercel.js +3 -84
- package/aec-execution.js +3 -312
- package/breakglass.js +3 -582
- package/capability-receipt.js +4 -0
- package/cf1-conformance.js +3 -106
- package/challenge-store.js +3 -69
- package/control-plane.js +3 -340
- package/coverage.js +3 -721
- package/demo.mjs +1 -1
- package/deployment-attestation.js +3 -247
- package/dist/action-control-manifest.d.ts +82 -0
- package/dist/action-control-manifest.d.ts.map +1 -0
- package/dist/action-control-manifest.js +422 -0
- package/dist/action-control-manifest.js.map +1 -0
- package/dist/action-escrow-custodian.d.ts +74 -0
- package/dist/action-escrow-custodian.d.ts.map +1 -0
- package/dist/action-escrow-custodian.js +363 -0
- package/dist/action-escrow-custodian.js.map +1 -0
- package/dist/action-escrow-evidence.d.ts +130 -0
- package/dist/action-escrow-evidence.d.ts.map +1 -0
- package/dist/action-escrow-evidence.js +1012 -0
- package/dist/action-escrow-evidence.js.map +1 -0
- package/dist/action-escrow-package.d.ts +10 -0
- package/dist/action-escrow-package.d.ts.map +1 -0
- package/dist/action-escrow-package.js +1583 -0
- package/dist/action-escrow-package.js.map +1 -0
- package/dist/action-escrow-postgres.d.ts +62 -0
- package/dist/action-escrow-postgres.d.ts.map +1 -0
- package/dist/action-escrow-postgres.js +339 -0
- package/dist/action-escrow-postgres.js.map +1 -0
- package/dist/action-escrow-state.d.ts +141 -0
- package/dist/action-escrow-state.d.ts.map +1 -0
- package/dist/action-escrow-state.js +413 -0
- package/dist/action-escrow-state.js.map +1 -0
- package/dist/action-escrow-verifiers.d.ts +65 -0
- package/dist/action-escrow-verifiers.d.ts.map +1 -0
- package/dist/action-escrow-verifiers.js +421 -0
- package/dist/action-escrow-verifiers.js.map +1 -0
- package/dist/action-escrow.d.ts +112 -0
- package/dist/action-escrow.d.ts.map +1 -0
- package/dist/action-escrow.js +2661 -0
- package/dist/action-escrow.js.map +1 -0
- package/dist/action-packs.d.ts +54 -0
- package/dist/action-packs.d.ts.map +1 -0
- package/dist/action-packs.js +143 -0
- package/dist/action-packs.js.map +1 -0
- package/dist/adapters/_kit.d.ts +50 -0
- package/dist/adapters/_kit.d.ts.map +1 -0
- package/dist/adapters/_kit.js +98 -0
- package/dist/adapters/_kit.js.map +1 -0
- package/dist/adapters/aws.d.ts +158 -0
- package/dist/adapters/aws.d.ts.map +1 -0
- package/dist/adapters/aws.js +136 -0
- package/dist/adapters/aws.js.map +1 -0
- package/dist/adapters/cloudflare.d.ts +116 -0
- package/dist/adapters/cloudflare.d.ts.map +1 -0
- package/dist/adapters/cloudflare.js +75 -0
- package/dist/adapters/cloudflare.js.map +1 -0
- package/dist/adapters/gcp.d.ts +139 -0
- package/dist/adapters/gcp.d.ts.map +1 -0
- package/dist/adapters/gcp.js +69 -0
- package/dist/adapters/gcp.js.map +1 -0
- package/dist/adapters/github.d.ts +123 -0
- package/dist/adapters/github.d.ts.map +1 -0
- package/dist/adapters/github.js +105 -0
- package/dist/adapters/github.js.map +1 -0
- package/dist/adapters/jira.d.ts +109 -0
- package/dist/adapters/jira.d.ts.map +1 -0
- package/dist/adapters/jira.js +54 -0
- package/dist/adapters/jira.js.map +1 -0
- package/dist/adapters/k8s.d.ts +139 -0
- package/dist/adapters/k8s.d.ts.map +1 -0
- package/dist/adapters/k8s.js +69 -0
- package/dist/adapters/k8s.js.map +1 -0
- package/dist/adapters/linear.d.ts +109 -0
- package/dist/adapters/linear.d.ts.map +1 -0
- package/dist/adapters/linear.js +54 -0
- package/dist/adapters/linear.js.map +1 -0
- package/dist/adapters/salesforce.d.ts +109 -0
- package/dist/adapters/salesforce.d.ts.map +1 -0
- package/dist/adapters/salesforce.js +54 -0
- package/dist/adapters/salesforce.js.map +1 -0
- package/dist/adapters/stripe.d.ts +116 -0
- package/dist/adapters/stripe.d.ts.map +1 -0
- package/dist/adapters/stripe.js +77 -0
- package/dist/adapters/stripe.js.map +1 -0
- package/dist/adapters/supabase.d.ts +127 -0
- package/dist/adapters/supabase.d.ts.map +1 -0
- package/dist/adapters/supabase.js +112 -0
- package/dist/adapters/supabase.js.map +1 -0
- package/dist/adapters/terraform.d.ts +109 -0
- package/dist/adapters/terraform.d.ts.map +1 -0
- package/dist/adapters/terraform.js +58 -0
- package/dist/adapters/terraform.js.map +1 -0
- package/dist/adapters/vercel.d.ts +114 -0
- package/dist/adapters/vercel.d.ts.map +1 -0
- package/dist/adapters/vercel.js +81 -0
- package/dist/adapters/vercel.js.map +1 -0
- package/dist/aec-execution.d.ts +107 -0
- package/dist/aec-execution.d.ts.map +1 -0
- package/dist/aec-execution.js +334 -0
- package/dist/aec-execution.js.map +1 -0
- package/dist/breakglass.d.ts +205 -0
- package/dist/breakglass.d.ts.map +1 -0
- package/dist/breakglass.js +627 -0
- package/dist/breakglass.js.map +1 -0
- package/dist/capability-receipt.d.ts +601 -0
- package/dist/capability-receipt.d.ts.map +1 -0
- package/dist/capability-receipt.js +1177 -0
- package/dist/capability-receipt.js.map +1 -0
- package/dist/cf1-conformance.d.ts +83 -0
- package/dist/cf1-conformance.d.ts.map +1 -0
- package/dist/cf1-conformance.js +94 -0
- package/dist/cf1-conformance.js.map +1 -0
- package/dist/challenge-store.d.ts +20 -0
- package/dist/challenge-store.d.ts.map +1 -0
- package/dist/challenge-store.js +62 -0
- package/dist/challenge-store.js.map +1 -0
- package/dist/control-plane.d.ts +136 -0
- package/dist/control-plane.d.ts.map +1 -0
- package/dist/control-plane.js +349 -0
- package/dist/control-plane.js.map +1 -0
- package/dist/coverage.d.ts +156 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/coverage.js +768 -0
- package/dist/coverage.js.map +1 -0
- package/dist/deployment-attestation.d.ts +93 -0
- package/dist/deployment-attestation.d.ts.map +1 -0
- package/dist/deployment-attestation.js +262 -0
- package/dist/deployment-attestation.js.map +1 -0
- package/dist/eg1-conformance.d.ts +286 -0
- package/dist/eg1-conformance.d.ts.map +1 -0
- package/dist/eg1-conformance.js +379 -0
- package/dist/eg1-conformance.js.map +1 -0
- package/dist/enterprise.d.ts +88 -0
- package/dist/enterprise.d.ts.map +1 -0
- package/dist/enterprise.js +194 -0
- package/dist/enterprise.js.map +1 -0
- package/dist/evidence-postgres.d.ts +123 -0
- package/dist/evidence-postgres.d.ts.map +1 -0
- package/dist/evidence-postgres.js +343 -0
- package/dist/evidence-postgres.js.map +1 -0
- package/dist/evidence.d.ts +113 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +369 -0
- package/dist/evidence.js.map +1 -0
- package/dist/execution-binding.d.ts +49 -0
- package/dist/execution-binding.d.ts.map +1 -0
- package/dist/execution-binding.js +243 -0
- package/dist/execution-binding.js.map +1 -0
- package/dist/formal-runtime-map.d.ts +42 -0
- package/dist/formal-runtime-map.d.ts.map +1 -0
- package/dist/formal-runtime-map.js +23 -0
- package/dist/formal-runtime-map.js.map +1 -0
- package/dist/index.d.ts +1002 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1736 -0
- package/dist/index.js.map +1 -0
- package/dist/key-registry.d.ts +60 -0
- package/dist/key-registry.d.ts.map +1 -0
- package/dist/key-registry.js +144 -0
- package/dist/key-registry.js.map +1 -0
- package/dist/mcp.d.ts +53 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +112 -0
- package/dist/mcp.js.map +1 -0
- package/dist/metering.d.ts +80 -0
- package/dist/metering.d.ts.map +1 -0
- package/dist/metering.js +227 -0
- package/dist/metering.js.map +1 -0
- package/dist/metrics.d.ts +68 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +242 -0
- package/dist/metrics.js.map +1 -0
- package/dist/network-witness.d.ts +167 -0
- package/dist/network-witness.d.ts.map +1 -0
- package/dist/network-witness.js +520 -0
- package/dist/network-witness.js.map +1 -0
- package/dist/protocol-state.d.ts +141 -0
- package/dist/protocol-state.d.ts.map +1 -0
- package/dist/protocol-state.js +106 -0
- package/dist/protocol-state.js.map +1 -0
- package/dist/receipt-program.d.ts +59 -0
- package/dist/receipt-program.d.ts.map +1 -0
- package/dist/receipt-program.js +1053 -0
- package/dist/receipt-program.js.map +1 -0
- package/dist/reliance-kernel.d.ts +66 -0
- package/dist/reliance-kernel.d.ts.map +1 -0
- package/dist/reliance-kernel.js +103 -0
- package/dist/reliance-kernel.js.map +1 -0
- package/dist/reliance-packet.d.ts +67 -0
- package/dist/reliance-packet.d.ts.map +1 -0
- package/dist/reliance-packet.js +176 -0
- package/dist/reliance-packet.js.map +1 -0
- package/dist/remedy-program-postgres.d.ts +37 -0
- package/dist/remedy-program-postgres.d.ts.map +1 -0
- package/dist/remedy-program-postgres.js +291 -0
- package/dist/remedy-program-postgres.js.map +1 -0
- package/dist/remedy-program-receipt.d.ts +97 -0
- package/dist/remedy-program-receipt.d.ts.map +1 -0
- package/dist/remedy-program-receipt.js +817 -0
- package/dist/remedy-program-receipt.js.map +1 -0
- package/dist/remedy-program.d.ts +76 -0
- package/dist/remedy-program.d.ts.map +1 -0
- package/dist/remedy-program.js +1346 -0
- package/dist/remedy-program.js.map +1 -0
- package/dist/reports/art14.d.ts +101 -0
- package/dist/reports/art14.d.ts.map +1 -0
- package/dist/reports/art14.js +340 -0
- package/dist/reports/art14.js.map +1 -0
- package/dist/reports/assurance-package.d.ts +197 -0
- package/dist/reports/assurance-package.d.ts.map +1 -0
- package/dist/reports/assurance-package.js +313 -0
- package/dist/reports/assurance-package.js.map +1 -0
- package/dist/reports/auditor-workpaper.d.ts +194 -0
- package/dist/reports/auditor-workpaper.d.ts.map +1 -0
- package/dist/reports/auditor-workpaper.js +545 -0
- package/dist/reports/auditor-workpaper.js.map +1 -0
- package/dist/reports/external-verification.d.ts +76 -0
- package/dist/reports/external-verification.d.ts.map +1 -0
- package/dist/reports/external-verification.js +209 -0
- package/dist/reports/external-verification.js.map +1 -0
- package/dist/reports/reperform.d.ts +122 -0
- package/dist/reports/reperform.d.ts.map +1 -0
- package/dist/reports/reperform.js +432 -0
- package/dist/reports/reperform.js.map +1 -0
- package/dist/reports/underwriter.d.ts +127 -0
- package/dist/reports/underwriter.d.ts.map +1 -0
- package/dist/reports/underwriter.js +348 -0
- package/dist/reports/underwriter.js.map +1 -0
- package/dist/retention.d.ts +88 -0
- package/dist/retention.d.ts.map +1 -0
- package/dist/retention.js +90 -0
- package/dist/retention.js.map +1 -0
- package/dist/roster.d.ts +113 -0
- package/dist/roster.d.ts.map +1 -0
- package/dist/roster.js +261 -0
- package/dist/roster.js.map +1 -0
- package/dist/runtime-monitor.d.ts +132 -0
- package/dist/runtime-monitor.d.ts.map +1 -0
- package/dist/runtime-monitor.js +197 -0
- package/dist/runtime-monitor.js.map +1 -0
- package/dist/settlement.d.ts +68 -0
- package/dist/settlement.d.ts.map +1 -0
- package/dist/settlement.js +306 -0
- package/dist/settlement.js.map +1 -0
- package/dist/siem.d.ts +261 -0
- package/dist/siem.d.ts.map +1 -0
- package/dist/siem.js +247 -0
- package/dist/siem.js.map +1 -0
- package/dist/store-postgres.d.ts +107 -0
- package/dist/store-postgres.d.ts.map +1 -0
- package/dist/store-postgres.js +152 -0
- package/dist/store-postgres.js.map +1 -0
- package/dist/store.d.ts +95 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +202 -0
- package/dist/store.js.map +1 -0
- package/dist/strict-json.d.ts +14 -0
- package/dist/strict-json.d.ts.map +1 -0
- package/dist/strict-json.js +132 -0
- package/dist/strict-json.js.map +1 -0
- package/dist/trust-program-adapters.d.ts +122 -0
- package/dist/trust-program-adapters.d.ts.map +1 -0
- package/dist/trust-program-adapters.js +802 -0
- package/dist/trust-program-adapters.js.map +1 -0
- package/dist/trust-program-postgres.d.ts +41 -0
- package/dist/trust-program-postgres.d.ts.map +1 -0
- package/dist/trust-program-postgres.js +346 -0
- package/dist/trust-program-postgres.js.map +1 -0
- package/dist/trust-program-revocation.d.ts +206 -0
- package/dist/trust-program-revocation.d.ts.map +1 -0
- package/dist/trust-program-revocation.js +600 -0
- package/dist/trust-program-revocation.js.map +1 -0
- package/dist/trust-program.d.ts +197 -0
- package/dist/trust-program.d.ts.map +1 -0
- package/dist/trust-program.js +1351 -0
- package/dist/trust-program.js.map +1 -0
- package/dist/witness-postgres.d.ts +43 -0
- package/dist/witness-postgres.d.ts.map +1 -0
- package/dist/witness-postgres.js +92 -0
- package/dist/witness-postgres.js.map +1 -0
- package/dist/zk-range-proof.d.ts +78 -0
- package/dist/zk-range-proof.d.ts.map +1 -0
- package/dist/zk-range-proof.js +228 -0
- package/dist/zk-range-proof.js.map +1 -0
- package/eg1-conformance.js +3 -404
- package/enterprise.js +3 -180
- package/ep-assure.mjs +1 -0
- package/evidence-postgres.js +3 -356
- package/evidence.js +4 -360
- package/execution-binding.js +3 -212
- package/formal-runtime-map.js +4 -0
- package/index.js +4 -1249
- package/key-registry.js +3 -134
- package/mcp.js +3 -109
- package/metering.js +3 -226
- package/metrics.js +3 -231
- package/network-witness.js +3 -499
- package/package.json +47 -4
- package/receipt-program.js +4 -0
- package/reliance-kernel.js +3 -112
- package/reliance-packet.js +3 -175
- package/remedy-program-postgres.js +4 -0
- package/remedy-program-receipt.js +4 -0
- package/remedy-program.js +4 -0
- package/reports/art14.js +0 -0
- package/reports/assurance-package.js +2 -312
- package/reports/auditor-workpaper.js +3 -537
- package/reports/external-verification.js +3 -222
- package/reports/reperform.js +3 -424
- package/reports/underwriter.js +3 -339
- package/retention.js +3 -84
- package/roster.js +3 -264
- package/runtime-monitor.js +4 -0
- package/settlement.js +3 -299
- package/siem.js +3 -235
- package/src/action-control-manifest.ts +424 -0
- package/src/action-escrow-custodian.ts +427 -0
- package/src/action-escrow-evidence.ts +1178 -0
- package/src/action-escrow-package.ts +1821 -0
- package/src/action-escrow-postgres.ts +366 -0
- package/src/action-escrow-state.ts +498 -0
- package/src/action-escrow-verifiers.ts +484 -0
- package/src/action-escrow.ts +3039 -0
- package/src/action-packs.ts +170 -0
- package/src/adapters/_kit.ts +117 -0
- package/src/adapters/aws.ts +142 -0
- package/src/adapters/cloudflare.ts +78 -0
- package/src/adapters/gcp.ts +71 -0
- package/src/adapters/github.ts +110 -0
- package/src/adapters/jira.ts +56 -0
- package/src/adapters/k8s.ts +71 -0
- package/src/adapters/linear.ts +56 -0
- package/src/adapters/salesforce.ts +56 -0
- package/src/adapters/stripe.ts +82 -0
- package/src/adapters/supabase.ts +122 -0
- package/src/adapters/terraform.ts +60 -0
- package/src/adapters/vercel.ts +85 -0
- package/src/aec-execution.ts +354 -0
- package/src/breakglass.ts +647 -0
- package/src/capability-receipt.ts +1258 -0
- package/src/cf1-conformance.ts +120 -0
- package/src/challenge-store.ts +70 -0
- package/src/control-plane.ts +374 -0
- package/src/coverage.ts +832 -0
- package/src/deployment-attestation.ts +259 -0
- package/src/eg1-conformance.ts +471 -0
- package/src/enterprise.ts +195 -0
- package/src/evidence-postgres.ts +374 -0
- package/src/evidence.ts +367 -0
- package/src/execution-binding.ts +236 -0
- package/src/formal-runtime-map.ts +24 -0
- package/src/index.ts +2122 -0
- package/src/key-registry.ts +150 -0
- package/src/mcp.ts +120 -0
- package/src/metering.ts +237 -0
- package/src/metrics.ts +232 -0
- package/src/network-witness.ts +585 -0
- package/src/protocol-state.ts +143 -0
- package/src/protocol-state.typecheck.ts +94 -0
- package/src/receipt-program.ts +1105 -0
- package/src/reliance-kernel.ts +119 -0
- package/src/reliance-packet.ts +206 -0
- package/src/remedy-program-postgres.ts +367 -0
- package/src/remedy-program-receipt.ts +891 -0
- package/src/remedy-program.ts +1388 -0
- package/src/reports/art14.ts +361 -0
- package/src/reports/assurance-package.ts +330 -0
- package/src/reports/auditor-workpaper.ts +558 -0
- package/src/reports/external-verification.ts +232 -0
- package/src/reports/reperform.ts +441 -0
- package/src/reports/underwriter.ts +356 -0
- package/src/retention.ts +100 -0
- package/src/roster.ts +273 -0
- package/src/runtime-monitor.ts +217 -0
- package/src/settlement.ts +313 -0
- package/src/siem.ts +247 -0
- package/src/store-postgres.ts +167 -0
- package/src/store.ts +214 -0
- package/src/strict-json.ts +115 -0
- package/src/trust-program-adapters.ts +847 -0
- package/src/trust-program-postgres.ts +429 -0
- package/src/trust-program-revocation.ts +751 -0
- package/src/trust-program.ts +1460 -0
- package/src/witness-postgres.ts +106 -0
- package/src/zk-range-proof.ts +250 -0
- package/store-postgres.js +3 -161
- package/store.js +4 -198
- package/strict-json.js +3 -85
- package/trust-program-adapters.js +4 -0
- package/trust-program-postgres.js +4 -0
- package/trust-program-revocation.js +4 -0
- package/trust-program.js +4 -0
- package/witness-postgres.js +3 -96
- package/zk-range-proof.js +4 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* EMILIA Gate — reference DURABLE consumption backend: Postgres
|
|
5
|
+
* (EP-GATE-PG-CONSUMPTION-v2).
|
|
6
|
+
*
|
|
7
|
+
* Replay defense that survives restarts. This module implements the backend
|
|
8
|
+
* contract consumed by `createDurableConsumptionStore` in ./store.js:
|
|
9
|
+
*
|
|
10
|
+
* backend = {
|
|
11
|
+
* async addIfAbsent(key, value, { ttlSeconds }?) : boolean // true iff inserted
|
|
12
|
+
* async compareAndSet(key, expected, replacement, { ttlSeconds }?) : boolean
|
|
13
|
+
* async deleteIfValue(key, expected) : boolean
|
|
14
|
+
* async has(key) : boolean
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* plus a `cleanupExpired(now)` garbage-collection statement.
|
|
18
|
+
*
|
|
19
|
+
* Each transition is one SQL statement. `addIfAbsent` decides first use;
|
|
20
|
+
* `compareAndSet` and `deleteIfValue` bind commit/release to the opaque owner of
|
|
21
|
+
* the current reservation. There is no read-then-write interval in which a
|
|
22
|
+
* delayed worker can overwrite or remove a newer worker's reservation.
|
|
23
|
+
*
|
|
24
|
+
* FAIL-CLOSED CONTRACT: if the injected `query` THROWS (connection down,
|
|
25
|
+
* timeout, constraint other than the PK, ...) the error PROPAGATES to the
|
|
26
|
+
* caller — nothing here catches it. The gate must refuse an action when
|
|
27
|
+
* one-time-use cannot be proven; a backend error is NEVER treated as
|
|
28
|
+
* not-consumed (which would admit replays during an outage) and never treated
|
|
29
|
+
* as consumed-silently (which would mask the outage as a replay verdict).
|
|
30
|
+
* Likewise, an EXPIRED row that cleanup has not yet removed still refuses:
|
|
31
|
+
* TTL expiry is garbage collection, never a grant — the gate already rejects
|
|
32
|
+
* stale receipts on freshness, so nothing is lost by refusing conservatively.
|
|
33
|
+
*
|
|
34
|
+
* Deterministic by construction: the pg-style `query(text, params)` function
|
|
35
|
+
* and the clock (`now`) are both injected, so tests run against an in-memory
|
|
36
|
+
* fake with real ON CONFLICT semantics — no network, no database.
|
|
37
|
+
*/
|
|
38
|
+
export const PG_CONSUMPTION_VERSION = 'EP-GATE-PG-CONSUMPTION-v2';
|
|
39
|
+
/** Single consumption table. Timestamps are epoch milliseconds (BIGINT) so the
|
|
40
|
+
* injected JS clock maps 1:1 onto column values with no timezone ambiguity. */
|
|
41
|
+
export const CONSUMPTION_TABLE = 'ep_gate_consumption';
|
|
42
|
+
export const CONSUMPTION_TABLE_DDL = `CREATE TABLE IF NOT EXISTS ${CONSUMPTION_TABLE} (
|
|
43
|
+
consumption_key TEXT PRIMARY KEY,
|
|
44
|
+
state TEXT NOT NULL,
|
|
45
|
+
consumed_at BIGINT NOT NULL,
|
|
46
|
+
expires_at BIGINT
|
|
47
|
+
);
|
|
48
|
+
CREATE INDEX IF NOT EXISTS ${CONSUMPTION_TABLE}_expires_idx
|
|
49
|
+
ON ${CONSUMPTION_TABLE} (expires_at) WHERE expires_at IS NOT NULL;`;
|
|
50
|
+
/** The exact statements the backend issues — exported for transparency and so
|
|
51
|
+
* a fake client can implement them without parsing SQL. */
|
|
52
|
+
export const CONSUMPTION_SQL = {
|
|
53
|
+
health: `SELECT
|
|
54
|
+
to_regclass('public.${CONSUMPTION_TABLE}') IS NOT NULL AS table_ready,
|
|
55
|
+
CASE WHEN to_regclass('public.${CONSUMPTION_TABLE}') IS NULL THEN FALSE
|
|
56
|
+
ELSE has_table_privilege(current_user, to_regclass('public.${CONSUMPTION_TABLE}'), 'SELECT,INSERT,UPDATE,DELETE') END AS can_use`,
|
|
57
|
+
/** $1 key, $2 state, $3 consumed_at ms, $4 expires_at ms|null. rowCount 1 = consumed, 0 = replay. */
|
|
58
|
+
addIfAbsent: `INSERT INTO ${CONSUMPTION_TABLE} (consumption_key, state, consumed_at, expires_at) `
|
|
59
|
+
+ 'VALUES ($1, $2, $3, $4) ON CONFLICT (consumption_key) DO NOTHING',
|
|
60
|
+
/** $1 key, $2 expected state, $3 replacement, $4 consumed_at ms, $5 expires_at ms|null. */
|
|
61
|
+
compareAndSet: `UPDATE ${CONSUMPTION_TABLE} SET state = $3, consumed_at = $4, expires_at = $5 `
|
|
62
|
+
+ 'WHERE consumption_key = $1 AND state = $2',
|
|
63
|
+
/** $1 key, $2 expected state. */
|
|
64
|
+
deleteIfValue: `DELETE FROM ${CONSUMPTION_TABLE} WHERE consumption_key = $1 AND state = $2`,
|
|
65
|
+
/** $1 key. Any row — even an expired one — counts as consumed until cleaned. */
|
|
66
|
+
has: `SELECT 1 FROM ${CONSUMPTION_TABLE} WHERE consumption_key = $1`,
|
|
67
|
+
/** $1 now ms. Removes ONLY rows whose TTL has elapsed; NULL expires_at never expires. */
|
|
68
|
+
cleanupExpired: `DELETE FROM ${CONSUMPTION_TABLE} WHERE state LIKE 'committed:%' `
|
|
69
|
+
+ 'AND expires_at IS NOT NULL AND expires_at <= $1',
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Create the Postgres consumption backend.
|
|
73
|
+
* @param {{ query?: (text: string, params: any[]) => Promise<{ rowCount: number, rows?: any[] }>, now?: number|function }} [o]
|
|
74
|
+
* `query` is a pg-style query function (e.g. `pool.query.bind(pool)`). Injected
|
|
75
|
+
* so tests need no database. MUST throw on failure — errors propagate (fail
|
|
76
|
+
* closed). `now` is the injected clock (ms or () => ms).
|
|
77
|
+
* @returns backend conforming to createDurableConsumptionStore, plus cleanupExpired(now).
|
|
78
|
+
*/
|
|
79
|
+
export function createPostgresBackend({ query, now = Date.now, } = {}) {
|
|
80
|
+
if (typeof query !== 'function') {
|
|
81
|
+
throw new Error('createPostgresBackend: query must be an async (text, params) => { rowCount } function '
|
|
82
|
+
+ '(e.g. pg pool.query). It must THROW on failure — a backend error must never look like a verdict.');
|
|
83
|
+
}
|
|
84
|
+
let lastObservedNow = Number.NEGATIVE_INFINITY;
|
|
85
|
+
const checkedNow = (candidate) => {
|
|
86
|
+
if (!Number.isSafeInteger(candidate) || candidate < 0) {
|
|
87
|
+
throw new Error('consumption clock must return a non-negative safe-integer epoch millisecond');
|
|
88
|
+
}
|
|
89
|
+
if (candidate < lastObservedNow) {
|
|
90
|
+
throw new Error(`consumption clock regression refused: ${candidate} < ${lastObservedNow}`);
|
|
91
|
+
}
|
|
92
|
+
lastObservedNow = candidate;
|
|
93
|
+
return candidate;
|
|
94
|
+
};
|
|
95
|
+
const nowMs = () => checkedNow(typeof now === 'function' ? now() : now);
|
|
96
|
+
const expiryFor = (opt) => {
|
|
97
|
+
const ttl = Number(opt?.ttlSeconds);
|
|
98
|
+
return Number.isFinite(ttl) && ttl > 0 ? nowMs() + ttl * 1000 : null;
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
durable: true,
|
|
102
|
+
async health() {
|
|
103
|
+
const res = await query(CONSUMPTION_SQL.health, []);
|
|
104
|
+
if (!res || res.rowCount !== 1 || !Array.isArray(res.rows) || res.rows.length !== 1) {
|
|
105
|
+
throw new Error('consumption health: malformed Postgres result');
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
ok: res.rows[0].table_ready === true && res.rows[0].can_use === true,
|
|
109
|
+
version: PG_CONSUMPTION_VERSION,
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
/** True iff THIS call inserted the row — the atomic consumed-vs-replay decision. */
|
|
113
|
+
async addIfAbsent(key, value, opt) {
|
|
114
|
+
const res = await query(CONSUMPTION_SQL.addIfAbsent, [key, value, nowMs(), expiryFor(opt)]);
|
|
115
|
+
// Fail closed on a malformed driver result: without a numeric rowCount we
|
|
116
|
+
// cannot PROVE first-use, so refuse loudly rather than guess.
|
|
117
|
+
if (!res || typeof res.rowCount !== 'number') {
|
|
118
|
+
throw new Error('addIfAbsent: query result has no numeric rowCount — cannot prove one-time use');
|
|
119
|
+
}
|
|
120
|
+
return res.rowCount === 1;
|
|
121
|
+
},
|
|
122
|
+
/** Ownership-fenced reserved -> committed transition. */
|
|
123
|
+
async compareAndSet(key, expected, replacement, opt) {
|
|
124
|
+
const res = await query(CONSUMPTION_SQL.compareAndSet, [key, expected, replacement, nowMs(), expiryFor(opt)]);
|
|
125
|
+
if (!res || typeof res.rowCount !== 'number') {
|
|
126
|
+
throw new Error('compareAndSet: query result has no numeric rowCount — cannot prove reservation ownership');
|
|
127
|
+
}
|
|
128
|
+
return res.rowCount === 1;
|
|
129
|
+
},
|
|
130
|
+
/** Remove only the reservation owned by the caller. */
|
|
131
|
+
async deleteIfValue(key, expected) {
|
|
132
|
+
const res = await query(CONSUMPTION_SQL.deleteIfValue, [key, expected]);
|
|
133
|
+
if (!res || typeof res.rowCount !== 'number') {
|
|
134
|
+
throw new Error('deleteIfValue: query result has no numeric rowCount — cannot prove reservation ownership');
|
|
135
|
+
}
|
|
136
|
+
return res.rowCount === 1;
|
|
137
|
+
},
|
|
138
|
+
/** Present = consumed. Expired-but-uncleaned rows still count (conservative). */
|
|
139
|
+
async has(key) {
|
|
140
|
+
const res = await query(CONSUMPTION_SQL.has, [key]);
|
|
141
|
+
return (res?.rows?.length ?? res?.rowCount ?? 0) > 0;
|
|
142
|
+
},
|
|
143
|
+
/** Garbage-collect rows whose TTL elapsed. Returns the number removed. */
|
|
144
|
+
async cleanupExpired(at) {
|
|
145
|
+
const cleanupAt = at === undefined ? nowMs() : checkedNow(at);
|
|
146
|
+
const res = await query(CONSUMPTION_SQL.cleanupExpired, [cleanupAt]);
|
|
147
|
+
return res?.rowCount ?? 0;
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export default { createPostgresBackend, CONSUMPTION_TABLE_DDL, CONSUMPTION_TABLE, CONSUMPTION_SQL, PG_CONSUMPTION_VERSION };
|
|
152
|
+
//# sourceMappingURL=store-postgres.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-postgres.js","sourceRoot":"","sources":["../src/store-postgres.ts"],"names":[],"mappings":"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"}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @emilia-protocol/gate — consumption store (replay defense).
|
|
3
|
+
* @license Apache-2.0
|
|
4
|
+
*
|
|
5
|
+
* A receipt authorizes ONE action, once. The gate consumes a receipt's
|
|
6
|
+
* identifier the first time it is used; any later presentation of the same
|
|
7
|
+
* receipt is a replay and is refused. The in-memory store is an explicit
|
|
8
|
+
* test/demo opt-in; security-bearing gates use the durable contract below.
|
|
9
|
+
*/
|
|
10
|
+
export declare class MemoryConsumptionStore {
|
|
11
|
+
durable: boolean;
|
|
12
|
+
ownershipFenced: boolean;
|
|
13
|
+
permanentConsumption: boolean;
|
|
14
|
+
seen: Set<string>;
|
|
15
|
+
reserved: Set<string>;
|
|
16
|
+
constructor();
|
|
17
|
+
/** Returns true the FIRST time a key is seen, false on every replay. */
|
|
18
|
+
consume(key: any): Promise<boolean>;
|
|
19
|
+
/** Reserve an id while an action is in flight; blocks concurrent replay. */
|
|
20
|
+
reserve(key: any): Promise<boolean>;
|
|
21
|
+
/** Commit a reserved id after an external-effect attempt begins. */
|
|
22
|
+
commit(key: any): Promise<boolean>;
|
|
23
|
+
/** Release only when the caller can prove the external effect never began. */
|
|
24
|
+
release(key: any): Promise<boolean>;
|
|
25
|
+
has(key: any): Promise<boolean>;
|
|
26
|
+
get size(): number;
|
|
27
|
+
}
|
|
28
|
+
export declare const DURABLE_CONSUMPTION_VERSION = "EP-GATE-DURABLE-CONSUMPTION-v2";
|
|
29
|
+
/** Capability contract required by security-bearing Gate execution paths. */
|
|
30
|
+
export declare function isSecureConsumptionStore(store: any): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Production custody for replay defense: a durable consumption store backed by
|
|
33
|
+
* any shared key-value backend (Redis, Postgres, DynamoDB, ...), so a receipt
|
|
34
|
+
* consumed on one pod/lambda cannot be replayed on another.
|
|
35
|
+
*
|
|
36
|
+
* The backend MUST provide atomic insert-if-absent plus atomic conditional
|
|
37
|
+
* transition and delete. Together they make replay defense and reservation
|
|
38
|
+
* ownership sound under concurrency:
|
|
39
|
+
*
|
|
40
|
+
* backend = {
|
|
41
|
+
* async addIfAbsent(key, value): boolean
|
|
42
|
+
* async compareAndSet(key, expected, replacement): boolean
|
|
43
|
+
* async deleteIfValue(key, expected): boolean
|
|
44
|
+
* async has(key): boolean
|
|
45
|
+
* }
|
|
46
|
+
*
|
|
47
|
+
* State per receipt id is a single ownership-fenced value. A reservation is
|
|
48
|
+
* `reserved:v2:<random token>` and only the store instance holding that token
|
|
49
|
+
* can commit or release it. This prevents a delayed worker from deleting or
|
|
50
|
+
* committing a newer worker's reservation after timeout/failover.
|
|
51
|
+
*
|
|
52
|
+
* Reservations deliberately receive NO TTL. A crash after an external effect
|
|
53
|
+
* has begun is an indeterminate outcome, and automatically reopening the key
|
|
54
|
+
* would permit a duplicate effect. Operators must reconcile abandoned
|
|
55
|
+
* reservations. `ttlSeconds` applies only after a value is committed, when the
|
|
56
|
+
* receipt's own freshness window independently prevents reuse.
|
|
57
|
+
*/
|
|
58
|
+
/**
|
|
59
|
+
* @param {any} backend
|
|
60
|
+
* @param {{ ttlSeconds?: number, reservationTokenFactory?: () => string }} [options]
|
|
61
|
+
*/
|
|
62
|
+
export declare function createDurableConsumptionStore(backend: any, { ttlSeconds, reservationTokenFactory }?: {
|
|
63
|
+
ttlSeconds?: number | null;
|
|
64
|
+
reservationTokenFactory?: () => string;
|
|
65
|
+
}): {
|
|
66
|
+
durable: boolean;
|
|
67
|
+
ownershipFenced: boolean;
|
|
68
|
+
permanentConsumption: boolean;
|
|
69
|
+
retentionSeconds: number | null;
|
|
70
|
+
health(): Promise<any>;
|
|
71
|
+
reserve(key: any): Promise<boolean>;
|
|
72
|
+
commit(key: any): Promise<boolean>;
|
|
73
|
+
release(key: any): Promise<boolean>;
|
|
74
|
+
consume(key: any): Promise<boolean>;
|
|
75
|
+
has(key: any): Promise<boolean>;
|
|
76
|
+
};
|
|
77
|
+
/** In-memory reference backend (for tests/single-process). addIfAbsent is atomic in a single thread. */
|
|
78
|
+
export declare function createMemoryBackend(): {
|
|
79
|
+
durable: boolean;
|
|
80
|
+
addIfAbsent(key: any, value: any): Promise<boolean>;
|
|
81
|
+
compareAndSet(key: any, expected: any, replacement: any): Promise<boolean>;
|
|
82
|
+
deleteIfValue(key: any, expected: any): Promise<boolean>;
|
|
83
|
+
has(key: any): Promise<boolean>;
|
|
84
|
+
get(key: any): Promise<any>;
|
|
85
|
+
readonly size: number;
|
|
86
|
+
};
|
|
87
|
+
declare const _default: {
|
|
88
|
+
MemoryConsumptionStore: typeof MemoryConsumptionStore;
|
|
89
|
+
createDurableConsumptionStore: typeof createDurableConsumptionStore;
|
|
90
|
+
createMemoryBackend: typeof createMemoryBackend;
|
|
91
|
+
isSecureConsumptionStore: typeof isSecureConsumptionStore;
|
|
92
|
+
DURABLE_CONSUMPTION_VERSION: string;
|
|
93
|
+
};
|
|
94
|
+
export default _default;
|
|
95
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,qBAAa,sBAAsB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,EAAE,OAAO,CAAC;IACzB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAClB,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;;IAUtB,wEAAwE;IAClE,OAAO,CAAC,GAAG,KAAA;IAMjB,4EAA4E;IACtE,OAAO,CAAC,GAAG,KAAA;IAMjB,oEAAoE;IAC9D,MAAM,CAAC,GAAG,KAAA;IAMhB,8EAA8E;IACxE,OAAO,CAAC,GAAG,KAAA;IAKX,GAAG,CAAC,GAAG,KAAA;IAIb,IAAI,IAAI,WAEP;CACF;AAED,eAAO,MAAM,2BAA2B,mCAAmC,CAAC;AAE5E,6EAA6E;AAC7E,wBAAgB,wBAAwB,CAAC,KAAK,KAAA,WAQ7C;AAYD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH;;;GAGG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,KAAA,EACP,EAAE,UAAU,EAAE,uBAAiD,EAAE,GAAE;IACjE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,uBAAuB,CAAC,EAAE,MAAM,MAAM,CAAC;CACnC;;;;;;;;;;;EAiEP;AAED,wGAAwG;AACxG,wBAAgB,mBAAmB;;;;;;;;EAkBlC;;;;;;;;AAED,wBAME"}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* @emilia-protocol/gate — consumption store (replay defense).
|
|
4
|
+
* @license Apache-2.0
|
|
5
|
+
*
|
|
6
|
+
* A receipt authorizes ONE action, once. The gate consumes a receipt's
|
|
7
|
+
* identifier the first time it is used; any later presentation of the same
|
|
8
|
+
* receipt is a replay and is refused. The in-memory store is an explicit
|
|
9
|
+
* test/demo opt-in; security-bearing gates use the durable contract below.
|
|
10
|
+
*/
|
|
11
|
+
export class MemoryConsumptionStore {
|
|
12
|
+
durable;
|
|
13
|
+
ownershipFenced;
|
|
14
|
+
permanentConsumption;
|
|
15
|
+
seen;
|
|
16
|
+
reserved;
|
|
17
|
+
constructor() {
|
|
18
|
+
this.durable = false;
|
|
19
|
+
this.ownershipFenced = false;
|
|
20
|
+
this.permanentConsumption = false;
|
|
21
|
+
this.seen = new Set();
|
|
22
|
+
this.reserved = new Set();
|
|
23
|
+
}
|
|
24
|
+
/** Returns true the FIRST time a key is seen, false on every replay. */
|
|
25
|
+
async consume(key) {
|
|
26
|
+
if (this.seen.has(key) || this.reserved.has(key))
|
|
27
|
+
return false;
|
|
28
|
+
this.seen.add(key);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
/** Reserve an id while an action is in flight; blocks concurrent replay. */
|
|
32
|
+
async reserve(key) {
|
|
33
|
+
if (this.seen.has(key) || this.reserved.has(key))
|
|
34
|
+
return false;
|
|
35
|
+
this.reserved.add(key);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
/** Commit a reserved id after an external-effect attempt begins. */
|
|
39
|
+
async commit(key) {
|
|
40
|
+
this.reserved.delete(key);
|
|
41
|
+
this.seen.add(key);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
/** Release only when the caller can prove the external effect never began. */
|
|
45
|
+
async release(key) {
|
|
46
|
+
this.reserved.delete(key);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
async has(key) {
|
|
50
|
+
return this.seen.has(key) || this.reserved.has(key);
|
|
51
|
+
}
|
|
52
|
+
get size() {
|
|
53
|
+
return this.seen.size;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export const DURABLE_CONSUMPTION_VERSION = 'EP-GATE-DURABLE-CONSUMPTION-v2';
|
|
57
|
+
/** Capability contract required by security-bearing Gate execution paths. */
|
|
58
|
+
export function isSecureConsumptionStore(store) {
|
|
59
|
+
if (!store || typeof store !== 'object')
|
|
60
|
+
return false;
|
|
61
|
+
return store.durable === true
|
|
62
|
+
&& store.ownershipFenced === true
|
|
63
|
+
&& store.permanentConsumption === true
|
|
64
|
+
&& typeof store.consume === 'function'
|
|
65
|
+
&& typeof store.reserve === 'function'
|
|
66
|
+
&& typeof store.commit === 'function';
|
|
67
|
+
}
|
|
68
|
+
const COMMITTED_VALUE = 'committed:v2';
|
|
69
|
+
const RESERVED_PREFIX = 'reserved:v2:';
|
|
70
|
+
function defaultReservationToken() {
|
|
71
|
+
if (typeof globalThis.crypto?.randomUUID !== 'function') {
|
|
72
|
+
throw new Error('secure crypto.randomUUID() is required for durable reservation fencing');
|
|
73
|
+
}
|
|
74
|
+
return globalThis.crypto.randomUUID();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Production custody for replay defense: a durable consumption store backed by
|
|
78
|
+
* any shared key-value backend (Redis, Postgres, DynamoDB, ...), so a receipt
|
|
79
|
+
* consumed on one pod/lambda cannot be replayed on another.
|
|
80
|
+
*
|
|
81
|
+
* The backend MUST provide atomic insert-if-absent plus atomic conditional
|
|
82
|
+
* transition and delete. Together they make replay defense and reservation
|
|
83
|
+
* ownership sound under concurrency:
|
|
84
|
+
*
|
|
85
|
+
* backend = {
|
|
86
|
+
* async addIfAbsent(key, value): boolean
|
|
87
|
+
* async compareAndSet(key, expected, replacement): boolean
|
|
88
|
+
* async deleteIfValue(key, expected): boolean
|
|
89
|
+
* async has(key): boolean
|
|
90
|
+
* }
|
|
91
|
+
*
|
|
92
|
+
* State per receipt id is a single ownership-fenced value. A reservation is
|
|
93
|
+
* `reserved:v2:<random token>` and only the store instance holding that token
|
|
94
|
+
* can commit or release it. This prevents a delayed worker from deleting or
|
|
95
|
+
* committing a newer worker's reservation after timeout/failover.
|
|
96
|
+
*
|
|
97
|
+
* Reservations deliberately receive NO TTL. A crash after an external effect
|
|
98
|
+
* has begun is an indeterminate outcome, and automatically reopening the key
|
|
99
|
+
* would permit a duplicate effect. Operators must reconcile abandoned
|
|
100
|
+
* reservations. `ttlSeconds` applies only after a value is committed, when the
|
|
101
|
+
* receipt's own freshness window independently prevents reuse.
|
|
102
|
+
*/
|
|
103
|
+
/**
|
|
104
|
+
* @param {any} backend
|
|
105
|
+
* @param {{ ttlSeconds?: number, reservationTokenFactory?: () => string }} [options]
|
|
106
|
+
*/
|
|
107
|
+
export function createDurableConsumptionStore(backend, { ttlSeconds, reservationTokenFactory = defaultReservationToken } = {}) {
|
|
108
|
+
for (const m of ['addIfAbsent', 'compareAndSet', 'deleteIfValue', 'has']) {
|
|
109
|
+
if (typeof backend?.[m] !== 'function') {
|
|
110
|
+
throw new Error(`createDurableConsumptionStore: backend must implement async ${m}(). `
|
|
111
|
+
+ 'addIfAbsent and conditional transitions MUST be atomic or replay defense is not fleet-safe.');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (typeof reservationTokenFactory !== 'function') {
|
|
115
|
+
throw new Error('createDurableConsumptionStore: reservationTokenFactory must be a function');
|
|
116
|
+
}
|
|
117
|
+
if (ttlSeconds !== undefined && ttlSeconds !== null
|
|
118
|
+
&& (!Number.isSafeInteger(ttlSeconds) || ttlSeconds <= 0)) {
|
|
119
|
+
throw new Error('createDurableConsumptionStore: ttlSeconds must be a positive safe integer when supplied');
|
|
120
|
+
}
|
|
121
|
+
const opt = ttlSeconds ? { ttlSeconds } : undefined;
|
|
122
|
+
const ownedReservations = new Map();
|
|
123
|
+
function ownedValue(key) {
|
|
124
|
+
const token = ownedReservations.get(key);
|
|
125
|
+
if (!token) {
|
|
126
|
+
throw new Error(`durable consumption transition refused: this store does not own reservation ${key}`);
|
|
127
|
+
}
|
|
128
|
+
return `${RESERVED_PREFIX}${token}`;
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
durable: backend.durable === true,
|
|
132
|
+
ownershipFenced: true,
|
|
133
|
+
permanentConsumption: ttlSeconds === undefined || ttlSeconds === null,
|
|
134
|
+
retentionSeconds: ttlSeconds ?? null,
|
|
135
|
+
async health() {
|
|
136
|
+
if (typeof backend.health !== 'function')
|
|
137
|
+
return { ok: false, reason: 'backend_health_unavailable' };
|
|
138
|
+
return backend.health();
|
|
139
|
+
},
|
|
140
|
+
async reserve(key) {
|
|
141
|
+
const token = reservationTokenFactory();
|
|
142
|
+
if (typeof token !== 'string' || token.length < 16) {
|
|
143
|
+
throw new Error('reservationTokenFactory must return an unpredictable string of at least 16 characters');
|
|
144
|
+
}
|
|
145
|
+
const inserted = (await backend.addIfAbsent(key, `${RESERVED_PREFIX}${token}`)) === true;
|
|
146
|
+
if (inserted)
|
|
147
|
+
ownedReservations.set(key, token);
|
|
148
|
+
return inserted;
|
|
149
|
+
},
|
|
150
|
+
async commit(key) {
|
|
151
|
+
const expected = ownedValue(key);
|
|
152
|
+
const changed = await backend.compareAndSet(key, expected, COMMITTED_VALUE, opt);
|
|
153
|
+
if (changed !== true) {
|
|
154
|
+
throw new Error(`durable consumption commit refused: reservation ownership was lost for ${key}`);
|
|
155
|
+
}
|
|
156
|
+
ownedReservations.delete(key);
|
|
157
|
+
return true;
|
|
158
|
+
},
|
|
159
|
+
async release(key) {
|
|
160
|
+
const expected = ownedValue(key);
|
|
161
|
+
const deleted = await backend.deleteIfValue(key, expected);
|
|
162
|
+
ownedReservations.delete(key);
|
|
163
|
+
if (deleted !== true) {
|
|
164
|
+
throw new Error(`durable consumption release refused: reservation ownership was lost for ${key}`);
|
|
165
|
+
}
|
|
166
|
+
return true;
|
|
167
|
+
},
|
|
168
|
+
async consume(key) { return (await backend.addIfAbsent(key, COMMITTED_VALUE, opt)) === true; },
|
|
169
|
+
async has(key) { return (await backend.has(key)) === true; },
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/** In-memory reference backend (for tests/single-process). addIfAbsent is atomic in a single thread. */
|
|
173
|
+
export function createMemoryBackend() {
|
|
174
|
+
const map = new Map();
|
|
175
|
+
return {
|
|
176
|
+
durable: false,
|
|
177
|
+
async addIfAbsent(key, value) { if (map.has(key))
|
|
178
|
+
return false; map.set(key, value); return true; },
|
|
179
|
+
async compareAndSet(key, expected, replacement) {
|
|
180
|
+
if (map.get(key) !== expected)
|
|
181
|
+
return false;
|
|
182
|
+
map.set(key, replacement);
|
|
183
|
+
return true;
|
|
184
|
+
},
|
|
185
|
+
async deleteIfValue(key, expected) {
|
|
186
|
+
if (map.get(key) !== expected)
|
|
187
|
+
return false;
|
|
188
|
+
return map.delete(key);
|
|
189
|
+
},
|
|
190
|
+
async has(key) { return map.has(key); },
|
|
191
|
+
async get(key) { return map.get(key); },
|
|
192
|
+
get size() { return map.size; },
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
export default {
|
|
196
|
+
MemoryConsumptionStore,
|
|
197
|
+
createDurableConsumptionStore,
|
|
198
|
+
createMemoryBackend,
|
|
199
|
+
isSecureConsumptionStore,
|
|
200
|
+
DURABLE_CONSUMPTION_VERSION,
|
|
201
|
+
};
|
|
202
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const MAX_JSON_DEPTH = 64;
|
|
2
|
+
export declare function strictJsonGate(raw: any): {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
reason: string;
|
|
5
|
+
} | {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
reason?: undefined;
|
|
8
|
+
};
|
|
9
|
+
declare const _default: {
|
|
10
|
+
strictJsonGate: typeof strictJsonGate;
|
|
11
|
+
MAX_JSON_DEPTH: number;
|
|
12
|
+
};
|
|
13
|
+
export default _default;
|
|
14
|
+
//# sourceMappingURL=strict-json.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strict-json.d.ts","sourceRoot":"","sources":["../src/strict-json.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,cAAc,KAAK,CAAC;AAoBjC,wBAAgB,cAAc,CAAC,GAAG,KAAA;;;;;;EAwFjC;;;;;AAED,wBAAkD"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// Duplicate-name and Unicode-scalar gate for signed nested JSON such as
|
|
4
|
+
// WebAuthn clientDataJSON. JSON.parse remains the syntax gate.
|
|
5
|
+
export const MAX_JSON_DEPTH = 64;
|
|
6
|
+
function hasUnpairedUtf16Surrogate(value) {
|
|
7
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
8
|
+
const code = value.charCodeAt(index);
|
|
9
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
10
|
+
const next = value.charCodeAt(index + 1);
|
|
11
|
+
if (next < 0xdc00 || next > 0xdfff)
|
|
12
|
+
return true;
|
|
13
|
+
index += 1;
|
|
14
|
+
}
|
|
15
|
+
else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
export function strictJsonGate(raw) {
|
|
22
|
+
if (typeof raw !== 'string')
|
|
23
|
+
return { ok: false, reason: 'JSON input must be text' };
|
|
24
|
+
if (hasUnpairedUtf16Surrogate(raw)) {
|
|
25
|
+
return { ok: false, reason: 'unpaired Unicode surrogate' };
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
JSON.parse(raw);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return { ok: false, reason: 'invalid JSON syntax' };
|
|
32
|
+
}
|
|
33
|
+
let index = 0;
|
|
34
|
+
const stack = [];
|
|
35
|
+
let reason = null;
|
|
36
|
+
const escapes = { '"': '"', '\\': '\\', '/': '/', b: '\b', f: '\f', n: '\n', r: '\r', t: '\t' };
|
|
37
|
+
function readString() {
|
|
38
|
+
index += 1;
|
|
39
|
+
let output = '';
|
|
40
|
+
while (index < raw.length) {
|
|
41
|
+
const character = raw[index];
|
|
42
|
+
if (character === '"') {
|
|
43
|
+
index += 1;
|
|
44
|
+
return output;
|
|
45
|
+
}
|
|
46
|
+
if (character !== '\\') {
|
|
47
|
+
output += character;
|
|
48
|
+
index += 1;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const escape = raw[index + 1];
|
|
52
|
+
if (escape !== 'u') {
|
|
53
|
+
output += escapes[escape] ?? '';
|
|
54
|
+
index += 2;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const first = Number.parseInt(raw.slice(index + 2, index + 6), 16);
|
|
58
|
+
index += 6;
|
|
59
|
+
if (first >= 0xd800 && first <= 0xdbff) {
|
|
60
|
+
if (raw[index] === '\\' && raw[index + 1] === 'u') {
|
|
61
|
+
const second = Number.parseInt(raw.slice(index + 2, index + 6), 16);
|
|
62
|
+
if (second >= 0xdc00 && second <= 0xdfff) {
|
|
63
|
+
output += String.fromCharCode(first, second);
|
|
64
|
+
index += 6;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
reason = 'unpaired high surrogate escape';
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
if (first >= 0xdc00 && first <= 0xdfff) {
|
|
72
|
+
reason = 'unpaired low surrogate escape';
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
output += String.fromCharCode(first);
|
|
76
|
+
}
|
|
77
|
+
reason = 'unterminated string';
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
while (index < raw.length) {
|
|
81
|
+
const character = raw[index];
|
|
82
|
+
if (character === '{') {
|
|
83
|
+
stack.push({ object: true, keys: new Set(), expectsKey: true });
|
|
84
|
+
if (stack.length > MAX_JSON_DEPTH)
|
|
85
|
+
return { ok: false, reason: `nesting depth exceeds ${MAX_JSON_DEPTH}` };
|
|
86
|
+
index += 1;
|
|
87
|
+
}
|
|
88
|
+
else if (character === '[') {
|
|
89
|
+
stack.push({ object: false });
|
|
90
|
+
if (stack.length > MAX_JSON_DEPTH)
|
|
91
|
+
return { ok: false, reason: `nesting depth exceeds ${MAX_JSON_DEPTH}` };
|
|
92
|
+
index += 1;
|
|
93
|
+
}
|
|
94
|
+
else if (character === '}' || character === ']') {
|
|
95
|
+
stack.pop();
|
|
96
|
+
index += 1;
|
|
97
|
+
}
|
|
98
|
+
else if (character === ',') {
|
|
99
|
+
const top = stack.at(-1);
|
|
100
|
+
if (top?.object)
|
|
101
|
+
top.expectsKey = true;
|
|
102
|
+
index += 1;
|
|
103
|
+
}
|
|
104
|
+
else if (character === '"') {
|
|
105
|
+
const top = stack.at(-1);
|
|
106
|
+
const isKey = Boolean(top?.object && top.expectsKey);
|
|
107
|
+
const value = readString();
|
|
108
|
+
if (reason)
|
|
109
|
+
return { ok: false, reason };
|
|
110
|
+
if (isKey) {
|
|
111
|
+
// `isKey` is only true when `top?.object && top.expectsKey` held above,
|
|
112
|
+
// which guarantees `top` is a defined object-frame here; narrow the
|
|
113
|
+
// type for the compiler without altering the runtime reference.
|
|
114
|
+
const frame = top;
|
|
115
|
+
// `readString()` only ever returns null on a path that also sets
|
|
116
|
+
// `reason`, and the `if (reason) return` above already exited in
|
|
117
|
+
// that case, so `value` is guaranteed to be a string here.
|
|
118
|
+
const key = value;
|
|
119
|
+
if (frame.keys.has(key))
|
|
120
|
+
return { ok: false, reason: 'duplicate object member name' };
|
|
121
|
+
frame.keys.add(key);
|
|
122
|
+
frame.expectsKey = false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
index += 1;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { ok: true };
|
|
130
|
+
}
|
|
131
|
+
export default { strictJsonGate, MAX_JSON_DEPTH };
|
|
132
|
+
//# sourceMappingURL=strict-json.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strict-json.js","sourceRoot":"","sources":["../src/strict-json.ts"],"names":[],"mappings":"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"}
|