@odatano/x402 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.
@@ -0,0 +1,49 @@
1
+ name: tests
2
+
3
+ # Run on every push to main and every pull request. Manual dispatch
4
+ # lets us re-run the workflow from the Actions tab without a commit.
5
+ on:
6
+ push:
7
+ branches: [main]
8
+ pull_request:
9
+ workflow_dispatch:
10
+
11
+ jobs:
12
+ test:
13
+ name: Node ${{ matrix.node }} on ubuntu-latest
14
+ runs-on: ubuntu-latest
15
+
16
+ strategy:
17
+ # One Node failing should not cancel the other matrix legs;
18
+ # often a regression hits one version only.
19
+ fail-fast: false
20
+ matrix:
21
+ # Matches ODATANO core's support range.
22
+ node: ['20.x', '22.x']
23
+
24
+ steps:
25
+ - name: Checkout
26
+ uses: actions/checkout@v4
27
+
28
+ - name: Setup Node ${{ matrix.node }}
29
+ uses: actions/setup-node@v4
30
+ with:
31
+ node-version: ${{ matrix.node }}
32
+ cache: 'npm'
33
+
34
+ - name: Install dependencies
35
+ # `npm ci` enforces the lockfile and refuses to mutate it.
36
+ # Faster and more deterministic than `npm install` for CI.
37
+ run: npm ci
38
+
39
+ - name: Lint
40
+ run: npm run lint
41
+
42
+ - name: Type-check + build
43
+ # tsc -p tsconfig.build.json emits .js/.d.ts next to .ts.
44
+ # We don't ship those emits from CI; the step just verifies
45
+ # the build passes against the declared types.
46
+ run: npm run build
47
+
48
+ - name: Run tests
49
+ run: npm test
package/CHANGELOG.md ADDED
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@odatano/x402` are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/) and the project adheres to [Semantic Versioning](https://semver.org/).
4
+
5
+ **Pre-1.0 caveat:** minor versions may include breaking changes until `1.0.0`.
6
+
7
+ ## [0.2.0] - 2026-05-15
8
+
9
+ ### Added
10
+ - **Client helpers**: `x402Fetch`, `x402Axios`, `createBridgePayHandler`, `encodePaymentEnvelope` for symmetric server + client usage. See [`docs/usage.md`](docs/usage.md#5-client-side-auto-handle-402-x402fetch--x402axios).
11
+ - **Facilitator adapter pattern**: `Facilitator` interface, `localFacilitator()` (default, in-process via `@odatano/core`), `httpFacilitator()` for delegating verify+settle to a hosted service. HTTP wire format documented in [`docs/facilitator-protocol.md`](docs/facilitator-protocol.md).
12
+ - **`facilitator` option** on `gateService` and `x402Middleware` for swapping in the local default, an HTTP delegate, or a mock for tests.
13
+ - **GitHub Actions CI** (`.github/workflows/test.yaml`): runs lint + build + tests on Node 20.x and 22.x for every push to `main` and every pull request.
14
+
15
+ ### Changed
16
+ - Test count: 144 → 177 (added 4 client suites and 2 facilitator-adapter suites).
17
+
18
+ ### Notes
19
+ - `0.1.0` consumers upgrade without code changes. The new `facilitator` option defaults to the previous in-process behaviour, so existing call sites are untouched.
20
+
21
+ ## [0.1.0] - 2026-05-13
22
+
23
+ ### Added
24
+ - Initial release. Cardano-x402-v2 payment gating for SAP CAP and Express.
25
+ - `gateService(srv, opts)` for CAP `before('*')` integration.
26
+ - `x402Middleware(opts)` for plain Express routes.
27
+ - Facilitator pipeline: decode, validate (six mandatory checks), `checkNonceUnspent`, `settle` (submit + poll-until-confirmed), `onAccepted` audit callback.
28
+ - Helpers: `verifyConfirmedPayment` (post-paid flow), `buildUnsignedPaymentTx` (browser-buyer flow).
29
+ - CAP plugin auto-discovery via `cds-plugin.js`.
30
+ - 144 unit tests across 13 suites.
31
+
32
+ ### Spec compatibility
33
+ - Implements Cardano-x402-**v2** only. v1 envelopes are rejected with `unsupported_version`; v1-style network strings (`cardano-preprod` with hyphen) are rejected with `invalid_network_format`.
34
+ - v1 and v2 facilitators cannot share a route: they use different header names (`X-PAYMENT` vs `PAYMENT-SIGNATURE`) and incompatible 402 bodies. To migrate from v1, replace the middleware in one commit; clients must upgrade simultaneously.
package/README.md CHANGED
@@ -2,31 +2,19 @@
2
2
 
3
3
  x402 payment gating for SAP CAP applications, backed by Cardano.
4
4
 
5
- Wire a single `before('*')` hook into your CAP service and every gated request returns **HTTP 402 Payment Required** until the caller proves on-chain settlement. Asset-agnostic pay in ADA, USDM, or any native asset.
5
+ Wire a single `before('*')` hook into your CAP service. Every gated request returns **HTTP 402 Payment Required** until the caller proves on-chain settlement. Asset-agnostic: pay in ADA, USDM, or any native asset.
6
6
 
7
7
  Implements the **Cardano-x402-v2** spec on top of [`@odatano/core`](https://www.npmjs.com/package/@odatano/core).
8
8
 
9
- ---
10
-
11
- ## What is x402?
12
-
13
- [x402](https://www.x402.org/) is the dormant HTTP `402 Payment Required` status code, revived. Servers respond `402` with a machine-readable body describing the price, asset, and recipient. Clients build, sign, and submit a payment, then retry the request with a `PAYMENT-SIGNATURE` header. Settlement happens on-chain.
14
-
15
- The original x402 spec is Coinbase / EVM-flavoured. The **Cardano-x402-v2** spec (in progress at [masumi-network/x402-cardano](https://github.com/masumi-network/x402-cardano)) adapts it to Cardano's UTxO model. This library is a from-scratch v2 implementation in TypeScript.
16
-
17
- ---
18
-
19
9
  ## Install
20
10
 
21
11
  ```bash
22
12
  npm install @odatano/x402 @odatano/core
23
13
  ```
24
14
 
25
- `@odatano/core` (the Cardano bridge) is a peer dependency install whichever version meets `>=1.7.8`.
26
-
27
- ---
15
+ `@odatano/core` (the Cardano bridge) is a peer dependency. Install whichever version meets `>=1.7.8`.
28
16
 
29
- ## Quick Start — CAP service gate
17
+ ## Quick Start
30
18
 
31
19
  ```typescript
32
20
  // srv/prices-service.ts
@@ -38,16 +26,10 @@ export class PricesService extends cds.ApplicationService {
38
26
  gateService(this, {
39
27
  payTo: 'addr_test1...your-preprod-address...',
40
28
  network: 'cardano:preprod',
41
- asset: 'lovelace', // or '<policy>.<nameHex>' for native tokens
29
+ asset: 'lovelace', // or '<policy>.<nameHex>' for native tokens
42
30
  routePricing: {
43
- Quotes: '500000', // 0.5 ADA per Quotes read
44
- getBestPrice: '1000000', // 1 ADA per getBestPrice action call
45
- },
46
- description: 'Synthetic price feed',
47
- onAccepted: async (claim, req) => {
48
- // Optional audit — runs once per accepted payment, after settlement.
49
- // Errors here are logged but don't block the response.
50
- console.log(`paid ${claim.amountUnits} ${claim.asset} (tx=${claim.txHash})`);
31
+ Quotes: '500000', // 0.5 ADA per Quotes read
32
+ getBestPrice: '1000000', // 1 ADA per getBestPrice action call
51
33
  },
52
34
  });
53
35
  return super.init();
@@ -71,280 +53,41 @@ Configure the Cardano backend in `package.json`:
71
53
  }
72
54
  ```
73
55
 
74
- That's it `cds watch` and every request to a gated route gets:
75
-
76
- ```http
77
- HTTP/1.1 402 Payment Required
78
- Content-Type: application/json
79
- ```
80
- ```json
81
- {
82
- "x402Version": 2,
83
- "error": "PAYMENT-SIGNATURE header is required",
84
- "accepts": [{
85
- "scheme": "exact",
86
- "network": "cardano:preprod",
87
- "asset": "lovelace",
88
- "amount": "500000",
89
- "payTo": "addr_test1...",
90
- "resource": {
91
- "url": "/odata/v4/prices/Quotes",
92
- "description": "Synthetic price feed",
93
- "mimeType": "application/json"
94
- },
95
- "assetTransferMethod": "default",
96
- "maxTimeoutSeconds": 600
97
- }]
98
- }
99
- ```
100
-
101
- A working version of this is in [`examples/cap-app/`](examples/cap-app/).
102
-
103
- ---
104
-
105
- ## Usage patterns
106
-
107
- ### 1. CAP service gate (`gateService`)
108
-
109
- For OData-served entities and bound/unbound actions. Pricing keys can be entity names (for CRUD) or action names — the gate tries both.
110
-
111
- ```typescript
112
- gateService(this, {
113
- payTo, network, asset,
114
- routePricing: { Quotes: '500000', getBestPrice: '1000000' },
115
- });
116
- ```
117
-
118
- When a payment is accepted, the verified `PaymentClaim` is stashed on `req.payment` for downstream handlers, and an `X-PAYMENT-RESPONSE` header is set on the response.
56
+ `cds watch`, then probe a gated route: it returns `402` with a v2-shape body. A working example lives in [`examples/cap-app/`](examples/cap-app/).
119
57
 
120
- ### 2. Express middleware (`x402Middleware`)
58
+ ## What's in the box
121
59
 
122
- For plain Express routes (e.g. mounted alongside CAP via `cds.on('bootstrap', app => …)`):
123
-
124
- ```typescript
125
- import { x402Middleware } from '@odatano/x402';
126
-
127
- app.use('/api/premium', x402Middleware({
128
- payTo, network, asset,
129
- priceUnits: '1000000',
130
- skipPaths: /(\$metadata|^\/?$)/i,
131
- onAccepted: async (claim, req) => { /* audit */ },
132
- }));
133
- ```
134
-
135
- ### 3. Programmatic — verify a post-paid tx
136
-
137
- For subscription / pre-paid flows where the buyer hands you a tx hash:
138
-
139
- ```typescript
140
- import { verifyConfirmedPayment } from '@odatano/x402';
141
-
142
- const result = await verifyConfirmedPayment({
143
- txHash: 'ab8f…',
144
- requiredAmount: '1000000',
145
- asset: 'lovelace',
146
- payTo: 'addr_test1...',
147
- network: 'cardano:preprod',
148
- });
149
-
150
- if (result.ok) {
151
- // result.amountUnits is what was actually paid (may exceed requiredAmount)
152
- } else {
153
- // result.code: 'pending' | 'wrong_asset' | 'insufficient_amount' | ...
154
- }
155
- ```
156
-
157
- ### 4. Programmatic — server-side unsigned-tx builder for browser buyers
158
-
159
- When the buyer's CIP-30 wallet can sign but not coin-select:
160
-
161
- ```typescript
162
- import { buildUnsignedPaymentTx, buildPaymentRequirements, flatRequirements } from '@odatano/x402';
163
-
164
- const body = buildPaymentRequirements({ amount: '1000000', asset: 'lovelace', payTo, network: 'cardano:preprod', resource: '/r' });
165
- const requirements = flatRequirements(body);
166
-
167
- const { unsignedTxCborHex, txHashHex, nonceRef } = await buildUnsignedPaymentTx({
168
- buyerBech32: 'addr_test1...buyer...',
169
- requirements,
170
- });
171
-
172
- // Browser signs unsignedTxCborHex via CIP-30, then assembles the
173
- // PAYMENT-SIGNATURE envelope with `nonceRef` as payload.nonce.
174
- ```
60
+ - **`gateService(srv, opts)`** for CAP services and **`x402Middleware(opts)`** for plain Express routes.
61
+ - **`x402Fetch` / `x402Axios`** wrappers that auto-handle 402 on the client side.
62
+ - **`Facilitator` adapter:** `localFacilitator()` (default, in-process via `@odatano/core`) or `httpFacilitator()` to delegate verify+settle to a hosted service.
63
+ - **Helpers:** `buildUnsignedPaymentTx` (browser-buyer flow), `verifyConfirmedPayment` (post-paid / subscription).
175
64
 
176
- ---
65
+ ## Documentation
177
66
 
178
- ## Configuration reference
179
-
180
- ### `gateService(srv, options)` / `x402Middleware(options)`
181
-
182
- | Option | Type | Required | Default | Notes |
183
- |---|---|---|---|---|
184
- | `payTo` | `string` (bech32) | yes | | Recipient address |
185
- | `network` | `'cardano:mainnet' \| 'cardano:preprod' \| 'cardano:preview'` | yes | — | **v2 uses colon separator** (v1 hyphen rejected) |
186
- | `asset` | `string` | yes | — | `'lovelace'` for ADA, or `'<policyIdHex>.<assetNameHex>'` for native tokens |
187
- | `priceUnits` | `string \| number \| bigint` | one of priceUnits / routePricing | — | Single price for everything under the mount |
188
- | `routePricing` | `Record<string, string \| number \| bigint>` | one of priceUnits / routePricing | — | Per-entity / per-action prices. Unmapped keys pass through unless `priceUnits` is also set |
189
- | `skipPaths` | `RegExp` | no | matches `$metadata`, `$batch`, root, `/index` | Express only — paths to bypass |
190
- | `description` | `string` | no | `''` | Embedded in `accepts[0].resource.description` |
191
- | `mimeType` | `string` | no | `'application/json'` | Embedded in `accepts[0].resource.mimeType` |
192
- | `assetTransferMethod` | `'default' \| 'masumi' \| 'script'` | no | `'default'` | v2 field; MVP supports only `default` |
193
- | `maxTimeoutSeconds` | `number` | no | `600` | Buyer-side TTL hint |
194
- | `extra` | `Record<string, unknown>` | no | — | Free-form extras (decimals, fingerprint, UI hints) |
195
- | `settlePollBudgetMs` | `number` | no | `60_000` | How long to poll for chain confirmation before returning `402 pending` |
196
- | `allowNoTtl` | `boolean` | no | `false` | If `true`, accept txs with no validity-range upper bound |
197
- | `onAccepted` | `(claim, req) => void \| Promise<void>` | no | — | Audit callback. Errors logged, never block response |
198
- | `resourceUrl` | `(req) => string` | no (CAP only) | derives from `req.http.req.originalUrl` | Override the resource URL emitted in the 402 body |
199
-
200
- ---
201
-
202
- ## The buyer flow
203
-
204
- ```
205
- Server Buyer (browser / CLI)
206
- │ │
207
- │ ◄── GET /odata/v4/prices/Quotes ─────│
208
- │ │
209
- │ ──── 402 + accepts[0] (price, payTo) ►│
210
- │ │
211
- │ ┌────────┴────────┐
212
- │ │ build tx │
213
- │ │ sign via CIP-30 │
214
- │ │ base64-encode │
215
- │ └────────┬────────┘
216
- │ │
217
- │ ◄── GET /odata/v4/prices/Quotes ─────│
218
- │ PAYMENT-SIGNATURE: <base64> │
219
- │ │
220
- ┌──────┴──────┐ │
221
- │ 6 checks │ │
222
- │ + submit │ │
223
- │ + poll │ │
224
- │ + onAccepted│ │
225
- └──────┬──────┘ │
226
- │ │
227
- │ ─────── 200 OK + data ───────────────►│
228
- │ X-PAYMENT-RESPONSE: <base64> │
229
- ```
230
-
231
- `PAYMENT-SIGNATURE` envelope shape:
232
-
233
- ```json
234
- {
235
- "x402Version": 2,
236
- "scheme": "exact",
237
- "network": "cardano:preprod",
238
- "payload": {
239
- "transaction": "<base64-CBOR of signed tx>",
240
- "nonce": "<txHash>#<outputIndex>"
241
- }
242
- }
243
- ```
244
-
245
- The `nonce` references a UTxO that **must also appear as an input of the payment tx**. Once the tx settles, that UTxO is consumed — replay defense is on-chain, no DB table needed.
246
-
247
- ---
248
-
249
- ## Six mandatory facilitator checks
250
-
251
- Every accepted payment passes all six (in order):
252
-
253
- | # | Check | Code on failure |
254
- |---|---|---|
255
- | 1 | Network matches requirements | `network_mismatch` |
256
- | 2 | At least one output to `payTo` | `wrong_recipient` |
257
- | 3 | Sum of payTo outputs for asset ≥ required | `insufficient_amount` |
258
- | 4 | Exact policy + asset-name match | `wrong_asset` |
259
- | 5 | Nonce UTxO referenced as tx input **and** unspent on chain | `nonce_not_referenced` / `replay_detected` |
260
- | 6 | Validity-range upper bound still in future | `expired_ttl` |
261
-
262
- Plus a sanity guard: tx has at least one vkey witness → `unsigned_transaction`.
263
-
264
- Rejected requests get `402` with an `error` field of the form `"<base> (<code>): <reason>"` so clients can parse the code without breaking wire format.
265
-
266
- ---
267
-
268
- ## Architecture
269
-
270
- ```
271
- ┌──────────────────┐
272
- consumer │ CAP application │
273
- │ │
274
- │ gateService(this,│
275
- │ { … }) │
276
- └────────┬─────────┘
277
-
278
-
279
- ┌──────────────────┐ bridge.ts
280
- │ @odatano/x402 │ ──────────────┐
281
- │ │ │
282
- │ core/ ──────┤ pure logic │
283
- │ facilitator/ ────┤ chain-touching│
284
- │ middleware/ ────┤ Express + CAP │
285
- │ helpers/ ────┤ tx-build, │
286
- │ │ verify-post-paid
287
- └──────────────────┘ │
288
-
289
- ┌────────────────────────┐
290
- │ @odatano/core 1.7.8 │
291
- │ ┌──────┬──────┬─────┐ │
292
- │ │ Blkf │Koios │Ogms │ │
293
- │ └──────┴──────┴─────┘ │
294
- └────────────────────────┘
295
-
296
-
297
- Cardano network
298
- ```
299
-
300
- Pure modules (`core/*`) are decoupled from the bridge and can be unit-tested without any chain backend. The facilitator orchestrates `decode → validate → checkNonceUnspent → settle → onAccepted`.
301
-
302
- ---
67
+ | Doc | Covers |
68
+ |---|---|
69
+ | [`docs/usage.md`](docs/usage.md) | All five usage patterns + full configuration reference |
70
+ | [`docs/protocol.md`](docs/protocol.md) | Buyer-flow diagram, `PAYMENT-SIGNATURE` envelope, the six mandatory facilitator checks |
71
+ | [`docs/architecture.md`](docs/architecture.md) | Module layout, pure-vs-chain split, plugin auto-discovery |
72
+ | [`docs/facilitator-protocol.md`](docs/facilitator-protocol.md) | HTTP wire format for the hosted-facilitator pattern (`httpFacilitator()`) |
73
+ | [`CHANGELOG.md`](CHANGELOG.md) | Versioned changes, latest first |
303
74
 
304
75
  ## Requirements
305
76
 
306
77
  - Node.js 22+
307
78
  - `@sap/cds >= 9` (peer)
308
79
  - `@odatano/core >= 1.7.8` (peer)
309
- - `express ^4` (peer) only required if you use `x402Middleware`
80
+ - `express ^4` (peer), only if you use `x402Middleware`
310
81
  - A Cardano backend reachable via `@odatano/core` (Blockfrost / Koios / Ogmios)
311
82
 
312
- ---
313
-
314
83
  ## Development
315
84
 
316
85
  ```bash
317
- npm install # Workspace install covers root + examples/*
318
- npm run build # tsc emits .js/.d.ts next to .ts (outDir: .)
319
- npm test # 144 tests, ~12s
320
- npm run test:coverage # Coverage report
321
- npm run watch # cds watch — for the root project itself
322
- ```
323
-
324
- The example app:
325
-
326
- ```bash
327
- cd examples/cap-app
328
- npm start # cds-serve with in-memory SQLite
329
- ```
330
-
331
- Then probe:
332
-
333
- ```bash
334
- curl -s http://localhost:4004/odata/v4/prices/Quotes | jq .
335
- # → 402 with v2 body
86
+ npm install # Workspace install: covers root + examples/*
87
+ npm run build # tsc, emits .js/.d.ts next to .ts (outDir: .)
88
+ npm test # 177 tests, ~13s
336
89
  ```
337
90
 
338
- ---
339
-
340
- ## Versioning + spec compatibility
341
-
342
- - **Library:** `0.1.0` (pre-1.0; expect minor breakages between minors until 1.0)
343
- - **Spec:** Cardano-x402-**v2** only. v1 envelopes are rejected with `unsupported_version`. v1-style network strings (`cardano-preprod`) are rejected with `invalid_network_format`.
344
- - **Coexistence:** A v1 facilitator and a v2 facilitator can't share the same route — they use different header names (`X-PAYMENT` vs `PAYMENT-SIGNATURE`) and incompatible 402 bodies. To migrate from v1, replace the middleware in one commit; clients must upgrade simultaneously.
345
-
346
- ---
347
-
348
91
  ## License
349
92
 
350
93
  Apache-2.0.
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@odatano/x402",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "x402 Cardano-v2 payment library for SAP CAP applications",
5
5
  "license": "Apache-2.0",
6
6
  "main": "srv/index.js",
7
- "types": "srv/index.d.ts",
7
+ "types": "srv/cds-augment.d.ts",
8
8
  "publishConfig": {
9
9
  "access": "public"
10
10
  },
@@ -26,10 +26,16 @@
26
26
  "@emurgo/cardano-serialization-lib-nodejs": "^15.0.3"
27
27
  },
28
28
  "peerDependencies": {
29
+ "@cap-js/cds-types": ">=0.15",
29
30
  "@odatano/core": ">=1.7.8",
30
31
  "@sap/cds": ">=9",
31
32
  "express": "^4"
32
33
  },
34
+ "peerDependenciesMeta": {
35
+ "@cap-js/cds-types": {
36
+ "optional": true
37
+ }
38
+ },
33
39
  "devDependencies": {
34
40
  "@cap-js/cds-types": "^0.15.0",
35
41
  "@cap-js/cds-typer": "^0.38.0",
@@ -0,0 +1,17 @@
1
+ /// <reference types="@cap-js/cds-types" />
2
+ /**
3
+ * Public types entry — hand-written wrapper around the generated barrel.
4
+ *
5
+ * Why this file exists: the triple-slash reference below is the only
6
+ * way to ship `@cap-js/cds-types`' module-augmentation of `@sap/cds`
7
+ * to TypeScript consumers without forcing them to mutate their own
8
+ * tsconfig. References authored inside `.ts` sources get stripped by
9
+ * tsc during `.d.ts` emit — references in hand-written `.d.ts` files
10
+ * are preserved verbatim, so we use a hand-written one as the
11
+ * advertised `types` entry in package.json.
12
+ *
13
+ * Consumers should still install `@cap-js/cds-types` (declared as an
14
+ * optional peer dependency) — this file only routes the augmentation
15
+ * once that package is resolvable.
16
+ */
17
+ export * from './index';
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `x402Axios` — attach a response interceptor to an existing axios
3
+ * instance so 402 responses trigger a payment and retry.
4
+ *
5
+ * **No hard axios dependency.** We use structural typing for the
6
+ * instance: anything with the standard axios shape (interceptors,
7
+ * request, defaults.headers) works. Verified against axios 1.x.
8
+ *
9
+ * Usage:
10
+ * import axios from 'axios';
11
+ * import { x402Axios, createBridgePayHandler } from '@odatano/x402';
12
+ *
13
+ * const client = x402Axios(axios.create({ baseURL: '...' }), {
14
+ * pay: createBridgePayHandler({ buyerBech32, signTx }),
15
+ * });
16
+ * await client.get('/api/premium/foo'); // returns 200 after pay
17
+ */
18
+ import type { X402ClientOptions } from './types';
19
+ interface AxiosRequestConfigLike {
20
+ headers?: Record<string, unknown>;
21
+ [k: string]: unknown;
22
+ }
23
+ interface AxiosInstanceLike {
24
+ interceptors: {
25
+ response: {
26
+ use: (onFulfilled: (res: unknown) => unknown, onRejected: (err: unknown) => unknown) => number;
27
+ };
28
+ };
29
+ request: (cfg: AxiosRequestConfigLike) => Promise<unknown>;
30
+ }
31
+ /**
32
+ * Attach the x402 response interceptor in-place and return the same
33
+ * instance for chaining. The interceptor only fires on 402 responses;
34
+ * everything else passes through unchanged.
35
+ */
36
+ export declare function x402Axios<T extends AxiosInstanceLike>(instance: T, opts: X402ClientOptions): T;
37
+ export {};
38
+ //# sourceMappingURL=axios.d.ts.map
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ /**
3
+ * `x402Axios` — attach a response interceptor to an existing axios
4
+ * instance so 402 responses trigger a payment and retry.
5
+ *
6
+ * **No hard axios dependency.** We use structural typing for the
7
+ * instance: anything with the standard axios shape (interceptors,
8
+ * request, defaults.headers) works. Verified against axios 1.x.
9
+ *
10
+ * Usage:
11
+ * import axios from 'axios';
12
+ * import { x402Axios, createBridgePayHandler } from '@odatano/x402';
13
+ *
14
+ * const client = x402Axios(axios.create({ baseURL: '...' }), {
15
+ * pay: createBridgePayHandler({ buyerBech32, signTx }),
16
+ * });
17
+ * await client.get('/api/premium/foo'); // returns 200 after pay
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.x402Axios = x402Axios;
21
+ const envelope_1 = require("./envelope");
22
+ // Marker key on the config to break infinite-retry loops.
23
+ const RETRY_KEY = '__x402_x402Retries';
24
+ function isAxiosError(e) {
25
+ return !!e && typeof e === 'object' && 'response' in e;
26
+ }
27
+ /**
28
+ * Attach the x402 response interceptor in-place and return the same
29
+ * instance for chaining. The interceptor only fires on 402 responses;
30
+ * everything else passes through unchanged.
31
+ */
32
+ function x402Axios(instance, opts) {
33
+ if (typeof opts?.pay !== 'function') {
34
+ throw new TypeError('x402Axios: opts.pay must be a function');
35
+ }
36
+ const maxRetries = opts.maxRetries ?? 1;
37
+ const selectFirst = (a) => a[0];
38
+ const select = opts.selectAccepts ?? selectFirst;
39
+ instance.interceptors.response.use((response) => response, async (error) => {
40
+ if (!isAxiosError(error) || error.response?.status !== 402 || !error.config) {
41
+ throw error;
42
+ }
43
+ const cfg = error.config;
44
+ const retries = Number(cfg[RETRY_KEY] ?? 0);
45
+ if (retries >= maxRetries)
46
+ throw error;
47
+ const body = error.response.data;
48
+ if (body?.x402Version !== 2 || !Array.isArray(body.accepts) || body.accepts.length === 0) {
49
+ throw error;
50
+ }
51
+ const chosen = select(body.accepts);
52
+ if (!chosen)
53
+ throw error;
54
+ const { signedTxCborHex, nonceRef } = await opts.pay(chosen);
55
+ const header = (0, envelope_1.encodePaymentEnvelope)({
56
+ network: chosen.network,
57
+ signedTxCborHex,
58
+ nonceRef,
59
+ });
60
+ const nextCfg = {
61
+ ...cfg,
62
+ headers: { ...(cfg.headers ?? {}), 'PAYMENT-SIGNATURE': header },
63
+ [RETRY_KEY]: retries + 1,
64
+ };
65
+ return instance.request(nextCfg);
66
+ });
67
+ return instance;
68
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Build the `PAYMENT-SIGNATURE` header value for a Cardano-x402-v2 retry.
3
+ *
4
+ * Inverse of `srv/core/decode.ts`. The wire format is:
5
+ *
6
+ * PAYMENT-SIGNATURE: base64(JSON.stringify({
7
+ * x402Version: 2,
8
+ * scheme: 'exact',
9
+ * network: 'cardano:preprod' | 'cardano:mainnet' | 'cardano:preview',
10
+ * payload: {
11
+ * transaction: '<base64 CBOR of signed tx>',
12
+ * nonce: '<txHash>#<outputIndex>'
13
+ * }
14
+ * }))
15
+ *
16
+ * Pure function — no chain calls, no I/O. Callable from any runtime
17
+ * that has `Buffer` (Node) or a polyfill (browser bundlers usually
18
+ * provide one via `buffer`).
19
+ */
20
+ import type { Network } from '../core/network';
21
+ export interface EncodeEnvelopeArgs {
22
+ network: Network;
23
+ /** Hex of the SIGNED payment tx (vkey witnesses already attached). */
24
+ signedTxCborHex: string;
25
+ /** `<txHash>#<outputIndex>` UTxO-ref nonce. */
26
+ nonceRef: string;
27
+ }
28
+ /**
29
+ * Encode the v2 PAYMENT-SIGNATURE envelope. Validates shape eagerly so
30
+ * a malformed call fails here, not on the server's `decode()`.
31
+ */
32
+ export declare function encodePaymentEnvelope(args: EncodeEnvelopeArgs): string;
33
+ //# sourceMappingURL=envelope.d.ts.map