@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.
- package/.github/workflows/test.yaml +49 -0
- package/CHANGELOG.md +34 -0
- package/README.md +24 -281
- package/package.json +8 -2
- package/srv/cds-augment.d.ts +17 -0
- package/srv/client/axios.d.ts +38 -0
- package/srv/client/axios.js +68 -0
- package/srv/client/envelope.d.ts +33 -0
- package/srv/client/envelope.js +52 -0
- package/srv/client/fetch.d.ts +30 -0
- package/srv/client/fetch.js +76 -0
- package/srv/client/pay-handlers.d.ts +41 -0
- package/srv/client/pay-handlers.js +47 -0
- package/srv/client/types.d.ts +44 -0
- package/srv/client/types.js +10 -0
- package/srv/facilitator/adapter.d.ts +69 -0
- package/srv/facilitator/adapter.js +52 -0
- package/srv/facilitator/http.d.ts +43 -0
- package/srv/facilitator/http.js +99 -0
- package/srv/index.d.ts +7 -0
- package/srv/index.js +15 -1
- package/srv/middleware/cap.d.ts +7 -0
- package/srv/middleware/cap.js +3 -2
- package/srv/middleware/express.d.ts +8 -0
- package/srv/middleware/express.js +3 -2
|
@@ -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
|
|
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
|
|
26
|
-
|
|
27
|
-
---
|
|
15
|
+
`@odatano/core` (the Cardano bridge) is a peer dependency. Install whichever version meets `>=1.7.8`.
|
|
28
16
|
|
|
29
|
-
## Quick Start
|
|
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',
|
|
29
|
+
asset: 'lovelace', // or '<policy>.<nameHex>' for native tokens
|
|
42
30
|
routePricing: {
|
|
43
|
-
Quotes:
|
|
44
|
-
getBestPrice:
|
|
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
|
-
|
|
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
|
-
|
|
58
|
+
## What's in the box
|
|
121
59
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
|
183
|
-
|
|
184
|
-
| `
|
|
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)
|
|
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
|
|
318
|
-
npm run build # tsc
|
|
319
|
-
npm test #
|
|
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.
|
|
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/
|
|
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
|