@reapp-sdk/express-middleware 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,206 +1,168 @@
1
- # @reapp-sdk/express-middleware 0.1.0
1
+ # @reapp-sdk/express-middleware 0.2.0
2
2
 
3
- Fail-closed Express middleware for REAPP payment-gated APIs on Stellar.
3
+ Fail-closed Express 4/5 paid JSON routes for REAPP on Stellar.
4
4
 
5
- The middleware issues an x402-style `402` challenge, accepts an `X-PAYMENT`
6
- settlement proof, and independently verifies the successful Stellar transaction
7
- before a protected handler runs. The transaction must contain one unambiguous
8
- payment event from the configured MandateRegistry and one matching SEP-41
9
- transfer from the mandate user to the configured merchant. Header claims never
10
- replace contract evidence.
11
-
12
- Supports Express 4 and Express 5. Ships as ESM with TypeScript declarations.
5
+ The package authenticates an exact-origin GET challenge, verifies the on-chain
6
+ settlement independently, atomically claims fulfillment, stores the exact JSON
7
+ result before sending it, and replays those bytes on recovery. A settlement can
8
+ never re-run arbitrary fulfillment work.
13
9
 
14
10
  ## Install
15
11
 
16
12
  ```bash
17
- npm install @reapp-sdk/express-middleware express
13
+ npm install @reapp-sdk/express-middleware@0.2.0 express@5.2.1
18
14
  ```
19
15
 
20
- ## Quick start
16
+ The exact T2 compatibility name `@reapp/express-middleware` exposes the same
17
+ typed ESM API.
18
+
19
+ ## Safe paid route
21
20
 
22
21
  ```ts
23
22
  import express from "express";
24
23
  import {
25
- InMemoryRedemptionStore,
26
- createReappPaymentMiddleware,
27
- getVerifiedPayment,
24
+ InMemoryBoundRedemptionStore,
25
+ createBoundReappPaidJsonRoute,
28
26
  } from "@reapp-sdk/express-middleware";
29
27
 
30
28
  const app = express();
31
29
 
32
- const requirePayment = createReappPaymentMiddleware({
30
+ const paidResearch = createBoundReappPaidJsonRoute({
33
31
  merchant: process.env.REAPP_MERCHANT_ADDRESS!,
34
32
  sourceAccount: process.env.REAPP_READ_SOURCE_ADDRESS!,
33
+ audience: "https://api.example", // exact public origin; never Host-derived
34
+ challengeSecret: process.env.REAPP_CHALLENGE_SECRET!, // at least 32 bytes
35
35
  amount: "1.00",
36
- resource: "/source/market",
37
- // Local development and one-process demos only. See Production replay safety.
38
- redemptionStore: new InMemoryRedemptionStore(),
39
- });
40
-
41
- app.get("/source/market", requirePayment, (_request, response) => {
42
- const payment = getVerifiedPayment(response);
43
- response.json({
44
- source: "verified research data",
45
- settlement: payment?.txHash,
46
- mandate: payment?.mandateId,
47
- });
48
- });
49
-
36
+ resource: (request) => request.originalUrl,
37
+ // One-process demo only. Production needs a shared linearizable store.
38
+ redemptionStore: new InMemoryBoundRedemptionStore(),
39
+ }, async ({ request, payment }) => ({
40
+ body: {
41
+ ok: true,
42
+ resource: request.params.id,
43
+ data: await loadResearchOnce(request.params.id),
44
+ settledTx: payment.txHash,
45
+ },
46
+ }));
47
+
48
+ app.get("/source/:id", paidResearch);
50
49
  app.listen(4021);
51
50
  ```
52
51
 
53
- `sourceAccount` is a funded Stellar `G...` address used only to simulate the
54
- read-only `get_mandate` call. The verifier never signs or submits a transaction.
52
+ The fulfillment callback receives no Express `Response` and cannot stream. Its
53
+ JSON result is bounded, hashed, and committed to the same redemption store
54
+ before any bytes are written to the client.
55
55
 
56
- ## What is verified
56
+ ## Bound-v2 authorization
57
57
 
58
- For each paid retry, the default verifier requires all of the following:
58
+ Before fulfillment is claimed, the package requires:
59
59
 
60
- 1. The configured RPC reports the exact configured network passphrase.
61
- 2. The transaction hash is valid, successful, and inside the configured ledger
62
- freshness window.
63
- 3. Exactly one contract event is emitted by the configured MandateRegistry with
64
- topics `payment` and the configured merchant, plus a 32-byte mandate id and
65
- an amount meeting the request price.
66
- 4. The event-derived mandate currently stored by the contract names the same
67
- merchant and SEP-41 asset and supplies the user and agent identities.
68
- 5. The same transaction contains exactly one matching transfer emitted by that
69
- asset, from the mandate user to the merchant, for the exact event amount.
70
- 6. A shared redemption store atomically consumes the network + registry +
71
- transaction key before the protected handler runs.
60
+ 1. `REAPP-PAYMENT-CAPABILITIES: reapp-bound-v2`; older clients receive `426`
61
+ before payment.
62
+ 2. An HMAC-authenticated challenge binding the exact public origin, GET method,
63
+ path and query, network identity, registry, merchant, asset, amount, decimals,
64
+ random id, and first-redemption deadline.
65
+ 3. A canonical proof whose Stellar Ed25519 signature binds that challenge,
66
+ transaction hash, and mandate id to the chain-derived mandate agent.
67
+ 4. The configured RPC's exact network passphrase and a successful fresh
68
+ transaction.
69
+ 5. One unambiguous payment event from the configured MandateRegistry.
70
+ 6. Current mandate user, agent, merchant, and asset identities.
71
+ 7. One matching same-transaction SEP-41 transfer from user to merchant.
72
72
 
73
- Only the transaction hash crosses from the HTTP adapter into the verifier.
74
- Caller-supplied `amount` and `mandateId` fields remain wire-format hints; the
75
- verified values placed in `response.locals` come from Stellar and the contract.
73
+ A copied public transaction hash cannot unlock data. Relaying a genuine quote
74
+ through another origin fails before the client pays because the signed audience
75
+ must equal the requested URL origin.
76
76
 
77
- ## Production replay safety
77
+ ## Atomic fulfillment state
78
78
 
79
- `InMemoryRedemptionStore` is intentionally limited to one Node.js process. A
80
- production deployment must provide a durable `RedemptionStore` whose
81
- `consumeOnce` operation is linearizable across every worker and host:
79
+ `BoundRedemptionStore` owns settlement binding and immutable response bytes in
80
+ one linearizable state machine:
82
81
 
83
- ```ts
84
- import type {
85
- RedemptionRecord,
86
- RedemptionStore,
87
- } from "@reapp-sdk/express-middleware";
88
-
89
- class SharedRedemptionStore implements RedemptionStore {
90
- async consumeOnce(
91
- record: Readonly<RedemptionRecord>,
92
- ): Promise<"consumed" | "duplicate"> {
93
- // Perform one atomic set-if-absent in your durable database.
94
- // Never delete a consumed key merely because downstream delivery failed.
95
- return atomicInsert(record.key, record) ? "consumed" : "duplicate";
96
- }
97
- }
82
+ ```text
83
+ missing -> executing -> completed(exact JSON bytes)
98
84
  ```
99
85
 
100
- Only the caller that receives `consumed` reaches the protected handler. A store
101
- error returns `503` and never serves the resource.
86
+ - First valid proof: chain verification, atomic claim, one callback execution.
87
+ - Same proof while executing: `503`; the callback is never started again.
88
+ - Same proof after completion: exact stored bytes are replayed; no verifier or
89
+ callback runs.
90
+ - Same transaction with another proof: `409`.
91
+ - Store/RPC outage: `503`; no protected result is sent.
92
+ - Callback exception: one sanitized terminal JSON result is stored and replayed.
93
+ - Completion-store failure: no result bytes are sent and the claim remains
94
+ executing; recovery cannot re-run it automatically. After confirming the
95
+ execution owner is dead, trusted operator/outbox code calls
96
+ `resolveBoundReappInterruptedDelivery` to store one terminal result.
97
+
98
+ The first-redemption deadline does not prevent later replay of an already
99
+ completed exact result. That replay is delivery recovery, not fresh payment
100
+ authorization.
101
+
102
+ ## Store deployment boundary
103
+
104
+ `InMemoryBoundRedemptionStore` is only for one-process demos and tests.
105
+ `FileBoundRedemptionStore` in the reference fulfillment app is restart-safe for
106
+ one Node.js process; instances targeting the same normalized path share one
107
+ in-process queue and use fsynced atomic replacement. It is not multi-process or
108
+ multi-host storage.
109
+
110
+ A production deployment must implement `BoundRedemptionStore.lookup`, `claim`,
111
+ and `complete` in one shared durable linearizable database. Never add a lease
112
+ that silently turns an executing claim back into runnable work. Side effects
113
+ must be transactionally coordinated with the claim through a durable job/outbox.
102
114
 
103
115
  ## Response behavior
104
116
 
105
- | Condition | Status | Behavior |
106
- |---|---:|---|
107
- | No proof or malformed/invalid settlement | `402` | Returns a fresh payment requirement. |
108
- | Settlement already consumed | `409` | Does not invite a second payment. |
109
- | RPC, mandate lookup, or redemption store unavailable | `503` | Fails closed with `Retry-After`; retry the same proof. |
110
- | Verified and atomically consumed | next handler | Stores chain-derived evidence in `response.locals.reappPayment`. |
117
+ | Condition | Status |
118
+ |---|---:|
119
+ | Missing/wrong bound-v2 capability | `426` before payment |
120
+ | Method other than GET | `405` |
121
+ | Missing, malformed, expired-first-use, mismatched, or unverified proof | `402` |
122
+ | Same settlement with a different proof | `409` |
123
+ | Existing execution or infrastructure/store outage | `503`, retry exact proof |
124
+ | New completed fulfillment | stored 2xx JSON |
125
+ | Exact completed recovery | byte-identical stored 2xx JSON |
111
126
 
112
- Challenges and successful responses receive `Cache-Control: private, no-store`
113
- and `Vary: X-PAYMENT` so a shared cache cannot bypass the payment boundary.
127
+ All responses are private/no-store. Proof and stored result material are
128
+ sensitive and must not be logged or exposed.
114
129
 
115
- ## Request-specific prices
130
+ ## Primary API
116
131
 
117
- `amount` and `resource` may be resolver functions. The middleware snapshots the
118
- resolved requirement before any asynchronous verification:
132
+ ### `createBoundReappPaidJsonRoute(options, fulfill)`
119
133
 
120
- ```ts
121
- const requirePayment = createReappPaymentMiddleware({
122
- merchant: MERCHANT,
123
- sourceAccount: READ_SOURCE,
124
- amount: (request) => priceFor(request.params.id),
125
- resource: (request) => `/source/${request.params.id}`,
126
- redemptionStore,
127
- });
128
- ```
134
+ Required options:
129
135
 
130
- Amounts are decimal strings and are converted to integer stroops. Floating-point
131
- amounts, scientific notation, over-precision, zero, and values outside i128 fail
132
- closed.
133
-
134
- ## API
135
-
136
- ### `createReappPaymentMiddleware(options)`
137
-
138
- Creates an Express 4/5 `RequestHandler`.
139
-
140
- | Option | Type | Meaning |
141
- |---|---|---|
142
- | `merchant` | `string` | Required Stellar address that must receive the transfer. |
143
- | `amount` | `string \| (request) => string` | Required decimal price. |
144
- | `redemptionStore` | `RedemptionStore` | Required atomic consume-once store. |
145
- | `sourceAccount` | `string?` | Funded `G...` address for read-only simulations; required by the default verifier. |
146
- | `resource` | `string \| (request) => string` | Challenge resource id; defaults to `request.originalUrl`. |
147
- | `asset` | `string?` | Required SEP-41 emitter; defaults to the configured network's native SAC. |
148
- | `networkConfig` | `NetworkConfig?` | RPC, passphrase, registry, and native SAC; defaults to REAPP testnet. |
149
- | `scheme` | `string?` | Wire scheme; defaults to `reapp-soroban`. |
150
- | `network` | `string?` | Wire network label; defaults to `stellar-testnet`. |
151
- | `decimals` | `number?` | Asset decimals; defaults to `7`. |
152
- | `maxProofAgeLedgers` | `number?` | Settlement freshness window; defaults to `120`. |
153
- | `pollAttempts` | `number?` | `NOT_FOUND` retries; defaults to `15`. |
154
- | `pollIntervalMs` | `number?` | Retry delay; defaults to `1000`. |
155
- | `maxHeaderBytes` | `number?` | Strict `X-PAYMENT` size limit; defaults to `8192`. |
156
- | `allowHttpRpc` | `boolean?` | Development-only plaintext RPC escape hatch; defaults to `false`. |
157
- | `verifier` | `PaymentVerifier?` | Explicit trusted verifier injection for tests or alternate RPC infrastructure. |
158
-
159
- ### Runtime exports
160
-
161
- | Export | Purpose |
136
+ | Option | Meaning |
162
137
  |---|---|
163
- | `createReappPaymentMiddleware` | Build the payment boundary. |
164
- | `getVerifiedPayment(response)` | Read the chain-derived `VerifiedPayment` after the gate. |
165
- | `InMemoryRedemptionStore` | One-process development/test store. |
166
- | `createStellarPaymentVerifier` | Build the independent Stellar verifier. |
167
- | `buildChallenge` | Build the isolated x402-style response object. |
168
- | `createRedemptionKey` | Create the normalized cross-network replay key. |
169
- | `extractContractEvents`, `interpretEvents` | Strict V3/V4 Stellar event decoding helpers. |
170
- | `selectPayment`, `selectTransfer` | Pure fail-closed event selection helpers. |
171
-
172
- The package also exports TypeScript types for requirements, verified payments,
173
- verifiers, redemption records and stores, middleware options, and decoded event
174
- evidence.
138
+ | `merchant` | Stellar address that must receive the verified transfer. |
139
+ | `amount` | Decimal price or request-specific resolver. |
140
+ | `audience` | Exact configured public HTTP(S) origin or safe resolver. |
141
+ | `challengeSecret` | Stable private 32–4096 byte challenge key. |
142
+ | `redemptionStore` | Atomic claim/result store shared by all serving workers. |
175
143
 
176
- ## Wire-format isolation
144
+ Optional controls include `resource`, `asset`, `networkConfig`, `network`,
145
+ `decimals`, `sourceAccount`, verifier/polling/freshness/header limits,
146
+ `challengeTtlSeconds`, development-only HTTP RPC, and `maxResponseBytes`.
147
+
148
+ Runtime exports include `createBoundReappPaidJsonRoute`,
149
+ `resolveBoundReappInterruptedDelivery`, `InMemoryBoundRedemptionStore`,
150
+ `createStellarPaymentVerifier`, strict event
151
+ selection helpers, and all TypeScript store/evidence/result types.
177
152
 
178
- x402 is evolving. REAPP keeps the HTTP challenge/proof shape in the isolated
179
- adapter inside `@reapp-sdk/core`; MandateRegistry does not depend on that shape.
180
- An x402 version change can replace the adapter while the same contract methods,
181
- storage, and verifier invariants remain intact.
153
+ Legacy proof-v1 middleware remains available only through the legacy API. The
154
+ low-level bound authorization middleware is intentionally not exported from the
155
+ package root; public paid endpoints use the result-storing route wrapper.
182
156
 
183
- ## Current protocol limits
157
+ ## Wire-format isolation
184
158
 
185
- - The settlement proof is bearer data. Atomic consumption prevents two
186
- successful redemptions, but a copied proof can race the legitimate requester.
187
- - The challenge resource is not committed into the current contract event.
188
- Exact requester/resource binding requires an authenticated nonce/signature or
189
- a future contract/event commitment; this package does not claim that property.
190
- - `get_mandate` reads current contract state, not a historical state snapshot.
191
- Same-transaction registry and token events prove settlement, while current
192
- state binds the mandate identities and asset.
159
+ x402 and AP2 evolve outside the MandateRegistry. HTTP/profile adapters may
160
+ change without changing contract storage or weakening `execute_payment`.
193
161
 
194
162
  ## Current contract evidence
195
163
 
196
- The testnet default is the upgradeable simple MandateRegistry:
197
-
198
- - Contract: [`CC6JMPDHRPBR2HBLJKRCIKV54HXDV2RFXDKW6MALQKWM6JEAJQHICRWE`](https://stellar.expert/explorer/testnet/contract/CC6JMPDHRPBR2HBLJKRCIKV54HXDV2RFXDKW6MALQKWM6JEAJQHICRWE)
164
+ - Testnet contract: [`CC6JMPDH…CRWE`](https://stellar.expert/explorer/testnet/contract/CC6JMPDHRPBR2HBLJKRCIKV54HXDV2RFXDKW6MALQKWM6JEAJQHICRWE)
199
165
  - WASM SHA-256: `13f7023d4a361b6e49d3d39f61f55c5eeece51a602013a3cddae420d2ce8552b`
200
- - Reproducible release: [`simple-v0.2.0`](https://github.com/reapp-protocol/reapp-protocol-contracts/releases/tag/simple-v0.2.0_contracts_simple_mandate_registry_mandate-registry_pkg0.2.0_cli25.1.0)
201
-
202
- The contract checks and consumes authorization inside `execute_payment` before
203
- the token transfer. Testnet operations include pause, authority rotation, and a
204
- 24-hour timelocked same-address upgrade path.
166
+ - Release: [`simple-v0.2.0`](https://github.com/reapp-protocol/reapp-protocol-contracts/releases/tag/simple-v0.2.0_contracts_simple_mandate_registry_mandate-registry_pkg0.2.0_cli25.1.0)
205
167
 
206
168
  Apache-2.0.
@@ -0,0 +1,36 @@
1
+ import type { Request, RequestHandler } from "express";
2
+ import { type BoundReappPaymentMiddlewareOptions } from "./bound.js";
3
+ import type { BoundDeliveryRecord, BoundRedemptionStore } from "./bound-store.js";
4
+ import type { VerifiedPayment } from "./types.js";
5
+ export interface BoundJsonResult {
6
+ /** Successful paid responses only. Defaults to 200. */
7
+ status?: number;
8
+ body: unknown;
9
+ }
10
+ export interface BoundJsonFulfillmentContext {
11
+ request: Request;
12
+ payment: Readonly<VerifiedPayment>;
13
+ }
14
+ export type BoundJsonFulfillment = (context: BoundJsonFulfillmentContext) => BoundJsonResult | Promise<BoundJsonResult>;
15
+ export interface BoundReappPaidJsonRouteOptions extends BoundReappPaymentMiddlewareOptions {
16
+ /** Maximum stored UTF-8 JSON response size. Defaults to 1 MiB. */
17
+ maxResponseBytes?: number;
18
+ }
19
+ /**
20
+ * Resolve an orphaned at-most-once execution without invoking fulfillment
21
+ * again. Call this only from trusted operator/outbox code after confirming the
22
+ * original execution owner is dead. The exact terminal bytes become immutable
23
+ * and subsequent receipt recovery replays them.
24
+ */
25
+ export declare function resolveBoundReappInterruptedDelivery(options: {
26
+ redemptionStore: BoundRedemptionStore;
27
+ record: Readonly<BoundDeliveryRecord>;
28
+ maxResponseBytes?: number;
29
+ }): Promise<Readonly<BoundDeliveryRecord>>;
30
+ /**
31
+ * Safe paid JSON route. Fulfillment executes once after an atomic claim; its
32
+ * exact bytes are stored before they are sent. Recovery replays those bytes and
33
+ * never invokes the fulfillment callback again.
34
+ */
35
+ export declare function createBoundReappPaidJsonRoute(options: BoundReappPaidJsonRouteOptions, fulfill: BoundJsonFulfillment): RequestHandler;
36
+ //# sourceMappingURL=bound-route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bound-route.d.ts","sourceRoot":"","sources":["../src/bound-route.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAgB,OAAO,EAAE,cAAc,EAAY,MAAM,SAAS,CAAC;AAC/E,OAAO,EAGL,KAAK,kCAAkC,EACxC,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EACV,mBAAmB,EACnB,oBAAoB,EAErB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,WAAW,eAAe;IAC9B,uDAAuD;IACvD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;CACpC;AAED,MAAM,MAAM,oBAAoB,GAAG,CACjC,OAAO,EAAE,2BAA2B,KACjC,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;AAEhD,MAAM,WAAW,8BAA+B,SAAQ,kCAAkC;IACxF,kEAAkE;IAClE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AA6BD;;;;;GAKG;AACH,wBAAsB,oCAAoC,CAAC,OAAO,EAAE;IAClE,eAAe,EAAE,oBAAoB,CAAC;IACtC,MAAM,EAAE,QAAQ,CAAC,mBAAmB,CAAC,CAAC;IACtC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,GAAG,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAC,CAkBzC;AAgDD;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,8BAA8B,EACvC,OAAO,EAAE,oBAAoB,GAC5B,cAAc,CAuDhB"}
@@ -0,0 +1,150 @@
1
+ import { createHash } from "node:crypto";
2
+ import { Buffer } from "buffer";
3
+ import { createBoundReappPaymentMiddleware, getBoundDeliveryContext, } from "./bound.js";
4
+ const CONTENT_TYPE = "application/json; charset=utf-8";
5
+ const DEFAULT_MAX_RESPONSE_BYTES = 1_048_576;
6
+ function storedResponse(status, body, maxBytes) {
7
+ if (!Number.isInteger(status) || status < 200 || status > 299) {
8
+ throw new Error("paid JSON fulfillment status must be an integer from 200 through 299");
9
+ }
10
+ const json = JSON.stringify(body);
11
+ if (json === undefined)
12
+ throw new Error("paid JSON fulfillment body must be JSON-serializable");
13
+ const bytes = Buffer.from(json, "utf8");
14
+ if (bytes.length > maxBytes)
15
+ throw new Error("paid JSON fulfillment body exceeds maxResponseBytes");
16
+ return Object.freeze({
17
+ status,
18
+ contentType: CONTENT_TYPE,
19
+ bodyBase64: bytes.toString("base64"),
20
+ bodySha256: createHash("sha256").update(bytes).digest("hex"),
21
+ });
22
+ }
23
+ function terminalFailure(maxBytes) {
24
+ return storedResponse(200, {
25
+ ok: false,
26
+ error: "paid fulfillment failed after settlement",
27
+ deliveryState: "terminal",
28
+ }, maxBytes);
29
+ }
30
+ /**
31
+ * Resolve an orphaned at-most-once execution without invoking fulfillment
32
+ * again. Call this only from trusted operator/outbox code after confirming the
33
+ * original execution owner is dead. The exact terminal bytes become immutable
34
+ * and subsequent receipt recovery replays them.
35
+ */
36
+ export async function resolveBoundReappInterruptedDelivery(options) {
37
+ if (options.record.state !== "executing") {
38
+ throw new Error("only an executing paid delivery can be resolved as interrupted");
39
+ }
40
+ const maxBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
41
+ if (!Number.isInteger(maxBytes) || maxBytes < 1 || maxBytes > 16_777_216) {
42
+ throw new Error("maxResponseBytes must be an integer from 1 through 16777216");
43
+ }
44
+ const completed = await options.redemptionStore.complete({
45
+ key: options.record.key,
46
+ proofDigest: options.record.proofDigest,
47
+ executionId: options.record.executionId,
48
+ response: terminalFailure(maxBytes),
49
+ });
50
+ if (completed.kind !== "completed" || completed.record.state !== "completed") {
51
+ throw new Error("interrupted paid delivery could not be resolved atomically");
52
+ }
53
+ return completed.record;
54
+ }
55
+ function decodeStoredResponse(stored, maxBytes) {
56
+ if (!Number.isInteger(stored.status)
57
+ || stored.status < 200
58
+ || stored.status > 299
59
+ || stored.contentType !== CONTENT_TYPE
60
+ || !/^[0-9a-f]{64}$/.test(stored.bodySha256)
61
+ || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(stored.bodyBase64)) {
62
+ throw new Error("stored paid response schema is invalid");
63
+ }
64
+ const bytes = Buffer.from(stored.bodyBase64, "base64");
65
+ if (bytes.length > maxBytes
66
+ || bytes.toString("base64") !== stored.bodyBase64
67
+ || createHash("sha256").update(bytes).digest("hex") !== stored.bodySha256) {
68
+ throw new Error("stored paid response integrity check failed");
69
+ }
70
+ return bytes;
71
+ }
72
+ function sendStored(response, stored, maxBytes) {
73
+ const bytes = decodeStoredResponse(stored, maxBytes);
74
+ response.status(stored.status);
75
+ response.set("content-type", stored.contentType);
76
+ response.set("content-length", String(bytes.length));
77
+ response.set("cache-control", "private, no-store");
78
+ response.set("x-content-type-options", "nosniff");
79
+ response.end(bytes);
80
+ }
81
+ function unavailable(response, message) {
82
+ response.status(503);
83
+ response.set("retry-after", "1");
84
+ response.set("cache-control", "private, no-store");
85
+ response.json({ error: message, retryable: true });
86
+ }
87
+ /**
88
+ * Safe paid JSON route. Fulfillment executes once after an atomic claim; its
89
+ * exact bytes are stored before they are sent. Recovery replays those bytes and
90
+ * never invokes the fulfillment callback again.
91
+ */
92
+ export function createBoundReappPaidJsonRoute(options, fulfill) {
93
+ if (typeof fulfill !== "function")
94
+ throw new Error("paid JSON fulfillment callback is required");
95
+ const maxBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
96
+ if (!Number.isInteger(maxBytes) || maxBytes < 1 || maxBytes > 16_777_216) {
97
+ throw new Error("maxResponseBytes must be an integer from 1 through 16777216");
98
+ }
99
+ const authorize = createBoundReappPaymentMiddleware(options);
100
+ return (request, response, next) => {
101
+ authorize(request, response, (authorizationError) => {
102
+ if (authorizationError) {
103
+ next(authorizationError);
104
+ return;
105
+ }
106
+ void (async () => {
107
+ const context = getBoundDeliveryContext(response);
108
+ if (!context)
109
+ throw new Error("bound delivery context was unavailable");
110
+ if (context.kind === "completed") {
111
+ if (context.record.state !== "completed")
112
+ throw new Error("completed delivery record is inconsistent");
113
+ sendStored(response, context.record.response, maxBytes);
114
+ return;
115
+ }
116
+ if (context.record.state !== "executing") {
117
+ throw new Error("claimed delivery record is inconsistent");
118
+ }
119
+ let result;
120
+ try {
121
+ const provided = await fulfill({ request, payment: context.record.payment });
122
+ result = storedResponse(provided.status ?? 200, provided.body, maxBytes);
123
+ }
124
+ catch {
125
+ // Store one immutable terminal result. Never re-run arbitrary paid work.
126
+ result = terminalFailure(maxBytes);
127
+ }
128
+ let completed;
129
+ try {
130
+ completed = await options.redemptionStore.complete({
131
+ key: context.record.key,
132
+ proofDigest: context.record.proofDigest,
133
+ executionId: context.record.executionId,
134
+ response: result,
135
+ });
136
+ }
137
+ catch {
138
+ unavailable(response, "paid fulfillment result store is unavailable; retry the same proof");
139
+ return;
140
+ }
141
+ if (completed.kind !== "completed" || completed.record.state !== "completed") {
142
+ unavailable(response, "paid fulfillment result could not be committed");
143
+ return;
144
+ }
145
+ sendStored(response, completed.record.response, maxBytes);
146
+ })().catch(next);
147
+ });
148
+ };
149
+ }
150
+ //# sourceMappingURL=bound-route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bound-route.js","sourceRoot":"","sources":["../src/bound-route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EACL,iCAAiC,EACjC,uBAAuB,GAExB,MAAM,YAAY,CAAC;AA4BpB,MAAM,YAAY,GAAG,iCAA0C,CAAC;AAChE,MAAM,0BAA0B,GAAG,SAAS,CAAC;AAE7C,SAAS,cAAc,CAAC,MAAc,EAAE,IAAa,EAAE,QAAgB;IACrE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,GAAG,IAAI,MAAM,GAAG,GAAG,EAAE,CAAC;QAC9D,MAAM,IAAI,KAAK,CAAC,sEAAsE,CAAC,CAAC;IAC1F,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IAClC,IAAI,IAAI,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;IAChG,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACxC,IAAI,KAAK,CAAC,MAAM,GAAG,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;IACpG,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,MAAM;QACN,WAAW,EAAE,YAAY;QACzB,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC;QACpC,UAAU,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;KAC7D,CAAC,CAAC;AACL,CAAC;AAED,SAAS,eAAe,CAAC,QAAgB;IACvC,OAAO,cAAc,CAAC,GAAG,EAAE;QACzB,EAAE,EAAE,KAAK;QACT,KAAK,EAAE,0CAA0C;QACjD,aAAa,EAAE,UAAU;KAC1B,EAAE,QAAQ,CAAC,CAAC;AACf,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,oCAAoC,CAAC,OAI1D;IACC,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,KAAK,WAAW,EAAE,CAAC;QACzC,MAAM,IAAI,KAAK,CAAC,gEAAgE,CAAC,CAAC;IACpF,CAAC;IACD,MAAM,QAAQ,GAAG,OAAO,CAAC,gBAAgB,IAAI,0BAA0B,CAAC;IACxE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,QAAQ,GAAG,CAAC,IAAI,QAAQ,GAAG,UAAU,EAAE,CAAC;QACzE,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;IACjF,CAAC;IACD,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,eAAe,CAAC,QAAQ,CAAC;QACvD,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG;QACvB,WAAW,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW;QACvC,WAAW,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW;QACvC,QAAQ,EAAE,eAAe,CAAC,QAAQ,CAAC;KACpC,CAAC,CAAC;IACH,IAAI,SAAS,CAAC,IAAI,KAAK,WAAW,IAAI,SAAS,CAAC,MAAM,CAAC,KAAK,KAAK,WAAW,EAAE,CAAC;QAC7E,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;IAChF,CAAC;IACD,OAAO,SAAS,CAAC,MAAM,CAAC;AAC1B,CAAC;AAED,SAAS,oBAAoB,CAC3B,MAAyC,EACzC,QAAgB;IAEhB,IACE,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC;WAC7B,MAAM,CAAC,MAAM,GAAG,GAAG;WACnB,MAAM,CAAC,MAAM,GAAG,GAAG;WACnB,MAAM,CAAC,WAAW,KAAK,YAAY;WACnC,CAAC,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC;WACzC,CAAC,kEAAkE,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,EAC9F,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;IAC5D,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IACvD,IACE,KAAK,CAAC,MAAM,GAAG,QAAQ;WACpB,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,MAAM,CAAC,UAAU;WAC9C,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,MAAM,CAAC,UAAU,EACzE,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,UAAU,CACjB,QAAkB,EAClB,MAAyC,EACzC,QAAgB;IAEhB,MAAM,KAAK,GAAG,oBAAoB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACrD,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC/B,QAAQ,CAAC,GAAG,CAAC,cAAc,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IACjD,QAAQ,CAAC,GAAG,CAAC,gBAAgB,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;IACrD,QAAQ,CAAC,GAAG,CAAC,eAAe,EAAE,mBAAmB,CAAC,CAAC;IACnD,QAAQ,CAAC,GAAG,CAAC,wBAAwB,EAAE,SAAS,CAAC,CAAC;IAClD,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AACtB,CAAC;AAED,SAAS,WAAW,CAAC,QAAkB,EAAE,OAAe;IACtD,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACrB,QAAQ,CAAC,GAAG,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;IACjC,QAAQ,CAAC,GAAG,CAAC,eAAe,EAAE,mBAAmB,CAAC,CAAC;IACnD,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AACrD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,6BAA6B,CAC3C,OAAuC,EACvC,OAA6B;IAE7B,IAAI,OAAO,OAAO,KAAK,UAAU;QAAE,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;IACjG,MAAM,QAAQ,GAAG,OAAO,CAAC,gBAAgB,IAAI,0BAA0B,CAAC;IACxE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,QAAQ,GAAG,CAAC,IAAI,QAAQ,GAAG,UAAU,EAAE,CAAC;QACzE,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;IACjF,CAAC;IACD,MAAM,SAAS,GAAG,iCAAiC,CAAC,OAAO,CAAC,CAAC;IAE7D,OAAO,CAAC,OAAgB,EAAE,QAAkB,EAAE,IAAkB,EAAQ,EAAE;QACxE,SAAS,CAAC,OAAO,EAAE,QAAQ,EAAE,CAAC,kBAA4B,EAAE,EAAE;YAC5D,IAAI,kBAAkB,EAAE,CAAC;gBACvB,IAAI,CAAC,kBAAkB,CAAC,CAAC;gBACzB,OAAO;YACT,CAAC;YACD,KAAK,CAAC,KAAK,IAAI,EAAE;gBACf,MAAM,OAAO,GAAG,uBAAuB,CAAC,QAAQ,CAAC,CAAC;gBAClD,IAAI,CAAC,OAAO;oBAAE,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;gBACxE,IAAI,OAAO,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;oBACjC,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,KAAK,WAAW;wBAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;oBACvG,UAAU,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;oBACxD,OAAO;gBACT,CAAC;gBACD,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,KAAK,WAAW,EAAE,CAAC;oBACzC,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;gBAC7D,CAAC;gBAED,IAAI,MAA+B,CAAC;gBACpC,IAAI,CAAC;oBACH,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;oBAC7E,MAAM,GAAG,cAAc,CAAC,QAAQ,CAAC,MAAM,IAAI,GAAG,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;gBAC3E,CAAC;gBAAC,MAAM,CAAC;oBACP,yEAAyE;oBACzE,MAAM,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;gBACrC,CAAC;gBAED,IAAI,SAAS,CAAC;gBACd,IAAI,CAAC;oBACH,SAAS,GAAG,MAAM,OAAO,CAAC,eAAe,CAAC,QAAQ,CAAC;wBACjD,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG;wBACvB,WAAW,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW;wBACvC,WAAW,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW;wBACvC,QAAQ,EAAE,MAAM;qBACjB,CAAC,CAAC;gBACL,CAAC;gBAAC,MAAM,CAAC;oBACP,WAAW,CAAC,QAAQ,EAAE,oEAAoE,CAAC,CAAC;oBAC5F,OAAO;gBACT,CAAC;gBACD,IAAI,SAAS,CAAC,IAAI,KAAK,WAAW,IAAI,SAAS,CAAC,MAAM,CAAC,KAAK,KAAK,WAAW,EAAE,CAAC;oBAC7E,WAAW,CAAC,QAAQ,EAAE,gDAAgD,CAAC,CAAC;oBACxE,OAAO;gBACT,CAAC;gBACD,UAAU,CAAC,QAAQ,EAAE,SAAS,CAAC,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;YAC5D,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,79 @@
1
+ import type { VerifiedPayment } from "./types.js";
2
+ export interface BoundRedemptionRecord {
3
+ /** Network-passphrase hash, registry id, and normalized transaction hash. */
4
+ key: string;
5
+ /** SHA-256 of the strict decoded bound proof. */
6
+ proofDigest: string;
7
+ /** Chain-derived evidence captured when this exact proof was first accepted. */
8
+ payment: Readonly<VerifiedPayment>;
9
+ }
10
+ export interface StoredBoundJsonResponse {
11
+ status: number;
12
+ contentType: "application/json; charset=utf-8";
13
+ bodyBase64: string;
14
+ bodySha256: string;
15
+ }
16
+ export type BoundDeliveryRecord = Readonly<BoundRedemptionRecord> & Readonly<{
17
+ executionId: string;
18
+ startedAt: number;
19
+ }> & ({
20
+ state: "executing";
21
+ response?: never;
22
+ } | {
23
+ state: "completed";
24
+ response: Readonly<StoredBoundJsonResponse>;
25
+ });
26
+ export type BoundRedemptionLookup = {
27
+ kind: "missing";
28
+ } | {
29
+ kind: "executing";
30
+ record: Readonly<BoundDeliveryRecord>;
31
+ } | {
32
+ kind: "completed";
33
+ record: Readonly<BoundDeliveryRecord>;
34
+ } | {
35
+ kind: "conflict";
36
+ };
37
+ export type BoundRedemptionClaim = {
38
+ kind: "claimed";
39
+ record: Readonly<BoundDeliveryRecord>;
40
+ } | {
41
+ kind: "executing";
42
+ record: Readonly<BoundDeliveryRecord>;
43
+ } | {
44
+ kind: "completed";
45
+ record: Readonly<BoundDeliveryRecord>;
46
+ } | {
47
+ kind: "conflict";
48
+ };
49
+ export interface BoundRedemptionCompletion {
50
+ key: string;
51
+ proofDigest: string;
52
+ executionId: string;
53
+ response: Readonly<StoredBoundJsonResponse>;
54
+ }
55
+ export type BoundRedemptionComplete = {
56
+ kind: "completed";
57
+ record: Readonly<BoundDeliveryRecord>;
58
+ } | {
59
+ kind: "conflict";
60
+ };
61
+ /**
62
+ * One linearizable store owns both settlement binding and immutable delivery
63
+ * bytes. A proof is claimed at most once; recovery either waits for that claim
64
+ * or replays its completed result without re-running fulfillment.
65
+ */
66
+ export interface BoundRedemptionStore {
67
+ lookup(key: string, proofDigest: string): BoundRedemptionLookup | Promise<BoundRedemptionLookup>;
68
+ claim(record: Readonly<BoundRedemptionRecord>, executionId: string, startedAt: number): BoundRedemptionClaim | Promise<BoundRedemptionClaim>;
69
+ complete(completion: Readonly<BoundRedemptionCompletion>): BoundRedemptionComplete | Promise<BoundRedemptionComplete>;
70
+ }
71
+ /** Process-local reference store. Production and restart drills need a durable shared store. */
72
+ export declare class InMemoryBoundRedemptionStore implements BoundRedemptionStore {
73
+ private readonly records;
74
+ lookup(key: string, proofDigest: string): BoundRedemptionLookup;
75
+ claim(record: Readonly<BoundRedemptionRecord>, executionId: string, startedAt: number): BoundRedemptionClaim;
76
+ complete(completion: Readonly<BoundRedemptionCompletion>): BoundRedemptionComplete;
77
+ get(key: string): Readonly<BoundDeliveryRecord> | undefined;
78
+ }
79
+ //# sourceMappingURL=bound-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bound-store.d.ts","sourceRoot":"","sources":["../src/bound-store.ts"],"names":[],"mappings":"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"}