@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.
Files changed (64) hide show
  1. package/.github/workflows/test.yaml +49 -0
  2. package/CHANGELOG.md +57 -0
  3. package/README.md +29 -281
  4. package/cds-plugin.js +2 -0
  5. package/db/x402-grants.cds +49 -0
  6. package/db/x402-receipts.cds +44 -0
  7. package/package.json +11 -4
  8. package/srv/bridge.d.ts +9 -12
  9. package/srv/bridge.js +10 -13
  10. package/srv/cds-augment.d.ts +17 -0
  11. package/srv/client/axios.d.ts +38 -0
  12. package/srv/client/axios.js +107 -0
  13. package/srv/client/envelope.d.ts +33 -0
  14. package/srv/client/envelope.js +52 -0
  15. package/srv/client/errors.d.ts +107 -0
  16. package/srv/client/errors.js +144 -0
  17. package/srv/client/fetch.d.ts +30 -0
  18. package/srv/client/fetch.js +141 -0
  19. package/srv/client/pay-handlers.d.ts +41 -0
  20. package/srv/client/pay-handlers.js +47 -0
  21. package/srv/client/types.d.ts +56 -0
  22. package/srv/client/types.js +10 -0
  23. package/srv/core/asset.d.ts +1 -1
  24. package/srv/core/decode.d.ts +2 -2
  25. package/srv/core/decode.js +5 -5
  26. package/srv/core/errors.js +3 -3
  27. package/srv/core/network.d.ts +1 -1
  28. package/srv/core/network.js +1 -1
  29. package/srv/core/requirements.d.ts +37 -5
  30. package/srv/core/requirements.js +43 -4
  31. package/srv/core/types.d.ts +68 -6
  32. package/srv/core/types.js +3 -3
  33. package/srv/core/validate.d.ts +31 -7
  34. package/srv/core/validate.js +84 -9
  35. package/srv/facilitator/adapter.d.ts +69 -0
  36. package/srv/facilitator/adapter.js +52 -0
  37. package/srv/facilitator/http.d.ts +43 -0
  38. package/srv/facilitator/http.js +99 -0
  39. package/srv/facilitator/nonce.d.ts +4 -4
  40. package/srv/facilitator/nonce.js +4 -4
  41. package/srv/facilitator/server.d.ts +68 -0
  42. package/srv/facilitator/server.js +167 -0
  43. package/srv/facilitator/settle.d.ts +2 -2
  44. package/srv/facilitator/settle.js +4 -4
  45. package/srv/facilitator/verify.d.ts +5 -5
  46. package/srv/facilitator/verify.js +19 -5
  47. package/srv/helpers/build-unsigned-tx.d.ts +5 -5
  48. package/srv/helpers/build-unsigned-tx.js +3 -3
  49. package/srv/helpers/verify-confirmed.d.ts +1 -1
  50. package/srv/helpers/verify-confirmed.js +1 -1
  51. package/srv/index.d.ts +11 -2
  52. package/srv/index.js +23 -3
  53. package/srv/middleware/cap.d.ts +53 -8
  54. package/srv/middleware/cap.js +87 -43
  55. package/srv/middleware/express.d.ts +22 -9
  56. package/srv/middleware/express.js +21 -21
  57. package/srv/middleware/grants.d.ts +64 -0
  58. package/srv/middleware/grants.js +113 -0
  59. package/srv/middleware/pricing.d.ts +41 -0
  60. package/srv/middleware/pricing.js +78 -0
  61. package/srv/middleware/receipts.d.ts +38 -0
  62. package/srv/middleware/receipts.js +68 -0
  63. package/srv/plugin.d.ts +2 -2
  64. 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 and so renames in core (`getTransaction` → `getTransactionByHash`)
7
+ * to, and so renames in core (`getTransaction` → `getTransactionByHash`)
8
8
  * stay isolated to this file.
9
9
  *
10
- * Two methods needed by Cardano-x402-v2 are NOT (yet) first-class on
11
- * `@odatano/core@1.7.7`:
12
- * - `isUtxoUnspent(txHash, outputIndex)` for replay-defense check 5b
13
- * - `getCurrentSlot()` for TTL check 6
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 implemented here as **shims** on top of existing core
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` wraps `getLatestBlock().slot` with a
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` backed by `consumed_by` (Blockfrost) /
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 both are "not spendable" from the
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) consumers cast to ODATANO's `ParsedTransaction`
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