@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/srv/client/fetch.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* `x402Fetch
|
|
3
|
+
* `x402Fetch`, drop-in fetch wrapper that auto-handles 402 responses.
|
|
4
4
|
*
|
|
5
5
|
* On a 402 response the wrapper:
|
|
6
6
|
* 1. Parses the body as a v2 `PaymentRequirementsBody`.
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Non-402 responses are passed through untouched. After `maxRetries`
|
|
13
13
|
* payment attempts, the last response (whether 402 or other) is
|
|
14
|
-
* returned to the caller
|
|
14
|
+
* returned to the caller, never an infinite loop.
|
|
15
15
|
*
|
|
16
16
|
* Native fetch is used by default (Node ≥18, all modern browsers).
|
|
17
17
|
* Pass `opts.fetch` to override (testing, custom agents, etc.).
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
20
|
exports.x402Fetch = x402Fetch;
|
|
21
21
|
const envelope_1 = require("./envelope");
|
|
22
|
+
const errors_1 = require("./errors");
|
|
22
23
|
/**
|
|
23
24
|
* Wrap `fetch` with 402-handling. The returned function has the same
|
|
24
25
|
* signature as native fetch, so it's a drop-in replacement.
|
|
@@ -37,34 +38,93 @@ function x402Fetch(opts) {
|
|
|
37
38
|
return async function paidFetch(input, init) {
|
|
38
39
|
let attemptsLeft = maxRetries;
|
|
39
40
|
// Contextual typing from FetchFn means we don't need to spell out
|
|
40
|
-
// RequestInfo / RequestInit explicitly
|
|
41
|
+
// RequestInfo / RequestInit explicitly, those are DOM-only globals.
|
|
41
42
|
let nextInit = init;
|
|
43
|
+
// Remember the last parsed 402 body so retries_exhausted carries
|
|
44
|
+
// the same diagnostic shape as a fresh server_rejected.
|
|
45
|
+
let lastBody;
|
|
42
46
|
// Loop: original request + up-to-maxRetries payment retries.
|
|
43
47
|
// eslint-disable-next-line no-constant-condition
|
|
44
48
|
while (true) {
|
|
45
49
|
const res = await baseFetch(input, nextInit);
|
|
46
|
-
if (res.status !== 402
|
|
50
|
+
if (res.status !== 402)
|
|
47
51
|
return res;
|
|
52
|
+
if (attemptsLeft <= 0) {
|
|
53
|
+
if (opts.errorOnFailure) {
|
|
54
|
+
// Use the last successfully-parsed 402 body if we have one
|
|
55
|
+
// (we typically do, since the prior attempt parsed it). If
|
|
56
|
+
// not, parse one more time to give the caller a useful error.
|
|
57
|
+
let body = lastBody;
|
|
58
|
+
if (!body) {
|
|
59
|
+
try {
|
|
60
|
+
body = await res.clone().json();
|
|
61
|
+
}
|
|
62
|
+
catch { /* fall through */ }
|
|
63
|
+
}
|
|
64
|
+
if (body) {
|
|
65
|
+
throw (0, errors_1.paymentErrorFromBody)(body, { kind: 'retries_exhausted', httpStatus: res.status });
|
|
66
|
+
}
|
|
67
|
+
throw new errors_1.X402PaymentError({
|
|
68
|
+
message: 'x402Fetch: retries exhausted with no parsable 402 body',
|
|
69
|
+
kind: 'retries_exhausted',
|
|
70
|
+
httpStatus: res.status,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return res;
|
|
74
|
+
}
|
|
48
75
|
// Parse 402 body. If it's not a v2 PaymentRequirementsBody we
|
|
49
|
-
// bail with the original response (
|
|
76
|
+
// bail with the original response (or throw under errorOnFailure).
|
|
50
77
|
let body;
|
|
51
78
|
try {
|
|
52
|
-
body =
|
|
79
|
+
body = await res.clone().json();
|
|
53
80
|
}
|
|
54
81
|
catch {
|
|
82
|
+
if (opts.errorOnFailure) {
|
|
83
|
+
throw new errors_1.X402PaymentError({
|
|
84
|
+
message: 'x402Fetch: 402 body was not JSON',
|
|
85
|
+
kind: 'invalid_402_body',
|
|
86
|
+
httpStatus: res.status,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
55
89
|
return res;
|
|
56
90
|
}
|
|
57
|
-
if (body
|
|
91
|
+
if (!body || body.x402Version !== 2 || !Array.isArray(body.accepts) || body.accepts.length === 0) {
|
|
92
|
+
if (opts.errorOnFailure) {
|
|
93
|
+
throw new errors_1.X402PaymentError({
|
|
94
|
+
message: 'x402Fetch: 402 body is not a v2 PaymentRequirementsBody',
|
|
95
|
+
kind: 'invalid_402_body',
|
|
96
|
+
httpStatus: res.status,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
58
99
|
return res;
|
|
59
100
|
}
|
|
101
|
+
lastBody = body;
|
|
60
102
|
const chosen = select(body.accepts);
|
|
61
|
-
if (!chosen)
|
|
103
|
+
if (!chosen) {
|
|
104
|
+
if (opts.errorOnFailure) {
|
|
105
|
+
throw (0, errors_1.paymentErrorFromBody)(body, { kind: 'server_rejected', httpStatus: res.status });
|
|
106
|
+
}
|
|
62
107
|
return res;
|
|
63
|
-
|
|
108
|
+
}
|
|
109
|
+
// Invoke user's pay handler. Wallet rejection / signer errors /
|
|
110
|
+
// network blips here all surface as X402PaymentError(pay_handler_failed)
|
|
111
|
+
// with the original error on `.cause`, regardless of errorOnFailure.
|
|
112
|
+
let payResult;
|
|
113
|
+
try {
|
|
114
|
+
payResult = await opts.pay(chosen);
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
throw new errors_1.X402PaymentError({
|
|
118
|
+
message: `x402Fetch: pay handler failed: ${err?.message ?? String(err)}`,
|
|
119
|
+
kind: 'pay_handler_failed',
|
|
120
|
+
accepts: body.accepts,
|
|
121
|
+
cause: err,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
64
124
|
const header = (0, envelope_1.encodePaymentEnvelope)({
|
|
65
125
|
network: chosen.network,
|
|
66
|
-
signedTxCborHex,
|
|
67
|
-
nonceRef,
|
|
126
|
+
signedTxCborHex: payResult.signedTxCborHex,
|
|
127
|
+
nonceRef: payResult.nonceRef,
|
|
68
128
|
});
|
|
69
129
|
// Merge PAYMENT-SIGNATURE into headers without mutating caller's init.
|
|
70
130
|
const mergedHeaders = new Headers(nextInit?.headers ?? init?.headers);
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `createBridgePayHandler` is the default for server-to-server flows
|
|
5
5
|
* (one CAP service calling another, AI-agent payments). It uses the
|
|
6
|
-
* existing `buildUnsignedPaymentTx` helper
|
|
7
|
-
* `@odatano/core` bridge access at runtime
|
|
6
|
+
* existing `buildUnsignedPaymentTx` helper, which requires
|
|
7
|
+
* `@odatano/core` bridge access at runtime, and delegates signing
|
|
8
8
|
* to a caller-supplied `signTx` callback.
|
|
9
9
|
*
|
|
10
10
|
* For browser CIP-30 wallets, write your own PayHandler: get UTxOs
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import type { PayHandler } from './types';
|
|
16
16
|
export interface BridgePayHandlerOptions {
|
|
17
|
-
/** Buyer bech32
|
|
17
|
+
/** Buyer bech32, used for UTxO lookup and change. */
|
|
18
18
|
buyerBech32: string;
|
|
19
19
|
/**
|
|
20
20
|
* Sign the unsigned tx CBOR. Returns the SIGNED tx CBOR hex
|
|
@@ -34,7 +34,7 @@ export interface BridgePayHandlerOptions {
|
|
|
34
34
|
* 2. caller-supplied `signTx` (signs the unsigned CBOR)
|
|
35
35
|
* 3. returns `{ signedTxCborHex, nonceRef }`
|
|
36
36
|
*
|
|
37
|
-
* The signed tx is NOT submitted here
|
|
37
|
+
* The signed tx is NOT submitted here, the x402 server submits it
|
|
38
38
|
* after validating the envelope (per Cardano-x402-v2 facilitator flow).
|
|
39
39
|
*/
|
|
40
40
|
export declare function createBridgePayHandler(opts: BridgePayHandlerOptions): PayHandler;
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* `createBridgePayHandler` is the default for server-to-server flows
|
|
6
6
|
* (one CAP service calling another, AI-agent payments). It uses the
|
|
7
|
-
* existing `buildUnsignedPaymentTx` helper
|
|
8
|
-
* `@odatano/core` bridge access at runtime
|
|
7
|
+
* existing `buildUnsignedPaymentTx` helper, which requires
|
|
8
|
+
* `@odatano/core` bridge access at runtime, and delegates signing
|
|
9
9
|
* to a caller-supplied `signTx` callback.
|
|
10
10
|
*
|
|
11
11
|
* For browser CIP-30 wallets, write your own PayHandler: get UTxOs
|
|
@@ -22,7 +22,7 @@ const build_unsigned_tx_1 = require("../helpers/build-unsigned-tx");
|
|
|
22
22
|
* 2. caller-supplied `signTx` (signs the unsigned CBOR)
|
|
23
23
|
* 3. returns `{ signedTxCborHex, nonceRef }`
|
|
24
24
|
*
|
|
25
|
-
* The signed tx is NOT submitted here
|
|
25
|
+
* The signed tx is NOT submitted here, the x402 server submits it
|
|
26
26
|
* after validating the envelope (per Cardano-x402-v2 facilitator flow).
|
|
27
27
|
*/
|
|
28
28
|
function createBridgePayHandler(opts) {
|
package/srv/client/types.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Client-side helper types
|
|
2
|
+
* Client-side helper types, symmetric to the server-side facilitator.
|
|
3
3
|
*
|
|
4
4
|
* A `PayHandler` is the one extension point: given the `accepts[]` entry
|
|
5
5
|
* the user chose, it must produce a signed payment tx (CBOR hex) + the
|
|
6
|
-
* v2 nonce reference. Everything else
|
|
7
|
-
* envelope encoding
|
|
6
|
+
* v2 nonce reference. Everything else, 402-detection, retry loop,
|
|
7
|
+
* envelope encoding, is generic and lives in `fetch.ts` / `axios.ts`.
|
|
8
8
|
*/
|
|
9
9
|
import type { PaymentRequirementEntry } from '../core/types';
|
|
10
10
|
/**
|
|
@@ -19,7 +19,7 @@ export interface PayHandlerResult {
|
|
|
19
19
|
/** Hex of the **signed** payment-tx CBOR (vkey witness set populated). */
|
|
20
20
|
signedTxCborHex: string;
|
|
21
21
|
/**
|
|
22
|
-
* v2 nonce reference `<txHash>#<outputIndex
|
|
22
|
+
* v2 nonce reference `<txHash>#<outputIndex>`, must point to an
|
|
23
23
|
* unspent UTxO that ALSO appears as an input of the signed tx.
|
|
24
24
|
* (Server enforces both at validate time.)
|
|
25
25
|
*/
|
|
@@ -28,17 +28,29 @@ export interface PayHandlerResult {
|
|
|
28
28
|
/** Pick which `accepts[]` entry to satisfy. Default picks the first. */
|
|
29
29
|
export type AcceptsSelector = (accepts: PaymentRequirementEntry[]) => PaymentRequirementEntry | undefined;
|
|
30
30
|
export interface X402ClientOptions {
|
|
31
|
-
/** Required
|
|
31
|
+
/** Required, how to produce the signed payment tx. */
|
|
32
32
|
pay: PayHandler;
|
|
33
33
|
/**
|
|
34
|
-
* Optional
|
|
34
|
+
* Optional, choose one of the `accepts[]` entries when the server
|
|
35
35
|
* offers multiple. Defaults to `accepts[0]`.
|
|
36
36
|
*/
|
|
37
37
|
selectAccepts?: AcceptsSelector;
|
|
38
38
|
/**
|
|
39
39
|
* Maximum number of 402-driven payment retries per request. Default 1
|
|
40
|
-
|
|
40
|
+
*, i.e. one payment attempt per request, no infinite loops.
|
|
41
41
|
*/
|
|
42
42
|
maxRetries?: number;
|
|
43
|
+
/**
|
|
44
|
+
* When `true`, throw an `X402PaymentError` after retries are
|
|
45
|
+
* exhausted (or when the 402 body is malformed). Default `false`:
|
|
46
|
+
*
|
|
47
|
+
* - `x402Fetch` returns the last `Response` (the 402 itself).
|
|
48
|
+
* - `x402Axios` re-throws the original AxiosError.
|
|
49
|
+
*
|
|
50
|
+
* Pay-handler throws are ALWAYS wrapped in `X402PaymentError` (kind:
|
|
51
|
+
* `'pay_handler_failed'`) regardless of this flag, with the original
|
|
52
|
+
* error preserved on `.cause`.
|
|
53
|
+
*/
|
|
54
|
+
errorOnFailure?: boolean;
|
|
43
55
|
}
|
|
44
56
|
//# sourceMappingURL=types.d.ts.map
|
package/srv/client/types.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Client-side helper types
|
|
3
|
+
* Client-side helper types, symmetric to the server-side facilitator.
|
|
4
4
|
*
|
|
5
5
|
* A `PayHandler` is the one extension point: given the `accepts[]` entry
|
|
6
6
|
* the user chose, it must produce a signed payment tx (CBOR hex) + the
|
|
7
|
-
* v2 nonce reference. Everything else
|
|
8
|
-
* envelope encoding
|
|
7
|
+
* v2 nonce reference. Everything else, 402-detection, retry loop,
|
|
8
|
+
* envelope encoding, is generic and lives in `fetch.ts` / `axios.ts`.
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
package/srv/core/asset.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface ParsedAsset {
|
|
|
21
21
|
assetNameHex: string;
|
|
22
22
|
/**
|
|
23
23
|
* Concatenation used as the canonical UTxO `unit` key by Blockfrost /
|
|
24
|
-
* Koios / ODATANO (`policyId + assetNameHex`). Empty for lovelace
|
|
24
|
+
* Koios / ODATANO (`policyId + assetNameHex`). Empty for lovelace ,
|
|
25
25
|
* comparisons against UTxO assets short-circuit via `isLovelace`.
|
|
26
26
|
*/
|
|
27
27
|
unit: string;
|
package/srv/core/decode.d.ts
CHANGED
|
@@ -12,14 +12,14 @@
|
|
|
12
12
|
* }
|
|
13
13
|
* }))
|
|
14
14
|
*
|
|
15
|
-
* The decoder is **pure
|
|
15
|
+
* The decoder is **pure**, no chain calls, no DB. It produces a
|
|
16
16
|
* `DecodedPayment` that downstream `validate.ts` checks against
|
|
17
17
|
* `PaymentRequirementEntry` (the 6 mandatory checks).
|
|
18
18
|
*/
|
|
19
19
|
import type { DecodedPayment } from './types';
|
|
20
20
|
/**
|
|
21
21
|
* Decode a `PAYMENT-SIGNATURE` header value end-to-end. Throws X402Error
|
|
22
|
-
* with a precise `code` on any malformed input
|
|
22
|
+
* with a precise `code` on any malformed input, the caller catches and
|
|
23
23
|
* surfaces the code in the 402 response body.
|
|
24
24
|
*/
|
|
25
25
|
export declare function decode(paymentHeader: string | undefined | null): DecodedPayment;
|
package/srv/core/decode.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* }
|
|
14
14
|
* }))
|
|
15
15
|
*
|
|
16
|
-
* The decoder is **pure
|
|
16
|
+
* The decoder is **pure**, no chain calls, no DB. It produces a
|
|
17
17
|
* `DecodedPayment` that downstream `validate.ts` checks against
|
|
18
18
|
* `PaymentRequirementEntry` (the 6 mandatory checks).
|
|
19
19
|
*/
|
|
@@ -59,7 +59,7 @@ const SUPPORTED_SCHEME = 'exact';
|
|
|
59
59
|
const NONCE_RE = /^([0-9a-f]{64})#(\d+)$/i;
|
|
60
60
|
function decodeBase64ToBuffer(s, errCode) {
|
|
61
61
|
// Node's Buffer.from is lenient (silently drops bad chars). Re-encode
|
|
62
|
-
// and compare modulo padding to catch malformed input early
|
|
62
|
+
// and compare modulo padding to catch malformed input early, otherwise
|
|
63
63
|
// garbage in `transaction` would only fail at CBOR parse time with a
|
|
64
64
|
// confusing error.
|
|
65
65
|
const buf = Buffer.from(s, 'base64');
|
|
@@ -121,7 +121,7 @@ function extractInputs(txBody) {
|
|
|
121
121
|
/**
|
|
122
122
|
* Pull validity range bounds. CSL exposes `ttl()` (upper) since Shelley
|
|
123
123
|
* and `validity_start_interval_bignum()` (lower) since Allegra. Both can
|
|
124
|
-
* be absent
|
|
124
|
+
* be absent, in which case we return null and the TTL check is skipped
|
|
125
125
|
* (per v2 spec: only validate TTL if buyer set one).
|
|
126
126
|
*/
|
|
127
127
|
function extractValidityRange(txBody) {
|
|
@@ -166,7 +166,7 @@ function parseNonceRef(nonce) {
|
|
|
166
166
|
}
|
|
167
167
|
/**
|
|
168
168
|
* Decode a `PAYMENT-SIGNATURE` header value end-to-end. Throws X402Error
|
|
169
|
-
* with a precise `code` on any malformed input
|
|
169
|
+
* with a precise `code` on any malformed input, the caller catches and
|
|
170
170
|
* surfaces the code in the 402 response body.
|
|
171
171
|
*/
|
|
172
172
|
function decode(paymentHeader) {
|
|
@@ -201,7 +201,7 @@ function decode(paymentHeader) {
|
|
|
201
201
|
throw new errors_1.X402Error(errors_1.Codes.MISSING_FIELD, 'payload.nonce is required (v2 UTxO-ref)');
|
|
202
202
|
}
|
|
203
203
|
// 3. Tx CBOR → CSL Transaction (parses both `transaction` and `fixed`
|
|
204
|
-
// representation; we need both
|
|
204
|
+
// representation; we need both, Transaction for body access,
|
|
205
205
|
// FixedTransaction for byte-stable hash).
|
|
206
206
|
const txBuf = decodeBase64ToBuffer(payload.transaction, errors_1.Codes.INVALID_CBOR);
|
|
207
207
|
let tx;
|
package/srv/core/errors.js
CHANGED
|
@@ -39,9 +39,9 @@ exports.Codes = Object.freeze({
|
|
|
39
39
|
WRONG_RECIPIENT: 'wrong_recipient', // check 2
|
|
40
40
|
INSUFFICIENT_AMOUNT: 'insufficient_amount', // check 3
|
|
41
41
|
WRONG_ASSET: 'wrong_asset', // check 4
|
|
42
|
-
REPLAY: 'replay_detected', // check 5
|
|
43
|
-
NONCE_NOT_REFERENCED: 'nonce_not_referenced', // check 5
|
|
44
|
-
EXPIRED_TTL: 'expired_ttl', // check 6
|
|
42
|
+
REPLAY: 'replay_detected', // check 5, UTxO already spent
|
|
43
|
+
NONCE_NOT_REFERENCED: 'nonce_not_referenced', // check 5, UTxO not in tx inputs
|
|
44
|
+
EXPIRED_TTL: 'expired_ttl', // check 6, validity range upper bound passed
|
|
45
45
|
// ---- supporting ----
|
|
46
46
|
UNSIGNED_TRANSACTION: 'unsigned_transaction', // sanity: no vkey witnesses
|
|
47
47
|
// ---- settle ----
|
package/srv/core/network.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export type Network = 'cardano:mainnet' | 'cardano:preprod' | 'cardano:preview';
|
|
|
10
10
|
export declare function isNetwork(s: unknown): s is Network;
|
|
11
11
|
/**
|
|
12
12
|
* Validate a network string and return it typed. Throws X402Error on
|
|
13
|
-
* malformed input
|
|
13
|
+
* malformed input, including v1-style hyphen variants, so the caller's
|
|
14
14
|
* 402 body carries a precise diagnostic.
|
|
15
15
|
*/
|
|
16
16
|
export declare function parseNetwork(s: string): Network;
|
package/srv/core/network.js
CHANGED
|
@@ -18,7 +18,7 @@ function isNetwork(s) {
|
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
20
|
* Validate a network string and return it typed. Throws X402Error on
|
|
21
|
-
* malformed input
|
|
21
|
+
* malformed input, including v1-style hyphen variants, so the caller's
|
|
22
22
|
* 402 body carries a precise diagnostic.
|
|
23
23
|
*/
|
|
24
24
|
function parseNetwork(s) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Build the canonical Cardano-x402-v2 PaymentRequirements body.
|
|
3
3
|
*
|
|
4
|
-
* Asset-agnostic by design
|
|
4
|
+
* Asset-agnostic by design, the consumer passes a v2 asset string
|
|
5
5
|
* (`<policy>.<nameHex>` or `'lovelace'`), a payTo bech32, a network, and
|
|
6
6
|
* a resource descriptor. There is NO USDM default and no decimals
|
|
7
|
-
* assumption
|
|
7
|
+
* assumption, both belong to the consumer's product config.
|
|
8
8
|
*
|
|
9
9
|
* The v2 shape diverges from v1 in five places (see `docs/spec-v2-summary.md`
|
|
10
10
|
* once written):
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* 5. `accepts[].assetTransferMethod`(new field)
|
|
18
18
|
*/
|
|
19
19
|
import { type Network } from './network';
|
|
20
|
-
import type { PaymentRequirementEntry, PaymentRequirementsBody, ResourceDescriptor, AssetTransferMethod } from './types';
|
|
20
|
+
import type { PaymentRequirementEntry, PaymentRequirementsBody, ResourceDescriptor, AssetTransferMethod, RouteOption } from './types';
|
|
21
21
|
export interface BuildPaymentRequirementsArgs {
|
|
22
22
|
/** Asset amount in raw units (BigInt-safe). */
|
|
23
23
|
amount: string | number | bigint;
|
|
@@ -41,11 +41,43 @@ export interface BuildPaymentRequirementsArgs {
|
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
43
|
* Construct a single `accepts[]` entry. Most callers want
|
|
44
|
-
* `buildPaymentRequirements()` which wraps this in the 402 envelope
|
|
44
|
+
* `buildPaymentRequirements()` which wraps this in the 402 envelope ,
|
|
45
45
|
* use `buildEntry()` directly when composing multi-asset accept lists.
|
|
46
46
|
*/
|
|
47
47
|
export declare function buildEntry(args: BuildPaymentRequirementsArgs): PaymentRequirementEntry;
|
|
48
48
|
export declare function buildPaymentRequirements(args: BuildPaymentRequirementsArgs): PaymentRequirementsBody;
|
|
49
|
-
/** Pick the first `accepts[]` entry
|
|
49
|
+
/** Pick the first `accepts[]` entry, what the validator inspects. */
|
|
50
50
|
export declare function flatRequirements(body: PaymentRequirementsBody): PaymentRequirementEntry;
|
|
51
|
+
export interface BuildMultiArgs {
|
|
52
|
+
/**
|
|
53
|
+
* Payment options the seller advertises. Buyer picks one implicitly by
|
|
54
|
+
* which (payTo, asset) the payment tx actually credits. MUST be non-empty.
|
|
55
|
+
*/
|
|
56
|
+
options: RouteOption[];
|
|
57
|
+
/** Default recipient if a `RouteOption` omits `payTo`. */
|
|
58
|
+
payTo: string;
|
|
59
|
+
/** Default network if a `RouteOption` omits `network`. */
|
|
60
|
+
network: Network | string;
|
|
61
|
+
/** Default asset if a `RouteOption` omits `asset`. */
|
|
62
|
+
asset?: string;
|
|
63
|
+
resource: ResourceDescriptor | string;
|
|
64
|
+
description?: string;
|
|
65
|
+
mimeType?: string;
|
|
66
|
+
outputSchema?: unknown;
|
|
67
|
+
assetTransferMethod?: AssetTransferMethod;
|
|
68
|
+
maxTimeoutSeconds?: number;
|
|
69
|
+
extra?: Record<string, unknown>;
|
|
70
|
+
/** Same as the single-entry builder's flag. See `BuildPaymentRequirementsArgs`. */
|
|
71
|
+
withMissingHeaderError?: boolean;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Build a multi-entry `accepts[]` body. Each option inherits the
|
|
75
|
+
* top-level `payTo` / `network` / `asset` / etc. defaults unless it sets
|
|
76
|
+
* its own. The `resource` block is shared across all entries (a 402 is
|
|
77
|
+
* for one route, even when it offers many payment options).
|
|
78
|
+
*
|
|
79
|
+
* Single-entry callers should keep using `buildPaymentRequirements()`;
|
|
80
|
+
* this function is the path for the multi-accept feature.
|
|
81
|
+
*/
|
|
82
|
+
export declare function buildPaymentRequirementsMulti(args: BuildMultiArgs): PaymentRequirementsBody;
|
|
51
83
|
//# sourceMappingURL=requirements.d.ts.map
|
package/srv/core/requirements.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Build the canonical Cardano-x402-v2 PaymentRequirements body.
|
|
4
4
|
*
|
|
5
|
-
* Asset-agnostic by design
|
|
5
|
+
* Asset-agnostic by design, the consumer passes a v2 asset string
|
|
6
6
|
* (`<policy>.<nameHex>` or `'lovelace'`), a payTo bech32, a network, and
|
|
7
7
|
* a resource descriptor. There is NO USDM default and no decimals
|
|
8
|
-
* assumption
|
|
8
|
+
* assumption, both belong to the consumer's product config.
|
|
9
9
|
*
|
|
10
10
|
* The v2 shape diverges from v1 in five places (see `docs/spec-v2-summary.md`
|
|
11
11
|
* once written):
|
|
@@ -21,6 +21,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
21
21
|
exports.buildEntry = buildEntry;
|
|
22
22
|
exports.buildPaymentRequirements = buildPaymentRequirements;
|
|
23
23
|
exports.flatRequirements = flatRequirements;
|
|
24
|
+
exports.buildPaymentRequirementsMulti = buildPaymentRequirementsMulti;
|
|
24
25
|
const network_1 = require("./network");
|
|
25
26
|
const asset_1 = require("./asset");
|
|
26
27
|
function normalizeResource(resource, description, mimeType, outputSchema) {
|
|
@@ -44,7 +45,7 @@ function normalizeResource(resource, description, mimeType, outputSchema) {
|
|
|
44
45
|
}
|
|
45
46
|
/**
|
|
46
47
|
* Construct a single `accepts[]` entry. Most callers want
|
|
47
|
-
* `buildPaymentRequirements()` which wraps this in the 402 envelope
|
|
48
|
+
* `buildPaymentRequirements()` which wraps this in the 402 envelope ,
|
|
48
49
|
* use `buildEntry()` directly when composing multi-asset accept lists.
|
|
49
50
|
*/
|
|
50
51
|
function buildEntry(args) {
|
|
@@ -77,10 +78,48 @@ function buildPaymentRequirements(args) {
|
|
|
77
78
|
accepts,
|
|
78
79
|
};
|
|
79
80
|
}
|
|
80
|
-
/** Pick the first `accepts[]` entry
|
|
81
|
+
/** Pick the first `accepts[]` entry, what the validator inspects. */
|
|
81
82
|
function flatRequirements(body) {
|
|
82
83
|
if (!body.accepts || body.accepts.length === 0) {
|
|
83
84
|
throw new Error('flatRequirements: PaymentRequirementsBody.accepts is empty');
|
|
84
85
|
}
|
|
85
86
|
return body.accepts[0];
|
|
86
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Build a multi-entry `accepts[]` body. Each option inherits the
|
|
90
|
+
* top-level `payTo` / `network` / `asset` / etc. defaults unless it sets
|
|
91
|
+
* its own. The `resource` block is shared across all entries (a 402 is
|
|
92
|
+
* for one route, even when it offers many payment options).
|
|
93
|
+
*
|
|
94
|
+
* Single-entry callers should keep using `buildPaymentRequirements()`;
|
|
95
|
+
* this function is the path for the multi-accept feature.
|
|
96
|
+
*/
|
|
97
|
+
function buildPaymentRequirementsMulti(args) {
|
|
98
|
+
if (!args.options || args.options.length === 0) {
|
|
99
|
+
throw new Error('buildPaymentRequirementsMulti: options must be non-empty');
|
|
100
|
+
}
|
|
101
|
+
const accepts = args.options.map((opt) => {
|
|
102
|
+
const asset = opt.asset ?? args.asset;
|
|
103
|
+
if (!asset) {
|
|
104
|
+
throw new Error('buildPaymentRequirementsMulti: option is missing `asset` and no top-level default was set');
|
|
105
|
+
}
|
|
106
|
+
return buildEntry({
|
|
107
|
+
amount: opt.amount,
|
|
108
|
+
asset,
|
|
109
|
+
payTo: opt.payTo ?? args.payTo,
|
|
110
|
+
network: opt.network ?? args.network,
|
|
111
|
+
resource: args.resource,
|
|
112
|
+
description: opt.description ?? args.description,
|
|
113
|
+
mimeType: opt.mimeType ?? args.mimeType,
|
|
114
|
+
outputSchema: args.outputSchema,
|
|
115
|
+
assetTransferMethod: opt.assetTransferMethod ?? args.assetTransferMethod,
|
|
116
|
+
maxTimeoutSeconds: opt.maxTimeoutSeconds ?? args.maxTimeoutSeconds,
|
|
117
|
+
extra: opt.extra ?? args.extra,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
x402Version: 2,
|
|
122
|
+
...(args.withMissingHeaderError ? { error: 'PAYMENT-SIGNATURE header is required' } : {}),
|
|
123
|
+
accepts,
|
|
124
|
+
};
|
|
125
|
+
}
|
package/srv/core/types.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Public type surface for x402 Cardano-v2.
|
|
3
3
|
*
|
|
4
|
-
* The shapes here match the v2 spec
|
|
5
|
-
* `docs/
|
|
6
|
-
*
|
|
4
|
+
* The shapes here match the Cardano-x402-v2 spec (envelope, requirements,
|
|
5
|
+
* payload). See `docs/protocol.md` for the wire-level reference. Keep
|
|
6
|
+
* this file as the single source of truth: middleware, facilitator and
|
|
7
7
|
* helpers all import from here.
|
|
8
8
|
*/
|
|
9
9
|
import type { Network } from './network';
|
|
10
10
|
/** Asset-transfer method. v2 spec. MVP supports only `default`. */
|
|
11
11
|
export type AssetTransferMethod = 'default' | 'masumi' | 'script';
|
|
12
|
-
/** v2 resource descriptor
|
|
12
|
+
/** v2 resource descriptor, was a bare string in v1. */
|
|
13
13
|
export interface ResourceDescriptor {
|
|
14
14
|
url: string;
|
|
15
15
|
description: string;
|
|
@@ -45,7 +45,7 @@ export interface PaymentRequirementsBody {
|
|
|
45
45
|
/**
|
|
46
46
|
* Decoded `PAYMENT-SIGNATURE` envelope. The envelope payload references
|
|
47
47
|
* a buyer-funded UTxO (the **nonce**) which must also appear in the
|
|
48
|
-
* payment tx's inputs
|
|
48
|
+
* payment tx's inputs, this is the v2 replay defense, on-chain.
|
|
49
49
|
*/
|
|
50
50
|
export interface PaymentEnvelope {
|
|
51
51
|
x402Version: 2;
|
|
@@ -54,7 +54,7 @@ export interface PaymentEnvelope {
|
|
|
54
54
|
payload: {
|
|
55
55
|
/** base64 CBOR of the signed payment tx */
|
|
56
56
|
transaction: string;
|
|
57
|
-
/** `<txHash>#<outputIndex
|
|
57
|
+
/** `<txHash>#<outputIndex>`, UTxO acting as replay nonce */
|
|
58
58
|
nonce: string;
|
|
59
59
|
};
|
|
60
60
|
}
|
|
@@ -69,6 +69,8 @@ export interface PaymentClaim {
|
|
|
69
69
|
unit: string;
|
|
70
70
|
/** The v2 asset string from requirements (passed-through for audit). */
|
|
71
71
|
asset: string;
|
|
72
|
+
/** Bech32 recipient the matched requirements entry credited. */
|
|
73
|
+
payTo: string;
|
|
72
74
|
/** The route / resource URL the buyer paid for. */
|
|
73
75
|
resourceUrl: string;
|
|
74
76
|
/** UTxO-ref nonce as `<txHash>#<index>`. */
|
|
@@ -113,4 +115,64 @@ export interface DecodedInput {
|
|
|
113
115
|
outputIndex: number;
|
|
114
116
|
}
|
|
115
117
|
export type { Network };
|
|
118
|
+
/**
|
|
119
|
+
* One payment option inside a multi-accept route. Lets a seller advertise
|
|
120
|
+
* e.g. "0.5 ADA or 0.1 USDM" for the same route. Top-level middleware
|
|
121
|
+
* options (`payTo`, `network`, `asset`, `description`, ...) fill any
|
|
122
|
+
* field the option leaves unset.
|
|
123
|
+
*
|
|
124
|
+
* The buyer's tx picks ONE option implicitly by which (payTo, asset) it
|
|
125
|
+
* actually pays; the facilitator selects the matching entry via
|
|
126
|
+
* `pickRequirement()` before running the six validation checks against it.
|
|
127
|
+
*/
|
|
128
|
+
export interface RouteOption {
|
|
129
|
+
amount: string | number | bigint;
|
|
130
|
+
asset?: string;
|
|
131
|
+
payTo?: string;
|
|
132
|
+
network?: Network | string;
|
|
133
|
+
description?: string;
|
|
134
|
+
mimeType?: string;
|
|
135
|
+
assetTransferMethod?: AssetTransferMethod;
|
|
136
|
+
maxTimeoutSeconds?: number;
|
|
137
|
+
extra?: Record<string, unknown>;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* What a `routePricing` entry or a `PriceResolver` may return.
|
|
141
|
+
*
|
|
142
|
+
* - scalar , single price in the route's default asset
|
|
143
|
+
* - `RouteOption` , single option with field overrides
|
|
144
|
+
* - `RouteOption[]` , multi-accept, buyer picks one on-chain
|
|
145
|
+
*
|
|
146
|
+
* Returning `null` from a resolver means "no gate, pass through" , the
|
|
147
|
+
* non-null type is what gates a request.
|
|
148
|
+
*/
|
|
149
|
+
export type PriceSpec = string | number | bigint | RouteOption | RouteOption[];
|
|
150
|
+
/**
|
|
151
|
+
* Context passed to a `PriceResolver`. Intentionally minimal so we don't
|
|
152
|
+
* leak express/CAP-specific shapes through the public API. Both
|
|
153
|
+
* middlewares fill the common fields; CAP-only fields are optional.
|
|
154
|
+
*/
|
|
155
|
+
export interface PricingContext {
|
|
156
|
+
/**
|
|
157
|
+
* CAP: req.event ('READ'|'CREATE'|action-name).
|
|
158
|
+
* Express: last URL segment with OData function args stripped.
|
|
159
|
+
*/
|
|
160
|
+
event: string;
|
|
161
|
+
/** CAP target name (e.g. 'PricesService.Quotes'). Undefined in Express. */
|
|
162
|
+
target?: string;
|
|
163
|
+
/** Express request path. Undefined in CAP. */
|
|
164
|
+
path?: string;
|
|
165
|
+
/** HTTP method. Express only. */
|
|
166
|
+
method?: string;
|
|
167
|
+
/** Lower-cased header map. Array values for multi-valued headers. */
|
|
168
|
+
headers: Record<string, string | string[] | undefined>;
|
|
169
|
+
/** Parsed query params (Express). */
|
|
170
|
+
query?: Record<string, string | string[] | undefined>;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Dynamic pricing function. Sync or async. Return `null` to skip the
|
|
174
|
+
* gate, a scalar / option / option-array to charge. Errors thrown here
|
|
175
|
+
* are treated as middleware failures and surface as 500 to the buyer.
|
|
176
|
+
*/
|
|
177
|
+
export type PriceResolver = (ctx: PricingContext) => PriceSpec | null | Promise<PriceSpec | null>;
|
|
116
178
|
//# sourceMappingURL=types.d.ts.map
|
package/srv/core/types.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Public type surface for x402 Cardano-v2.
|
|
4
4
|
*
|
|
5
|
-
* The shapes here match the v2 spec
|
|
6
|
-
* `docs/
|
|
7
|
-
*
|
|
5
|
+
* The shapes here match the Cardano-x402-v2 spec (envelope, requirements,
|
|
6
|
+
* payload). See `docs/protocol.md` for the wire-level reference. Keep
|
|
7
|
+
* this file as the single source of truth: middleware, facilitator and
|
|
8
8
|
* helpers all import from here.
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|