@odatano/x402 0.2.0 → 0.3.1
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 +35 -0
- package/README.md +5 -0
- package/cds-plugin.js +2 -0
- package/db/x402-grants.cds +49 -0
- package/db/x402-receipts.cds +44 -0
- package/package.json +4 -3
- package/srv/bridge.d.ts +9 -12
- package/srv/bridge.js +10 -13
- package/srv/client/axios.d.ts +1 -1
- package/srv/client/axios.js +45 -8
- package/srv/client/envelope.d.ts +1 -1
- package/srv/client/envelope.js +1 -1
- package/srv/client/errors.d.ts +86 -0
- package/srv/client/errors.js +107 -0
- package/srv/client/fetch.d.ts +2 -2
- package/srv/client/fetch.js +71 -11
- package/srv/client/pay-handlers.d.ts +4 -4
- package/srv/client/pay-handlers.js +3 -3
- package/srv/client/types.d.ts +19 -7
- package/srv/client/types.js +3 -3
- package/srv/core/asset.d.ts +1 -1
- package/srv/core/decode.d.ts +2 -2
- package/srv/core/decode.js +5 -5
- package/srv/core/errors.js +3 -3
- package/srv/core/network.d.ts +1 -1
- package/srv/core/network.js +1 -1
- package/srv/core/requirements.d.ts +37 -5
- package/srv/core/requirements.js +43 -4
- package/srv/core/types.d.ts +68 -6
- package/srv/core/types.js +3 -3
- package/srv/core/validate.d.ts +31 -7
- package/srv/core/validate.js +84 -9
- package/srv/facilitator/adapter.d.ts +8 -8
- package/srv/facilitator/adapter.js +5 -5
- package/srv/facilitator/http.d.ts +4 -4
- package/srv/facilitator/http.js +5 -5
- package/srv/facilitator/nonce.d.ts +4 -4
- package/srv/facilitator/nonce.js +4 -4
- package/srv/facilitator/server.d.ts +68 -0
- package/srv/facilitator/server.js +167 -0
- package/srv/facilitator/settle.d.ts +2 -2
- package/srv/facilitator/settle.js +4 -4
- package/srv/facilitator/verify.d.ts +5 -5
- package/srv/facilitator/verify.js +19 -5
- package/srv/helpers/build-unsigned-tx.d.ts +5 -5
- package/srv/helpers/build-unsigned-tx.js +3 -3
- package/srv/helpers/verify-confirmed.d.ts +1 -1
- package/srv/helpers/verify-confirmed.js +1 -1
- package/srv/index.d.ts +4 -2
- package/srv/index.js +9 -3
- package/srv/middleware/cap.d.ts +47 -9
- package/srv/middleware/cap.js +111 -43
- package/srv/middleware/express.d.ts +15 -10
- package/srv/middleware/express.js +18 -19
- package/srv/middleware/grants.d.ts +64 -0
- package/srv/middleware/grants.js +113 -0
- package/srv/middleware/pricing.d.ts +41 -0
- package/srv/middleware/pricing.js +78 -0
- package/srv/middleware/receipts.d.ts +38 -0
- package/srv/middleware/receipts.js +68 -0
- package/srv/plugin.d.ts +2 -2
- package/srv/plugin.js +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,41 @@ All notable changes to `@odatano/x402` are documented here. The format follows [
|
|
|
4
4
|
|
|
5
5
|
**Pre-1.0 caveat:** minor versions may include breaking changes until `1.0.0`.
|
|
6
6
|
|
|
7
|
+
## [0.3.1] - 2026-05-15
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **`gateService` now emits the canonical x402 v2 body on the wire** instead of CAP's OData-wrapped shape. The gate writes `httpRes.status(402).json(body)` directly when an Express response is reachable, then calls `req.reject(402, ...)` as the chain-terminator: the synchronous throw stops CAP's handler pipeline so the gated `on` handler never runs, and CAP's render attempt no-ops on `headersSent`. Non-HTTP transports (event invocations, `$batch` reuse) fall back to plain `req.reject`. Validated against `@sap/cds ^9`. Third-party x402 clients now interop with CAP-gated services without any unwrap shim. Closes the "Known issues" item from 0.3.0.
|
|
11
|
+
|
|
12
|
+
### Removed (breaking)
|
|
13
|
+
- **`unwrapCapEnvelope`** helper and its calls from `x402Fetch` / `x402Axios`. With the server fix above, the v2 body lands at the top level on the wire and the defensive unwrap is dead code. The export is gone; consumers who pulled it in (e.g. for wrapping the wrappers) should remove the import. **Pair the upgrade**: if you upgrade the `@odatano/x402` client to 0.3.1, upgrade the server in the same step, since 0.3.1 clients no longer unwrap a 0.3.0-style wrapped body.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- `srv/middleware/cap.ts` , new `send402` helper and `getHttpRes` accessor; the one 402 emit site routes through `send402`. The two 500 `req.reject` paths (pricing-resolver throw, facilitator throw) are unchanged.
|
|
17
|
+
- Test suite: 232 tests across 21 suites. CAP middleware tests gained 3 cases (canonical-wire-shape regression, `headersSent` defensive fallback, no-`http.res` transport fallback). Client tests dropped 7 cases tied to `unwrapCapEnvelope`.
|
|
18
|
+
|
|
19
|
+
## [0.3.0] - 2026-05-15
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **`createFacilitatorRouter()`** , reference HTTP facilitator. Returns an Express `Router` exposing `POST /verify-settle`, `GET /supported`, and an open `GET /healthz` liveness probe. Composable with any auth scheme via the `auth(req)` hook; defaults to `localFacilitator()`. Facilitator-side audit hooks (`onRejected`, `onPending`) fill the gap left by `onAccepted` (which is invoked client-side by `httpFacilitator()`). See [`examples/facilitator-server/`](examples/facilitator-server/) and [`docs/facilitator-protocol.md`](docs/facilitator-protocol.md#reference-implementation).
|
|
23
|
+
- **Multi-accept payment options** , `routePricing` (and `priceUnits`) now accept `RouteOption[]` so a single route can offer e.g. "0.5 ADA *or* 0.1 USDM". The buyer picks one implicitly by which `(payTo, asset)` the payment tx credits; new `pickRequirement()` selector in `srv/core/validate.ts` routes the tx to the matching entry before the six strict checks run. Single-entry behaviour is bit-identical to v0.2. New builder: `buildPaymentRequirementsMulti()`.
|
|
24
|
+
- **Dynamic `PriceResolver`** , `routePricing` can be a function `(PricingContext) => PriceSpec | null | Promise<...>`. Returning `null` passes the request through ungated, enabling free-tier, role-based, or per-payload pricing. `PricingContext` exposes `event`, `target` (CAP), `path`/`method`/`query` (Express), and `headers`. See [`docs/usage.md`](docs/usage.md#pricespec-and-priceresolver).
|
|
25
|
+
- **Receipts persistence (CAP)** , new `receipts?: boolean | { entity?: string }` option on `gateService`. When set, one INSERT per accepted payment, post-settle, pre-response. Default entity `odatano.x402.X402Receipts` ships in `db/x402-receipts.cds` and is auto-discovered by CAP. INSERT failures are logged and never block the response. See [`docs/usage.md`](docs/usage.md#receipts-persistence-receipts).
|
|
26
|
+
- **Subscription / time-limited grants (CAP)** , new `grants?: boolean | { ttlSeconds?: number; entity?: string }` option on `gateService`. On accepted payment the gate issues an opaque token and returns it via `X-PAYMENT-GRANT` / `X-PAYMENT-GRANT-EXPIRES` response headers; subsequent requests presenting the token on `X-PAYMENT-GRANT` bypass the 402 + verify+settle pipeline until expiry. Default TTL 3600s. Grants are single-route (strict URL equality). Default entity `odatano.x402.X402Grants` ships in `db/x402-grants.cds`. DB failures during issue or lookup are swallowed: failing DB never denies a paying buyer their response. See [`docs/usage.md`](docs/usage.md#subscription--time-limited-grants-grants).
|
|
27
|
+
- **Typed client errors** , new `X402PaymentError` class (with `kind`, `code`, `accepts`, `httpStatus`, `serverError`, `cause` fields). Thrown by `x402Fetch` and `x402Axios` to surface payment failures. Pay-handler errors are ALWAYS wrapped (with the original on `.cause`); add `errorOnFailure: true` to opt into typed throws on unrecovered 402s instead of the previous return-the-response / re-throw-AxiosError behaviour. Helpers `parseErrorCode` and `paymentErrorFromBody` are exported for consumers wrapping the wrappers. See [`docs/usage.md`](docs/usage.md#client-side-errors-x402paymenterror).
|
|
28
|
+
- **Browser-buyer example** , `examples/browser-buyer/` Vite scaffold showing CIP-30 wallet + `x402Fetch` wiring. Documents the typical "unsigned-from-server, signed-by-wallet" architecture (server exposes `POST /pay/intent` via `buildUnsignedPaymentTx`; browser signs via CIP-30). Includes CORS notes for cross-origin deployments.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- **`x402Fetch` / `x402Axios` now interop with `gateService`** out of the box. CAP's `req.reject(402, body)` wraps the canonical v2 body inside its standard OData error envelope (`{ error: { message: "<json>", code: "402", ... } }`), so previous client wrappers saw `body.x402Version === undefined` and bailed without retrying. Both clients now defensively unwrap the OData envelope before validating shape.
|
|
32
|
+
|
|
33
|
+
### Known issues
|
|
34
|
+
- The CAP `gateService` still emits 402 responses wrapped in CAP's OData error envelope on the wire (because `req.reject` is the only documented abort path). Third-party x402 clients hitting a CAP-gated server will see the wrapped shape; only `@odatano/x402`'s own clients unwrap it. Direct-write-to-`req.http.res` is the planned symmetric fix but needs validation against `@sap/cds` ^9 internals; tracked for v0.3.1.
|
|
35
|
+
- `PaymentClaim.payTo` , verified recipient address now populated on the claim (was previously only on the requirements entry). Useful for `onAccepted` audit and receipts.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
- Test count: 177 → 230 (HTTP-server round-trips, multi-accept + dynamic-pricing across `requirements`/`validate`/`verify`/`cap`/`express`, 4 receipts cases, 5 grants cases, 13 client-error cases).
|
|
39
|
+
- `srv/middleware/{cap,express}.ts` now emit `accepts[]` via `buildPaymentRequirementsMulti()`; single-entry callers are unaffected (one-entry array produces a body byte-identical to v0.2).
|
|
40
|
+
- `srv/facilitator/verify.ts` decodes the envelope BEFORE selecting a requirements entry; multi-accept depends on knowing which `(payTo, asset)` the tx actually credited.
|
|
41
|
+
|
|
7
42
|
## [0.2.0] - 2026-05-15
|
|
8
43
|
|
|
9
44
|
### Added
|
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# @odatano/x402
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@odatano/x402)
|
|
4
|
+
[](https://github.com/ODATANO/x402/actions/workflows/test.yaml)
|
|
5
|
+
[](https://www.npmjs.com/package/@odatano/core)
|
|
6
|
+
[](https://github.com/masumi-network/x402-cardano)
|
|
7
|
+
|
|
3
8
|
x402 payment gating for SAP CAP applications, backed by Cardano.
|
|
4
9
|
|
|
5
10
|
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.
|
package/cds-plugin.js
CHANGED
|
@@ -7,4 +7,6 @@
|
|
|
7
7
|
* (outDir: ".") so this require path resolves both in dev (tsx) and
|
|
8
8
|
* after `npm run build`.
|
|
9
9
|
*/
|
|
10
|
+
// CAP loads this file as a CommonJS entrypoint, so `require()` is required here.
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
10
12
|
module.exports = require('./srv/plugin');
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time-limited access grants issued after an accepted x402 payment.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: pay once, get N seconds of free access to the same route.
|
|
5
|
+
* The server issues an opaque random token on `accepted`, returns it via
|
|
6
|
+
* the `X-PAYMENT-GRANT` response header, and the buyer presents it on
|
|
7
|
+
* subsequent requests via `X-PAYMENT-GRANT` request header. Until the
|
|
8
|
+
* grant expires, the gate skips the 402 + verify+settle pipeline.
|
|
9
|
+
*
|
|
10
|
+
* Replay defense: each grant is single-route + time-bound. A grant
|
|
11
|
+
* issued for `/Quotes` cannot be used against `/getBestPrice` (cheap
|
|
12
|
+
* route boundary, the picker enforces equality). Stolen tokens are
|
|
13
|
+
* useful only until expiry.
|
|
14
|
+
*
|
|
15
|
+
* Cleanup: expired rows accumulate. The `lookupGrant` helper checks
|
|
16
|
+
* `expiresAt < now` and reports `expired`; consumers may run their own
|
|
17
|
+
* cleanup (`DELETE … WHERE expiresAt < now()`) on whatever schedule
|
|
18
|
+
* fits. The library does NOT auto-prune.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
namespace odatano.x402;
|
|
22
|
+
|
|
23
|
+
entity X402Grants {
|
|
24
|
+
key id : UUID;
|
|
25
|
+
|
|
26
|
+
@description: 'Opaque random token, base64url 32 bytes.'
|
|
27
|
+
token : String(64) @assert.unique;
|
|
28
|
+
|
|
29
|
+
@description: 'Resource URL this grant unlocks (exact match).'
|
|
30
|
+
route : String(500);
|
|
31
|
+
|
|
32
|
+
@description: 'Sender address from the original payment (if resolved).'
|
|
33
|
+
payerAddr : String(120);
|
|
34
|
+
|
|
35
|
+
@description: 'Tx hash of the payment that bought this grant.'
|
|
36
|
+
txHash : String(64);
|
|
37
|
+
|
|
38
|
+
@description: 'Asset of the underlying payment, audit only.'
|
|
39
|
+
asset : String(120);
|
|
40
|
+
|
|
41
|
+
@description: 'cardano:mainnet | cardano:preprod | cardano:preview.'
|
|
42
|
+
network : String(20);
|
|
43
|
+
|
|
44
|
+
@description: 'Server-side timestamp at issue.'
|
|
45
|
+
issuedAt : Timestamp;
|
|
46
|
+
|
|
47
|
+
@description: 'Server-side timestamp when the grant becomes invalid.'
|
|
48
|
+
expiresAt : Timestamp;
|
|
49
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional persistence for accepted x402 payments.
|
|
3
|
+
*
|
|
4
|
+
* Enabled per-service by setting `receipts: true` on `gateService(opts)`.
|
|
5
|
+
* The gate writes one row per accepted payment, after settle confirms,
|
|
6
|
+
* BEFORE the response is served. Insert failures are logged and never
|
|
7
|
+
* block the response, the canonical record is on chain regardless.
|
|
8
|
+
*
|
|
9
|
+
* Namespace and entity name are stable, consumers can SELECT against
|
|
10
|
+
* `odatano.x402.X402Receipts` directly or extend it in their own model.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
namespace odatano.x402;
|
|
14
|
+
|
|
15
|
+
entity X402Receipts {
|
|
16
|
+
key id : UUID;
|
|
17
|
+
|
|
18
|
+
@description: 'Lowercase 64-char hex of the buyer''s settled payment tx.'
|
|
19
|
+
txHash : String(64) @assert.unique;
|
|
20
|
+
|
|
21
|
+
@description: 'Sender address (first input bech32) if the facilitator resolved it.'
|
|
22
|
+
payerAddr : String(120);
|
|
23
|
+
|
|
24
|
+
@description: 'Recipient bech32 the route required.'
|
|
25
|
+
payTo : String(120);
|
|
26
|
+
|
|
27
|
+
@description: 'v2 asset string, ''lovelace'' or ''<policy>.<nameHex>''.'
|
|
28
|
+
asset : String(120);
|
|
29
|
+
|
|
30
|
+
@description: 'Amount paid in raw units, BigInt-safe string.'
|
|
31
|
+
amount : String(32);
|
|
32
|
+
|
|
33
|
+
@description: 'cardano:mainnet | cardano:preprod | cardano:preview.'
|
|
34
|
+
network : String(20);
|
|
35
|
+
|
|
36
|
+
@description: 'Resource URL the buyer paid for (request originalUrl or cap://<event>).'
|
|
37
|
+
route : String(500);
|
|
38
|
+
|
|
39
|
+
@description: '<txHash>#<index> of the UTxO that was the replay nonce.'
|
|
40
|
+
nonceRef : String(80);
|
|
41
|
+
|
|
42
|
+
@description: 'Server-side timestamp when the receipt was written (post-settle).'
|
|
43
|
+
at : Timestamp;
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@odatano/x402",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "x402 Cardano-v2 payment library for SAP CAP applications",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "srv/index.js",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@cap-js/cds-types": "^0.15.0",
|
|
41
40
|
"@cap-js/cds-typer": "^0.38.0",
|
|
41
|
+
"@cap-js/cds-types": "^0.15.0",
|
|
42
42
|
"@cap-js/sqlite": "^2",
|
|
43
43
|
"@odatano/core": "^1.7.8",
|
|
44
44
|
"@odatano/x402": ".",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"jest": "^29",
|
|
54
54
|
"ts-jest": "^29",
|
|
55
55
|
"tsx": "^4",
|
|
56
|
-
"typescript": "^5"
|
|
56
|
+
"typescript": "^5",
|
|
57
|
+
"typescript-eslint": "^8.59.3"
|
|
57
58
|
}
|
|
58
59
|
}
|
package/srv/bridge.d.ts
CHANGED
|
@@ -3,18 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The x402 modules (facilitator, helpers, middleware) all import from
|
|
5
5
|
* here so the underlying ODATANO surface is the only thing they couple
|
|
6
|
-
* to
|
|
6
|
+
* to, and so renames in core (`getTransaction` → `getTransactionByHash`)
|
|
7
7
|
* stay isolated to this file.
|
|
8
8
|
*
|
|
9
|
-
* Two methods
|
|
10
|
-
* `@odatano/core
|
|
11
|
-
* - `isUtxoUnspent(txHash, outputIndex)`
|
|
12
|
-
* - `getCurrentSlot()`
|
|
9
|
+
* Two methods specific to Cardano-x402-v2 are first-class on
|
|
10
|
+
* `@odatano/core` since `1.7.8` (our minimum peer):
|
|
11
|
+
* - `isUtxoUnspent(txHash, outputIndex)` for replay-defense check 5b
|
|
12
|
+
* - `getCurrentSlot()` for TTL check 6
|
|
13
13
|
*
|
|
14
|
-
* Both are
|
|
15
|
-
* methods, so x402 works against an unmodified 1.7.7. When ODATANO
|
|
16
|
-
* exposes either method natively (planned ≥1.7.8), we can swap the
|
|
17
|
-
* shim for a direct call without touching downstream code.
|
|
14
|
+
* Both are called through directly here; no shim layer remains.
|
|
18
15
|
*/
|
|
19
16
|
export interface BridgeAsset {
|
|
20
17
|
unit: string;
|
|
@@ -49,18 +46,18 @@ export declare function getProtocolParameters(): Promise<unknown>;
|
|
|
49
46
|
export declare function submitTransaction(signedCborHex: string): Promise<string>;
|
|
50
47
|
/**
|
|
51
48
|
* Current chain tip slot. First-class method on `CardanoClient` since
|
|
52
|
-
* `@odatano/core@1.7.8
|
|
49
|
+
* `@odatano/core@1.7.8`, wraps `getLatestBlock().slot` with a
|
|
53
50
|
* `ProviderUnavailableError` translation so consumers don't deal with
|
|
54
51
|
* `null` slots.
|
|
55
52
|
*/
|
|
56
53
|
export declare function getCurrentSlot(): Promise<number>;
|
|
57
54
|
/**
|
|
58
55
|
* Check whether a UTxO is still unspent. First-class method since
|
|
59
|
-
* `@odatano/core@1.7.8
|
|
56
|
+
* `@odatano/core@1.7.8`, backed by `consumed_by` (Blockfrost) /
|
|
60
57
|
* `is_spent` (Koios) / `queryLedgerState/utxo` (Ogmios).
|
|
61
58
|
*
|
|
62
59
|
* Returns `false` for txs that don't exist on chain or for
|
|
63
|
-
* out-of-range output indices
|
|
60
|
+
* out-of-range output indices, both are "not spendable" from the
|
|
64
61
|
* caller's perspective.
|
|
65
62
|
*/
|
|
66
63
|
export declare function isUtxoUnspent(txHash: string, outputIndex: number): Promise<boolean>;
|
package/srv/bridge.js
CHANGED
|
@@ -4,18 +4,15 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The x402 modules (facilitator, helpers, middleware) all import from
|
|
6
6
|
* here so the underlying ODATANO surface is the only thing they couple
|
|
7
|
-
* to
|
|
7
|
+
* to, and so renames in core (`getTransaction` → `getTransactionByHash`)
|
|
8
8
|
* stay isolated to this file.
|
|
9
9
|
*
|
|
10
|
-
* Two methods
|
|
11
|
-
* `@odatano/core
|
|
12
|
-
* - `isUtxoUnspent(txHash, outputIndex)`
|
|
13
|
-
* - `getCurrentSlot()`
|
|
10
|
+
* Two methods specific to Cardano-x402-v2 are first-class on
|
|
11
|
+
* `@odatano/core` since `1.7.8` (our minimum peer):
|
|
12
|
+
* - `isUtxoUnspent(txHash, outputIndex)` for replay-defense check 5b
|
|
13
|
+
* - `getCurrentSlot()` for TTL check 6
|
|
14
14
|
*
|
|
15
|
-
* Both are
|
|
16
|
-
* methods, so x402 works against an unmodified 1.7.7. When ODATANO
|
|
17
|
-
* exposes either method natively (planned ≥1.7.8), we can swap the
|
|
18
|
-
* shim for a direct call without touching downstream code.
|
|
15
|
+
* Both are called through directly here; no shim layer remains.
|
|
19
16
|
*/
|
|
20
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
18
|
exports.parseTransaction = void 0;
|
|
@@ -120,7 +117,7 @@ async function submitTransaction(signedCborHex) {
|
|
|
120
117
|
}
|
|
121
118
|
/**
|
|
122
119
|
* Current chain tip slot. First-class method on `CardanoClient` since
|
|
123
|
-
* `@odatano/core@1.7.8
|
|
120
|
+
* `@odatano/core@1.7.8`, wraps `getLatestBlock().slot` with a
|
|
124
121
|
* `ProviderUnavailableError` translation so consumers don't deal with
|
|
125
122
|
* `null` slots.
|
|
126
123
|
*/
|
|
@@ -130,11 +127,11 @@ async function getCurrentSlot() {
|
|
|
130
127
|
}
|
|
131
128
|
/**
|
|
132
129
|
* Check whether a UTxO is still unspent. First-class method since
|
|
133
|
-
* `@odatano/core@1.7.8
|
|
130
|
+
* `@odatano/core@1.7.8`, backed by `consumed_by` (Blockfrost) /
|
|
134
131
|
* `is_spent` (Koios) / `queryLedgerState/utxo` (Ogmios).
|
|
135
132
|
*
|
|
136
133
|
* Returns `false` for txs that don't exist on chain or for
|
|
137
|
-
* out-of-range output indices
|
|
134
|
+
* out-of-range output indices, both are "not spendable" from the
|
|
138
135
|
* caller's perspective.
|
|
139
136
|
*/
|
|
140
137
|
async function isUtxoUnspent(txHash, outputIndex) {
|
|
@@ -150,6 +147,6 @@ async function isUtxoUnspent(txHash, outputIndex) {
|
|
|
150
147
|
// parseTransaction is exported from @odatano/core's barrel and runs
|
|
151
148
|
// entirely client-side. Re-export so x402 users don't need a second
|
|
152
149
|
// import for tx introspection. We declare the type loosely (unknown
|
|
153
|
-
// CBOR-parsed shape)
|
|
150
|
+
// CBOR-parsed shape), consumers cast to ODATANO's `ParsedTransaction`
|
|
154
151
|
// from `@odatano/core` directly if they need the structured fields.
|
|
155
152
|
exports.parseTransaction = od ? od.parseTransaction : undefined;
|
package/srv/client/axios.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `x402Axios
|
|
2
|
+
* `x402Axios`, attach a response interceptor to an existing axios
|
|
3
3
|
* instance so 402 responses trigger a payment and retry.
|
|
4
4
|
*
|
|
5
5
|
* **No hard axios dependency.** We use structural typing for the
|
package/srv/client/axios.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* `x402Axios
|
|
3
|
+
* `x402Axios`, attach a response interceptor to an existing axios
|
|
4
4
|
* instance so 402 responses trigger a payment and retry.
|
|
5
5
|
*
|
|
6
6
|
* **No hard axios dependency.** We use structural typing for the
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
20
|
exports.x402Axios = x402Axios;
|
|
21
21
|
const envelope_1 = require("./envelope");
|
|
22
|
+
const errors_1 = require("./errors");
|
|
22
23
|
// Marker key on the config to break infinite-retry loops.
|
|
23
24
|
const RETRY_KEY = '__x402_x402Retries';
|
|
24
25
|
function isAxiosError(e) {
|
|
@@ -36,26 +37,62 @@ function x402Axios(instance, opts) {
|
|
|
36
37
|
const maxRetries = opts.maxRetries ?? 1;
|
|
37
38
|
const selectFirst = (a) => a[0];
|
|
38
39
|
const select = opts.selectAccepts ?? selectFirst;
|
|
40
|
+
function maybeWrap(body, kind, httpStatus, cause) {
|
|
41
|
+
if (body && body.x402Version === 2 && Array.isArray(body.accepts)) {
|
|
42
|
+
return (0, errors_1.paymentErrorFromBody)(body, { kind, httpStatus, cause });
|
|
43
|
+
}
|
|
44
|
+
return new errors_1.X402PaymentError({
|
|
45
|
+
message: `x402Axios: ${kind.replace('_', ' ')}`,
|
|
46
|
+
kind: 'invalid_402_body',
|
|
47
|
+
httpStatus,
|
|
48
|
+
...(cause !== undefined ? { cause } : {}),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
39
51
|
instance.interceptors.response.use((response) => response, async (error) => {
|
|
40
52
|
if (!isAxiosError(error) || error.response?.status !== 402 || !error.config) {
|
|
41
53
|
throw error;
|
|
42
54
|
}
|
|
43
55
|
const cfg = error.config;
|
|
44
56
|
const retries = Number(cfg[RETRY_KEY] ?? 0);
|
|
45
|
-
if (retries >= maxRetries)
|
|
46
|
-
throw error;
|
|
47
57
|
const body = error.response.data;
|
|
48
|
-
|
|
58
|
+
const status = error.response.status;
|
|
59
|
+
if (retries >= maxRetries) {
|
|
60
|
+
if (opts.errorOnFailure) {
|
|
61
|
+
throw maybeWrap(body, 'retries_exhausted', status, error);
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
if (!body || body.x402Version !== 2 || !Array.isArray(body.accepts) || body.accepts.length === 0) {
|
|
66
|
+
if (opts.errorOnFailure) {
|
|
67
|
+
throw maybeWrap(body, 'invalid_402_body', status, error);
|
|
68
|
+
}
|
|
49
69
|
throw error;
|
|
50
70
|
}
|
|
51
71
|
const chosen = select(body.accepts);
|
|
52
|
-
if (!chosen)
|
|
72
|
+
if (!chosen) {
|
|
73
|
+
if (opts.errorOnFailure) {
|
|
74
|
+
throw maybeWrap(body, 'server_rejected', status, error);
|
|
75
|
+
}
|
|
53
76
|
throw error;
|
|
54
|
-
|
|
77
|
+
}
|
|
78
|
+
// Wrap pay-handler throws unconditionally, callers benefit from
|
|
79
|
+
// the structured shape regardless of errorOnFailure.
|
|
80
|
+
let payResult;
|
|
81
|
+
try {
|
|
82
|
+
payResult = await opts.pay(chosen);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
throw new errors_1.X402PaymentError({
|
|
86
|
+
message: `x402Axios: pay handler failed: ${err?.message ?? String(err)}`,
|
|
87
|
+
kind: 'pay_handler_failed',
|
|
88
|
+
accepts: body.accepts,
|
|
89
|
+
cause: err,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
55
92
|
const header = (0, envelope_1.encodePaymentEnvelope)({
|
|
56
93
|
network: chosen.network,
|
|
57
|
-
signedTxCborHex,
|
|
58
|
-
nonceRef,
|
|
94
|
+
signedTxCborHex: payResult.signedTxCborHex,
|
|
95
|
+
nonceRef: payResult.nonceRef,
|
|
59
96
|
});
|
|
60
97
|
const nextCfg = {
|
|
61
98
|
...cfg,
|
package/srv/client/envelope.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* }
|
|
14
14
|
* }))
|
|
15
15
|
*
|
|
16
|
-
* Pure function
|
|
16
|
+
* Pure function, no chain calls, no I/O. Callable from any runtime
|
|
17
17
|
* that has `Buffer` (Node) or a polyfill (browser bundlers usually
|
|
18
18
|
* provide one via `buffer`).
|
|
19
19
|
*/
|
package/srv/client/envelope.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* }
|
|
15
15
|
* }))
|
|
16
16
|
*
|
|
17
|
-
* Pure function
|
|
17
|
+
* Pure function, no chain calls, no I/O. Callable from any runtime
|
|
18
18
|
* that has `Buffer` (Node) or a polyfill (browser bundlers usually
|
|
19
19
|
* provide one via `buffer`).
|
|
20
20
|
*/
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed client-side errors thrown by `x402Fetch` / `x402Axios`.
|
|
3
|
+
*
|
|
4
|
+
* The server-side `X402Error` (in `srv/core/errors.ts`) is a different
|
|
5
|
+
* class with a different purpose: it's thrown inside the decode +
|
|
6
|
+
* validate pipeline and carries one of the canonical `X402Code` values.
|
|
7
|
+
*
|
|
8
|
+
* `X402PaymentError` here is the client-facing analog: it surfaces in
|
|
9
|
+
* caller code (browser, CLI, server-to-server) when a payment attempt
|
|
10
|
+
* cannot complete. Callers can `instanceof`-check or pattern-match on
|
|
11
|
+
* `.kind` to distinguish:
|
|
12
|
+
*
|
|
13
|
+
* - 'server_rejected' , the server returned 402 with a structured
|
|
14
|
+
* body. `code` carries the server's
|
|
15
|
+
* canonical X402Code (e.g. `wrong_recipient`),
|
|
16
|
+
* `serverError` carries the raw error string
|
|
17
|
+
* for human display.
|
|
18
|
+
* - 'pay_handler_failed' , the user-supplied `pay` callback threw
|
|
19
|
+
* (wallet rejection, no funds, signer error,
|
|
20
|
+
* etc). `cause` holds the original error.
|
|
21
|
+
* - 'retries_exhausted' , the request was still 402 after
|
|
22
|
+
* `maxRetries` payment attempts. Same shape
|
|
23
|
+
* as `server_rejected` but indicates
|
|
24
|
+
* repeated failure.
|
|
25
|
+
* - 'invalid_402_body' , the server returned 402 but the body
|
|
26
|
+
* wasn't a v2 PaymentRequirementsBody.
|
|
27
|
+
*
|
|
28
|
+
* The class is plain (no abstract methods, no fluent builders) so users
|
|
29
|
+
* can construct it themselves if they're wrapping the wrappers.
|
|
30
|
+
*/
|
|
31
|
+
import type { PaymentRequirementEntry, PaymentRequirementsBody } from '../core/types';
|
|
32
|
+
export type X402PaymentErrorKind = 'server_rejected' | 'retries_exhausted' | 'pay_handler_failed' | 'invalid_402_body';
|
|
33
|
+
export interface X402PaymentErrorInit {
|
|
34
|
+
message: string;
|
|
35
|
+
kind: X402PaymentErrorKind;
|
|
36
|
+
/** Canonical X402Code from the server, when known. */
|
|
37
|
+
code?: string;
|
|
38
|
+
/** `accepts[]` from the 402 body, for the caller to retry against. */
|
|
39
|
+
accepts?: PaymentRequirementEntry[];
|
|
40
|
+
/** HTTP status code that triggered the error (usually 402). */
|
|
41
|
+
httpStatus?: number;
|
|
42
|
+
/** Verbatim `error` string from the 402 body, for human display. */
|
|
43
|
+
serverError?: string;
|
|
44
|
+
/** Wrapped underlying error (wallet rejection, axios error, etc.). */
|
|
45
|
+
cause?: unknown;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Thrown by `x402Fetch` and `x402Axios` when a payment attempt fails or
|
|
49
|
+
* is exhausted.
|
|
50
|
+
*
|
|
51
|
+
* `instanceof X402PaymentError` is the reliable runtime check; the
|
|
52
|
+
* `.kind` field is the discriminator for switching on cause.
|
|
53
|
+
*/
|
|
54
|
+
export declare class X402PaymentError extends Error {
|
|
55
|
+
readonly kind: X402PaymentErrorKind;
|
|
56
|
+
readonly code?: string;
|
|
57
|
+
readonly accepts?: PaymentRequirementEntry[];
|
|
58
|
+
readonly httpStatus?: number;
|
|
59
|
+
readonly serverError?: string;
|
|
60
|
+
readonly cause?: unknown;
|
|
61
|
+
constructor(init: X402PaymentErrorInit);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Parse the (server, canonical) error string from a `PaymentRequirementsBody`.
|
|
65
|
+
*
|
|
66
|
+
* The middleware encodes failures as `"<base error> (<code>): <reason>"`
|
|
67
|
+
* (see `cap.ts` / `express.ts`). We pull the code out so callers can
|
|
68
|
+
* dispatch on it without re-parsing the wire string. The reason is
|
|
69
|
+
* preserved in `.serverError`.
|
|
70
|
+
*
|
|
71
|
+
* Returns `undefined` when the body has no recognizable code (e.g. the
|
|
72
|
+
* MISSING_HEADER path, where the middleware omits the parenthesised
|
|
73
|
+
* suffix).
|
|
74
|
+
*/
|
|
75
|
+
export declare function parseErrorCode(serverError?: string): string | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Build an `X402PaymentError` from a parsed 402 body. `kind` defaults
|
|
78
|
+
* to `server_rejected`; pass `'retries_exhausted'` when called after
|
|
79
|
+
* the retry loop gave up.
|
|
80
|
+
*/
|
|
81
|
+
export declare function paymentErrorFromBody(body: PaymentRequirementsBody, init?: {
|
|
82
|
+
kind?: X402PaymentErrorKind;
|
|
83
|
+
httpStatus?: number;
|
|
84
|
+
cause?: unknown;
|
|
85
|
+
}): X402PaymentError;
|
|
86
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Typed client-side errors thrown by `x402Fetch` / `x402Axios`.
|
|
4
|
+
*
|
|
5
|
+
* The server-side `X402Error` (in `srv/core/errors.ts`) is a different
|
|
6
|
+
* class with a different purpose: it's thrown inside the decode +
|
|
7
|
+
* validate pipeline and carries one of the canonical `X402Code` values.
|
|
8
|
+
*
|
|
9
|
+
* `X402PaymentError` here is the client-facing analog: it surfaces in
|
|
10
|
+
* caller code (browser, CLI, server-to-server) when a payment attempt
|
|
11
|
+
* cannot complete. Callers can `instanceof`-check or pattern-match on
|
|
12
|
+
* `.kind` to distinguish:
|
|
13
|
+
*
|
|
14
|
+
* - 'server_rejected' , the server returned 402 with a structured
|
|
15
|
+
* body. `code` carries the server's
|
|
16
|
+
* canonical X402Code (e.g. `wrong_recipient`),
|
|
17
|
+
* `serverError` carries the raw error string
|
|
18
|
+
* for human display.
|
|
19
|
+
* - 'pay_handler_failed' , the user-supplied `pay` callback threw
|
|
20
|
+
* (wallet rejection, no funds, signer error,
|
|
21
|
+
* etc). `cause` holds the original error.
|
|
22
|
+
* - 'retries_exhausted' , the request was still 402 after
|
|
23
|
+
* `maxRetries` payment attempts. Same shape
|
|
24
|
+
* as `server_rejected` but indicates
|
|
25
|
+
* repeated failure.
|
|
26
|
+
* - 'invalid_402_body' , the server returned 402 but the body
|
|
27
|
+
* wasn't a v2 PaymentRequirementsBody.
|
|
28
|
+
*
|
|
29
|
+
* The class is plain (no abstract methods, no fluent builders) so users
|
|
30
|
+
* can construct it themselves if they're wrapping the wrappers.
|
|
31
|
+
*/
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.X402PaymentError = void 0;
|
|
34
|
+
exports.parseErrorCode = parseErrorCode;
|
|
35
|
+
exports.paymentErrorFromBody = paymentErrorFromBody;
|
|
36
|
+
/**
|
|
37
|
+
* Thrown by `x402Fetch` and `x402Axios` when a payment attempt fails or
|
|
38
|
+
* is exhausted.
|
|
39
|
+
*
|
|
40
|
+
* `instanceof X402PaymentError` is the reliable runtime check; the
|
|
41
|
+
* `.kind` field is the discriminator for switching on cause.
|
|
42
|
+
*/
|
|
43
|
+
class X402PaymentError extends Error {
|
|
44
|
+
kind;
|
|
45
|
+
code;
|
|
46
|
+
accepts;
|
|
47
|
+
httpStatus;
|
|
48
|
+
serverError;
|
|
49
|
+
// Override Error's `cause` typing, ours is `unknown` to allow any value.
|
|
50
|
+
cause;
|
|
51
|
+
constructor(init) {
|
|
52
|
+
super(init.message);
|
|
53
|
+
this.name = 'X402PaymentError';
|
|
54
|
+
this.kind = init.kind;
|
|
55
|
+
if (init.code !== undefined)
|
|
56
|
+
this.code = init.code;
|
|
57
|
+
if (init.accepts !== undefined)
|
|
58
|
+
this.accepts = init.accepts;
|
|
59
|
+
if (init.httpStatus !== undefined)
|
|
60
|
+
this.httpStatus = init.httpStatus;
|
|
61
|
+
if (init.serverError !== undefined)
|
|
62
|
+
this.serverError = init.serverError;
|
|
63
|
+
if (init.cause !== undefined)
|
|
64
|
+
this.cause = init.cause;
|
|
65
|
+
// V8: keep stack trace pointing at caller, not constructor.
|
|
66
|
+
if (typeof Error.captureStackTrace === 'function') {
|
|
67
|
+
Error
|
|
68
|
+
.captureStackTrace(this, X402PaymentError);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.X402PaymentError = X402PaymentError;
|
|
73
|
+
/**
|
|
74
|
+
* Parse the (server, canonical) error string from a `PaymentRequirementsBody`.
|
|
75
|
+
*
|
|
76
|
+
* The middleware encodes failures as `"<base error> (<code>): <reason>"`
|
|
77
|
+
* (see `cap.ts` / `express.ts`). We pull the code out so callers can
|
|
78
|
+
* dispatch on it without re-parsing the wire string. The reason is
|
|
79
|
+
* preserved in `.serverError`.
|
|
80
|
+
*
|
|
81
|
+
* Returns `undefined` when the body has no recognizable code (e.g. the
|
|
82
|
+
* MISSING_HEADER path, where the middleware omits the parenthesised
|
|
83
|
+
* suffix).
|
|
84
|
+
*/
|
|
85
|
+
function parseErrorCode(serverError) {
|
|
86
|
+
if (!serverError)
|
|
87
|
+
return undefined;
|
|
88
|
+
const m = serverError.match(/\(([a-z_]+)\)/);
|
|
89
|
+
return m ? m[1] : undefined;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Build an `X402PaymentError` from a parsed 402 body. `kind` defaults
|
|
93
|
+
* to `server_rejected`; pass `'retries_exhausted'` when called after
|
|
94
|
+
* the retry loop gave up.
|
|
95
|
+
*/
|
|
96
|
+
function paymentErrorFromBody(body, init = {}) {
|
|
97
|
+
const code = parseErrorCode(body.error);
|
|
98
|
+
return new X402PaymentError({
|
|
99
|
+
message: body.error ?? 'payment required',
|
|
100
|
+
kind: init.kind ?? 'server_rejected',
|
|
101
|
+
...(code !== undefined ? { code } : {}),
|
|
102
|
+
accepts: body.accepts,
|
|
103
|
+
httpStatus: init.httpStatus ?? 402,
|
|
104
|
+
...(body.error !== undefined ? { serverError: body.error } : {}),
|
|
105
|
+
...(init.cause !== undefined ? { cause: init.cause } : {}),
|
|
106
|
+
});
|
|
107
|
+
}
|
package/srv/client/fetch.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `x402Fetch
|
|
2
|
+
* `x402Fetch`, drop-in fetch wrapper that auto-handles 402 responses.
|
|
3
3
|
*
|
|
4
4
|
* On a 402 response the wrapper:
|
|
5
5
|
* 1. Parses the body as a v2 `PaymentRequirementsBody`.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Non-402 responses are passed through untouched. After `maxRetries`
|
|
12
12
|
* payment attempts, the last response (whether 402 or other) is
|
|
13
|
-
* returned to the caller
|
|
13
|
+
* returned to the caller, never an infinite loop.
|
|
14
14
|
*
|
|
15
15
|
* Native fetch is used by default (Node ≥18, all modern browsers).
|
|
16
16
|
* Pass `opts.fetch` to override (testing, custom agents, etc.).
|