@odatano/x402 0.1.0 → 0.3.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 +57 -0
- package/README.md +29 -281
- package/cds-plugin.js +2 -0
- package/db/x402-grants.cds +49 -0
- package/db/x402-receipts.cds +44 -0
- package/package.json +11 -4
- package/srv/bridge.d.ts +9 -12
- package/srv/bridge.js +10 -13
- package/srv/cds-augment.d.ts +17 -0
- package/srv/client/axios.d.ts +38 -0
- package/srv/client/axios.js +107 -0
- package/srv/client/envelope.d.ts +33 -0
- package/srv/client/envelope.js +52 -0
- package/srv/client/errors.d.ts +107 -0
- package/srv/client/errors.js +144 -0
- package/srv/client/fetch.d.ts +30 -0
- package/srv/client/fetch.js +141 -0
- package/srv/client/pay-handlers.d.ts +41 -0
- package/srv/client/pay-handlers.js +47 -0
- package/srv/client/types.d.ts +56 -0
- package/srv/client/types.js +10 -0
- 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 +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/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 +11 -2
- package/srv/index.js +23 -3
- package/srv/middleware/cap.d.ts +53 -8
- package/srv/middleware/cap.js +87 -43
- package/srv/middleware/express.d.ts +22 -9
- package/srv/middleware/express.js +21 -21
- 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/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;
|
|
@@ -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,107 @@
|
|
|
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
|
+
const errors_1 = require("./errors");
|
|
23
|
+
// Marker key on the config to break infinite-retry loops.
|
|
24
|
+
const RETRY_KEY = '__x402_x402Retries';
|
|
25
|
+
function isAxiosError(e) {
|
|
26
|
+
return !!e && typeof e === 'object' && 'response' in e;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Attach the x402 response interceptor in-place and return the same
|
|
30
|
+
* instance for chaining. The interceptor only fires on 402 responses;
|
|
31
|
+
* everything else passes through unchanged.
|
|
32
|
+
*/
|
|
33
|
+
function x402Axios(instance, opts) {
|
|
34
|
+
if (typeof opts?.pay !== 'function') {
|
|
35
|
+
throw new TypeError('x402Axios: opts.pay must be a function');
|
|
36
|
+
}
|
|
37
|
+
const maxRetries = opts.maxRetries ?? 1;
|
|
38
|
+
const selectFirst = (a) => a[0];
|
|
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
|
+
}
|
|
51
|
+
instance.interceptors.response.use((response) => response, async (error) => {
|
|
52
|
+
if (!isAxiosError(error) || error.response?.status !== 402 || !error.config) {
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
const cfg = error.config;
|
|
56
|
+
const retries = Number(cfg[RETRY_KEY] ?? 0);
|
|
57
|
+
// CAP-style servers wrap the v2 body inside an OData error envelope.
|
|
58
|
+
// Defensively unwrap before validating shape.
|
|
59
|
+
const body = (0, errors_1.unwrapCapEnvelope)(error.response.data);
|
|
60
|
+
const status = error.response.status;
|
|
61
|
+
if (retries >= maxRetries) {
|
|
62
|
+
if (opts.errorOnFailure) {
|
|
63
|
+
throw maybeWrap(body, 'retries_exhausted', status, error);
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
if (!body || body.x402Version !== 2 || !Array.isArray(body.accepts) || body.accepts.length === 0) {
|
|
68
|
+
if (opts.errorOnFailure) {
|
|
69
|
+
throw maybeWrap(body, 'invalid_402_body', status, error);
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
const chosen = select(body.accepts);
|
|
74
|
+
if (!chosen) {
|
|
75
|
+
if (opts.errorOnFailure) {
|
|
76
|
+
throw maybeWrap(body, 'server_rejected', status, error);
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
// Wrap pay-handler throws unconditionally, callers benefit from
|
|
81
|
+
// the structured shape regardless of errorOnFailure.
|
|
82
|
+
let payResult;
|
|
83
|
+
try {
|
|
84
|
+
payResult = await opts.pay(chosen);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
throw new errors_1.X402PaymentError({
|
|
88
|
+
message: `x402Axios: pay handler failed: ${err?.message ?? String(err)}`,
|
|
89
|
+
kind: 'pay_handler_failed',
|
|
90
|
+
accepts: body.accepts,
|
|
91
|
+
cause: err,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const header = (0, envelope_1.encodePaymentEnvelope)({
|
|
95
|
+
network: chosen.network,
|
|
96
|
+
signedTxCborHex: payResult.signedTxCborHex,
|
|
97
|
+
nonceRef: payResult.nonceRef,
|
|
98
|
+
});
|
|
99
|
+
const nextCfg = {
|
|
100
|
+
...cfg,
|
|
101
|
+
headers: { ...(cfg.headers ?? {}), 'PAYMENT-SIGNATURE': header },
|
|
102
|
+
[RETRY_KEY]: retries + 1,
|
|
103
|
+
};
|
|
104
|
+
return instance.request(nextCfg);
|
|
105
|
+
});
|
|
106
|
+
return instance;
|
|
107
|
+
}
|
|
@@ -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
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Build the `PAYMENT-SIGNATURE` header value for a Cardano-x402-v2 retry.
|
|
4
|
+
*
|
|
5
|
+
* Inverse of `srv/core/decode.ts`. The wire format is:
|
|
6
|
+
*
|
|
7
|
+
* PAYMENT-SIGNATURE: base64(JSON.stringify({
|
|
8
|
+
* x402Version: 2,
|
|
9
|
+
* scheme: 'exact',
|
|
10
|
+
* network: 'cardano:preprod' | 'cardano:mainnet' | 'cardano:preview',
|
|
11
|
+
* payload: {
|
|
12
|
+
* transaction: '<base64 CBOR of signed tx>',
|
|
13
|
+
* nonce: '<txHash>#<outputIndex>'
|
|
14
|
+
* }
|
|
15
|
+
* }))
|
|
16
|
+
*
|
|
17
|
+
* Pure function, no chain calls, no I/O. Callable from any runtime
|
|
18
|
+
* that has `Buffer` (Node) or a polyfill (browser bundlers usually
|
|
19
|
+
* provide one via `buffer`).
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.encodePaymentEnvelope = encodePaymentEnvelope;
|
|
23
|
+
const NONCE_RE = /^[0-9a-f]{64}#\d+$/i;
|
|
24
|
+
const HEX_RE = /^[0-9a-f]+$/i;
|
|
25
|
+
/**
|
|
26
|
+
* Encode the v2 PAYMENT-SIGNATURE envelope. Validates shape eagerly so
|
|
27
|
+
* a malformed call fails here, not on the server's `decode()`.
|
|
28
|
+
*/
|
|
29
|
+
function encodePaymentEnvelope(args) {
|
|
30
|
+
if (!args.network) {
|
|
31
|
+
throw new TypeError('encodePaymentEnvelope: network is required');
|
|
32
|
+
}
|
|
33
|
+
if (typeof args.signedTxCborHex !== 'string' || !HEX_RE.test(args.signedTxCborHex)) {
|
|
34
|
+
throw new TypeError('encodePaymentEnvelope: signedTxCborHex must be a hex string');
|
|
35
|
+
}
|
|
36
|
+
if (args.signedTxCborHex.length % 2 !== 0) {
|
|
37
|
+
throw new TypeError('encodePaymentEnvelope: signedTxCborHex has odd length');
|
|
38
|
+
}
|
|
39
|
+
if (!NONCE_RE.test(args.nonceRef)) {
|
|
40
|
+
throw new TypeError(`encodePaymentEnvelope: nonceRef must be '<txHash>#<outputIndex>' (64-hex#int), got '${args.nonceRef}'`);
|
|
41
|
+
}
|
|
42
|
+
const envelope = {
|
|
43
|
+
x402Version: 2,
|
|
44
|
+
scheme: 'exact',
|
|
45
|
+
network: args.network,
|
|
46
|
+
payload: {
|
|
47
|
+
transaction: Buffer.from(args.signedTxCborHex, 'hex').toString('base64'),
|
|
48
|
+
nonce: args.nonceRef,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
return Buffer.from(JSON.stringify(envelope), 'utf8').toString('base64');
|
|
52
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
* Defensively unwrap a CAP / OData error envelope around a v2 body.
|
|
65
|
+
*
|
|
66
|
+
* Background: `gateService` in this package historically (≤ v0.2)
|
|
67
|
+
* reaches a 402 via `req.reject(402, JSON.stringify(body))`, which
|
|
68
|
+
* CAP wraps into its standard OData error shape, putting the canonical
|
|
69
|
+
* v2 body inside `error.message` as a JSON string:
|
|
70
|
+
*
|
|
71
|
+
* { "error": { "message": "{\"x402Version\":2, ... }", "code": "402", ... } }
|
|
72
|
+
*
|
|
73
|
+
* The Express middleware emits the v2 body at the top level directly.
|
|
74
|
+
* This helper detects the CAP wrap and returns the unwrapped candidate
|
|
75
|
+
* so client wrappers can validate v2 shape uniformly. Non-CAP bodies
|
|
76
|
+
* pass through untouched.
|
|
77
|
+
*
|
|
78
|
+
* Symmetric server-side fix (emit canonical body even through CAP) is
|
|
79
|
+
* a separate concern, deferred until we can validate the CAP-version
|
|
80
|
+
* specific behaviour. The unwrap below keeps `x402Fetch` /
|
|
81
|
+
* `x402Axios` working against both shapes.
|
|
82
|
+
*/
|
|
83
|
+
export declare function unwrapCapEnvelope(parsed: unknown): unknown;
|
|
84
|
+
/**
|
|
85
|
+
* Parse the (server, canonical) error string from a `PaymentRequirementsBody`.
|
|
86
|
+
*
|
|
87
|
+
* The middleware encodes failures as `"<base error> (<code>): <reason>"`
|
|
88
|
+
* (see `cap.ts` / `express.ts`). We pull the code out so callers can
|
|
89
|
+
* dispatch on it without re-parsing the wire string. The reason is
|
|
90
|
+
* preserved in `.serverError`.
|
|
91
|
+
*
|
|
92
|
+
* Returns `undefined` when the body has no recognizable code (e.g. the
|
|
93
|
+
* MISSING_HEADER path, where the middleware omits the parenthesised
|
|
94
|
+
* suffix).
|
|
95
|
+
*/
|
|
96
|
+
export declare function parseErrorCode(serverError?: string): string | undefined;
|
|
97
|
+
/**
|
|
98
|
+
* Build an `X402PaymentError` from a parsed 402 body. `kind` defaults
|
|
99
|
+
* to `server_rejected`; pass `'retries_exhausted'` when called after
|
|
100
|
+
* the retry loop gave up.
|
|
101
|
+
*/
|
|
102
|
+
export declare function paymentErrorFromBody(body: PaymentRequirementsBody, init?: {
|
|
103
|
+
kind?: X402PaymentErrorKind;
|
|
104
|
+
httpStatus?: number;
|
|
105
|
+
cause?: unknown;
|
|
106
|
+
}): X402PaymentError;
|
|
107
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1,144 @@
|
|
|
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.unwrapCapEnvelope = unwrapCapEnvelope;
|
|
35
|
+
exports.parseErrorCode = parseErrorCode;
|
|
36
|
+
exports.paymentErrorFromBody = paymentErrorFromBody;
|
|
37
|
+
/**
|
|
38
|
+
* Thrown by `x402Fetch` and `x402Axios` when a payment attempt fails or
|
|
39
|
+
* is exhausted.
|
|
40
|
+
*
|
|
41
|
+
* `instanceof X402PaymentError` is the reliable runtime check; the
|
|
42
|
+
* `.kind` field is the discriminator for switching on cause.
|
|
43
|
+
*/
|
|
44
|
+
class X402PaymentError extends Error {
|
|
45
|
+
kind;
|
|
46
|
+
code;
|
|
47
|
+
accepts;
|
|
48
|
+
httpStatus;
|
|
49
|
+
serverError;
|
|
50
|
+
// Override Error's `cause` typing, ours is `unknown` to allow any value.
|
|
51
|
+
cause;
|
|
52
|
+
constructor(init) {
|
|
53
|
+
super(init.message);
|
|
54
|
+
this.name = 'X402PaymentError';
|
|
55
|
+
this.kind = init.kind;
|
|
56
|
+
if (init.code !== undefined)
|
|
57
|
+
this.code = init.code;
|
|
58
|
+
if (init.accepts !== undefined)
|
|
59
|
+
this.accepts = init.accepts;
|
|
60
|
+
if (init.httpStatus !== undefined)
|
|
61
|
+
this.httpStatus = init.httpStatus;
|
|
62
|
+
if (init.serverError !== undefined)
|
|
63
|
+
this.serverError = init.serverError;
|
|
64
|
+
if (init.cause !== undefined)
|
|
65
|
+
this.cause = init.cause;
|
|
66
|
+
// V8: keep stack trace pointing at caller, not constructor.
|
|
67
|
+
if (typeof Error.captureStackTrace === 'function') {
|
|
68
|
+
Error
|
|
69
|
+
.captureStackTrace(this, X402PaymentError);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.X402PaymentError = X402PaymentError;
|
|
74
|
+
/**
|
|
75
|
+
* Defensively unwrap a CAP / OData error envelope around a v2 body.
|
|
76
|
+
*
|
|
77
|
+
* Background: `gateService` in this package historically (≤ v0.2)
|
|
78
|
+
* reaches a 402 via `req.reject(402, JSON.stringify(body))`, which
|
|
79
|
+
* CAP wraps into its standard OData error shape, putting the canonical
|
|
80
|
+
* v2 body inside `error.message` as a JSON string:
|
|
81
|
+
*
|
|
82
|
+
* { "error": { "message": "{\"x402Version\":2, ... }", "code": "402", ... } }
|
|
83
|
+
*
|
|
84
|
+
* The Express middleware emits the v2 body at the top level directly.
|
|
85
|
+
* This helper detects the CAP wrap and returns the unwrapped candidate
|
|
86
|
+
* so client wrappers can validate v2 shape uniformly. Non-CAP bodies
|
|
87
|
+
* pass through untouched.
|
|
88
|
+
*
|
|
89
|
+
* Symmetric server-side fix (emit canonical body even through CAP) is
|
|
90
|
+
* a separate concern, deferred until we can validate the CAP-version
|
|
91
|
+
* specific behaviour. The unwrap below keeps `x402Fetch` /
|
|
92
|
+
* `x402Axios` working against both shapes.
|
|
93
|
+
*/
|
|
94
|
+
function unwrapCapEnvelope(parsed) {
|
|
95
|
+
if (!parsed || typeof parsed !== 'object')
|
|
96
|
+
return parsed;
|
|
97
|
+
const maybeError = parsed.error;
|
|
98
|
+
if (!maybeError || typeof maybeError !== 'object')
|
|
99
|
+
return parsed;
|
|
100
|
+
const msg = maybeError.message;
|
|
101
|
+
if (typeof msg !== 'string')
|
|
102
|
+
return parsed;
|
|
103
|
+
try {
|
|
104
|
+
return JSON.parse(msg);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return parsed;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Parse the (server, canonical) error string from a `PaymentRequirementsBody`.
|
|
112
|
+
*
|
|
113
|
+
* The middleware encodes failures as `"<base error> (<code>): <reason>"`
|
|
114
|
+
* (see `cap.ts` / `express.ts`). We pull the code out so callers can
|
|
115
|
+
* dispatch on it without re-parsing the wire string. The reason is
|
|
116
|
+
* preserved in `.serverError`.
|
|
117
|
+
*
|
|
118
|
+
* Returns `undefined` when the body has no recognizable code (e.g. the
|
|
119
|
+
* MISSING_HEADER path, where the middleware omits the parenthesised
|
|
120
|
+
* suffix).
|
|
121
|
+
*/
|
|
122
|
+
function parseErrorCode(serverError) {
|
|
123
|
+
if (!serverError)
|
|
124
|
+
return undefined;
|
|
125
|
+
const m = serverError.match(/\(([a-z_]+)\)/);
|
|
126
|
+
return m ? m[1] : undefined;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Build an `X402PaymentError` from a parsed 402 body. `kind` defaults
|
|
130
|
+
* to `server_rejected`; pass `'retries_exhausted'` when called after
|
|
131
|
+
* the retry loop gave up.
|
|
132
|
+
*/
|
|
133
|
+
function paymentErrorFromBody(body, init = {}) {
|
|
134
|
+
const code = parseErrorCode(body.error);
|
|
135
|
+
return new X402PaymentError({
|
|
136
|
+
message: body.error ?? 'payment required',
|
|
137
|
+
kind: init.kind ?? 'server_rejected',
|
|
138
|
+
...(code !== undefined ? { code } : {}),
|
|
139
|
+
accepts: body.accepts,
|
|
140
|
+
httpStatus: init.httpStatus ?? 402,
|
|
141
|
+
...(body.error !== undefined ? { serverError: body.error } : {}),
|
|
142
|
+
...(init.cause !== undefined ? { cause: init.cause } : {}),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `x402Fetch`, drop-in fetch wrapper that auto-handles 402 responses.
|
|
3
|
+
*
|
|
4
|
+
* On a 402 response the wrapper:
|
|
5
|
+
* 1. Parses the body as a v2 `PaymentRequirementsBody`.
|
|
6
|
+
* 2. Picks an `accepts[]` entry (via `selectAccepts`, default = first).
|
|
7
|
+
* 3. Calls the user's `pay` handler to get `{ signedTxCborHex, nonceRef }`.
|
|
8
|
+
* 4. Encodes a `PAYMENT-SIGNATURE` envelope.
|
|
9
|
+
* 5. Retries the original request with the header attached.
|
|
10
|
+
*
|
|
11
|
+
* Non-402 responses are passed through untouched. After `maxRetries`
|
|
12
|
+
* payment attempts, the last response (whether 402 or other) is
|
|
13
|
+
* returned to the caller, never an infinite loop.
|
|
14
|
+
*
|
|
15
|
+
* Native fetch is used by default (Node ≥18, all modern browsers).
|
|
16
|
+
* Pass `opts.fetch` to override (testing, custom agents, etc.).
|
|
17
|
+
*/
|
|
18
|
+
import type { X402ClientOptions } from './types';
|
|
19
|
+
type FetchFn = typeof globalThis.fetch;
|
|
20
|
+
export interface X402FetchOptions extends X402ClientOptions {
|
|
21
|
+
/** Override the underlying fetch. Defaults to `globalThis.fetch`. */
|
|
22
|
+
fetch?: FetchFn;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Wrap `fetch` with 402-handling. The returned function has the same
|
|
26
|
+
* signature as native fetch, so it's a drop-in replacement.
|
|
27
|
+
*/
|
|
28
|
+
export declare function x402Fetch(opts: X402FetchOptions): FetchFn;
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=fetch.d.ts.map
|