@odatano/x402 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/test.yaml +49 -0
- package/CHANGELOG.md +34 -0
- package/README.md +24 -281
- package/package.json +8 -2
- package/srv/cds-augment.d.ts +17 -0
- package/srv/client/axios.d.ts +38 -0
- package/srv/client/axios.js +68 -0
- package/srv/client/envelope.d.ts +33 -0
- package/srv/client/envelope.js +52 -0
- package/srv/client/fetch.d.ts +30 -0
- package/srv/client/fetch.js +76 -0
- package/srv/client/pay-handlers.d.ts +41 -0
- package/srv/client/pay-handlers.js +47 -0
- package/srv/client/types.d.ts +44 -0
- package/srv/client/types.js +10 -0
- package/srv/facilitator/adapter.d.ts +69 -0
- package/srv/facilitator/adapter.js +52 -0
- package/srv/facilitator/http.d.ts +43 -0
- package/srv/facilitator/http.js +99 -0
- package/srv/index.d.ts +7 -0
- package/srv/index.js +15 -1
- package/srv/middleware/cap.d.ts +7 -0
- package/srv/middleware/cap.js +3 -2
- package/srv/middleware/express.d.ts +8 -0
- package/srv/middleware/express.js +3 -2
|
@@ -0,0 +1,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,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
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `x402Fetch` — drop-in fetch wrapper that auto-handles 402 responses.
|
|
4
|
+
*
|
|
5
|
+
* On a 402 response the wrapper:
|
|
6
|
+
* 1. Parses the body as a v2 `PaymentRequirementsBody`.
|
|
7
|
+
* 2. Picks an `accepts[]` entry (via `selectAccepts`, default = first).
|
|
8
|
+
* 3. Calls the user's `pay` handler to get `{ signedTxCborHex, nonceRef }`.
|
|
9
|
+
* 4. Encodes a `PAYMENT-SIGNATURE` envelope.
|
|
10
|
+
* 5. Retries the original request with the header attached.
|
|
11
|
+
*
|
|
12
|
+
* Non-402 responses are passed through untouched. After `maxRetries`
|
|
13
|
+
* payment attempts, the last response (whether 402 or other) is
|
|
14
|
+
* returned to the caller — never an infinite loop.
|
|
15
|
+
*
|
|
16
|
+
* Native fetch is used by default (Node ≥18, all modern browsers).
|
|
17
|
+
* Pass `opts.fetch` to override (testing, custom agents, etc.).
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.x402Fetch = x402Fetch;
|
|
21
|
+
const envelope_1 = require("./envelope");
|
|
22
|
+
/**
|
|
23
|
+
* Wrap `fetch` with 402-handling. The returned function has the same
|
|
24
|
+
* signature as native fetch, so it's a drop-in replacement.
|
|
25
|
+
*/
|
|
26
|
+
function x402Fetch(opts) {
|
|
27
|
+
if (typeof opts?.pay !== 'function') {
|
|
28
|
+
throw new TypeError('x402Fetch: opts.pay must be a function');
|
|
29
|
+
}
|
|
30
|
+
const baseFetch = opts.fetch ?? globalThis.fetch;
|
|
31
|
+
if (typeof baseFetch !== 'function') {
|
|
32
|
+
throw new TypeError('x402Fetch: no fetch implementation available (Node ≥18 or pass opts.fetch)');
|
|
33
|
+
}
|
|
34
|
+
const maxRetries = opts.maxRetries ?? 1;
|
|
35
|
+
const selectFirst = (a) => a[0];
|
|
36
|
+
const select = opts.selectAccepts ?? selectFirst;
|
|
37
|
+
return async function paidFetch(input, init) {
|
|
38
|
+
let attemptsLeft = maxRetries;
|
|
39
|
+
// Contextual typing from FetchFn means we don't need to spell out
|
|
40
|
+
// RequestInfo / RequestInit explicitly — those are DOM-only globals.
|
|
41
|
+
let nextInit = init;
|
|
42
|
+
// Loop: original request + up-to-maxRetries payment retries.
|
|
43
|
+
// eslint-disable-next-line no-constant-condition
|
|
44
|
+
while (true) {
|
|
45
|
+
const res = await baseFetch(input, nextInit);
|
|
46
|
+
if (res.status !== 402 || attemptsLeft <= 0)
|
|
47
|
+
return res;
|
|
48
|
+
// Parse 402 body. If it's not a v2 PaymentRequirementsBody we
|
|
49
|
+
// bail with the original response (caller's problem to handle).
|
|
50
|
+
let body;
|
|
51
|
+
try {
|
|
52
|
+
body = (await res.clone().json());
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return res;
|
|
56
|
+
}
|
|
57
|
+
if (body?.x402Version !== 2 || !Array.isArray(body.accepts) || body.accepts.length === 0) {
|
|
58
|
+
return res;
|
|
59
|
+
}
|
|
60
|
+
const chosen = select(body.accepts);
|
|
61
|
+
if (!chosen)
|
|
62
|
+
return res;
|
|
63
|
+
const { signedTxCborHex, nonceRef } = await opts.pay(chosen);
|
|
64
|
+
const header = (0, envelope_1.encodePaymentEnvelope)({
|
|
65
|
+
network: chosen.network,
|
|
66
|
+
signedTxCborHex,
|
|
67
|
+
nonceRef,
|
|
68
|
+
});
|
|
69
|
+
// Merge PAYMENT-SIGNATURE into headers without mutating caller's init.
|
|
70
|
+
const mergedHeaders = new Headers(nextInit?.headers ?? init?.headers);
|
|
71
|
+
mergedHeaders.set('PAYMENT-SIGNATURE', header);
|
|
72
|
+
nextInit = { ...(nextInit ?? init ?? {}), headers: mergedHeaders };
|
|
73
|
+
attemptsLeft--;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-built `PayHandler` implementations.
|
|
3
|
+
*
|
|
4
|
+
* `createBridgePayHandler` is the default for server-to-server flows
|
|
5
|
+
* (one CAP service calling another, AI-agent payments). It uses the
|
|
6
|
+
* existing `buildUnsignedPaymentTx` helper — which requires
|
|
7
|
+
* `@odatano/core` bridge access at runtime — and delegates signing
|
|
8
|
+
* to a caller-supplied `signTx` callback.
|
|
9
|
+
*
|
|
10
|
+
* For browser CIP-30 wallets, write your own PayHandler: get UTxOs
|
|
11
|
+
* from `wallet.getUtxos()`, build the tx with browser CSL, sign via
|
|
12
|
+
* `wallet.signTx(cborHex, partialSign=true)` and merge the witness
|
|
13
|
+
* set. (See README "Custom PayHandler" section for an example.)
|
|
14
|
+
*/
|
|
15
|
+
import type { PayHandler } from './types';
|
|
16
|
+
export interface BridgePayHandlerOptions {
|
|
17
|
+
/** Buyer bech32 — used for UTxO lookup and change. */
|
|
18
|
+
buyerBech32: string;
|
|
19
|
+
/**
|
|
20
|
+
* Sign the unsigned tx CBOR. Returns the SIGNED tx CBOR hex
|
|
21
|
+
* (with the vkey witness set populated).
|
|
22
|
+
*
|
|
23
|
+
* For server-side raw-key signing: use CSL's
|
|
24
|
+
* `make_vkey_witness(txHash, privKey)` and attach it to the
|
|
25
|
+
* witness set, then serialize.
|
|
26
|
+
*/
|
|
27
|
+
signTx: (unsignedTxCborHex: string) => Promise<string>;
|
|
28
|
+
/** Forwarded to `buildUnsignedPaymentTx`. Default 1800 slots ≈ 30 min. */
|
|
29
|
+
ttlSlotsFromNow?: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build a `PayHandler` that runs the whole flow through `@odatano/core`:
|
|
33
|
+
* 1. `buildUnsignedPaymentTx` (UTxO selection + tx build)
|
|
34
|
+
* 2. caller-supplied `signTx` (signs the unsigned CBOR)
|
|
35
|
+
* 3. returns `{ signedTxCborHex, nonceRef }`
|
|
36
|
+
*
|
|
37
|
+
* The signed tx is NOT submitted here — the x402 server submits it
|
|
38
|
+
* after validating the envelope (per Cardano-x402-v2 facilitator flow).
|
|
39
|
+
*/
|
|
40
|
+
export declare function createBridgePayHandler(opts: BridgePayHandlerOptions): PayHandler;
|
|
41
|
+
//# sourceMappingURL=pay-handlers.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Pre-built `PayHandler` implementations.
|
|
4
|
+
*
|
|
5
|
+
* `createBridgePayHandler` is the default for server-to-server flows
|
|
6
|
+
* (one CAP service calling another, AI-agent payments). It uses the
|
|
7
|
+
* existing `buildUnsignedPaymentTx` helper — which requires
|
|
8
|
+
* `@odatano/core` bridge access at runtime — and delegates signing
|
|
9
|
+
* to a caller-supplied `signTx` callback.
|
|
10
|
+
*
|
|
11
|
+
* For browser CIP-30 wallets, write your own PayHandler: get UTxOs
|
|
12
|
+
* from `wallet.getUtxos()`, build the tx with browser CSL, sign via
|
|
13
|
+
* `wallet.signTx(cborHex, partialSign=true)` and merge the witness
|
|
14
|
+
* set. (See README "Custom PayHandler" section for an example.)
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.createBridgePayHandler = createBridgePayHandler;
|
|
18
|
+
const build_unsigned_tx_1 = require("../helpers/build-unsigned-tx");
|
|
19
|
+
/**
|
|
20
|
+
* Build a `PayHandler` that runs the whole flow through `@odatano/core`:
|
|
21
|
+
* 1. `buildUnsignedPaymentTx` (UTxO selection + tx build)
|
|
22
|
+
* 2. caller-supplied `signTx` (signs the unsigned CBOR)
|
|
23
|
+
* 3. returns `{ signedTxCborHex, nonceRef }`
|
|
24
|
+
*
|
|
25
|
+
* The signed tx is NOT submitted here — the x402 server submits it
|
|
26
|
+
* after validating the envelope (per Cardano-x402-v2 facilitator flow).
|
|
27
|
+
*/
|
|
28
|
+
function createBridgePayHandler(opts) {
|
|
29
|
+
if (!opts.buyerBech32) {
|
|
30
|
+
throw new TypeError('createBridgePayHandler: buyerBech32 is required');
|
|
31
|
+
}
|
|
32
|
+
if (typeof opts.signTx !== 'function') {
|
|
33
|
+
throw new TypeError('createBridgePayHandler: signTx must be a function');
|
|
34
|
+
}
|
|
35
|
+
return async function bridgePayHandler(requirement) {
|
|
36
|
+
const built = await (0, build_unsigned_tx_1.buildUnsignedPaymentTx)({
|
|
37
|
+
buyerBech32: opts.buyerBech32,
|
|
38
|
+
requirements: requirement,
|
|
39
|
+
ttlSlotsFromNow: opts.ttlSlotsFromNow,
|
|
40
|
+
});
|
|
41
|
+
const signedTxCborHex = await opts.signTx(built.unsignedTxCborHex);
|
|
42
|
+
if (typeof signedTxCborHex !== 'string' || signedTxCborHex.length === 0) {
|
|
43
|
+
throw new Error('createBridgePayHandler: signTx must resolve to a non-empty hex string');
|
|
44
|
+
}
|
|
45
|
+
return { signedTxCborHex, nonceRef: built.nonceRef };
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side helper types — symmetric to the server-side facilitator.
|
|
3
|
+
*
|
|
4
|
+
* A `PayHandler` is the one extension point: given the `accepts[]` entry
|
|
5
|
+
* the user chose, it must produce a signed payment tx (CBOR hex) + the
|
|
6
|
+
* v2 nonce reference. Everything else — 402-detection, retry loop,
|
|
7
|
+
* envelope encoding — is generic and lives in `fetch.ts` / `axios.ts`.
|
|
8
|
+
*/
|
|
9
|
+
import type { PaymentRequirementEntry } from '../core/types';
|
|
10
|
+
/**
|
|
11
|
+
* Signs/produces the payment for one `accepts[]` entry.
|
|
12
|
+
*
|
|
13
|
+
* Implementations: see `createBridgePayHandler` (server-to-server, uses
|
|
14
|
+
* `@odatano/core` to build + the caller-supplied `signTx` to sign), or
|
|
15
|
+
* write your own for browser CIP-30 wallets.
|
|
16
|
+
*/
|
|
17
|
+
export type PayHandler = (requirement: PaymentRequirementEntry) => Promise<PayHandlerResult>;
|
|
18
|
+
export interface PayHandlerResult {
|
|
19
|
+
/** Hex of the **signed** payment-tx CBOR (vkey witness set populated). */
|
|
20
|
+
signedTxCborHex: string;
|
|
21
|
+
/**
|
|
22
|
+
* v2 nonce reference `<txHash>#<outputIndex>` — must point to an
|
|
23
|
+
* unspent UTxO that ALSO appears as an input of the signed tx.
|
|
24
|
+
* (Server enforces both at validate time.)
|
|
25
|
+
*/
|
|
26
|
+
nonceRef: string;
|
|
27
|
+
}
|
|
28
|
+
/** Pick which `accepts[]` entry to satisfy. Default picks the first. */
|
|
29
|
+
export type AcceptsSelector = (accepts: PaymentRequirementEntry[]) => PaymentRequirementEntry | undefined;
|
|
30
|
+
export interface X402ClientOptions {
|
|
31
|
+
/** Required — how to produce the signed payment tx. */
|
|
32
|
+
pay: PayHandler;
|
|
33
|
+
/**
|
|
34
|
+
* Optional — choose one of the `accepts[]` entries when the server
|
|
35
|
+
* offers multiple. Defaults to `accepts[0]`.
|
|
36
|
+
*/
|
|
37
|
+
selectAccepts?: AcceptsSelector;
|
|
38
|
+
/**
|
|
39
|
+
* Maximum number of 402-driven payment retries per request. Default 1
|
|
40
|
+
* — i.e. one payment attempt per request, no infinite loops.
|
|
41
|
+
*/
|
|
42
|
+
maxRetries?: number;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Client-side helper types — symmetric to the server-side facilitator.
|
|
4
|
+
*
|
|
5
|
+
* A `PayHandler` is the one extension point: given the `accepts[]` entry
|
|
6
|
+
* the user chose, it must produce a signed payment tx (CBOR hex) + the
|
|
7
|
+
* v2 nonce reference. Everything else — 402-detection, retry loop,
|
|
8
|
+
* envelope encoding — is generic and lives in `fetch.ts` / `axios.ts`.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Facilitator adapter pattern.
|
|
3
|
+
*
|
|
4
|
+
* In v0.1 the verify+settle pipeline was hard-wired into the middlewares
|
|
5
|
+
* — they imported `process()` from `verify.ts` directly. That made it
|
|
6
|
+
* impossible to swap the in-process facilitator for a hosted one
|
|
7
|
+
* (the pattern Coinbase uses via `@coinbase/x402`).
|
|
8
|
+
*
|
|
9
|
+
* v0.2 introduces this `Facilitator` interface as the single
|
|
10
|
+
* extension point. Two implementations ship in-box:
|
|
11
|
+
*
|
|
12
|
+
* - `localFacilitator()` — runs verify+settle in-process via
|
|
13
|
+
* `@odatano/core`. Default everywhere.
|
|
14
|
+
* - `httpFacilitator()` — POSTs to a remote service (see
|
|
15
|
+
* `srv/facilitator/http.ts` for the wire
|
|
16
|
+
* format and `docs/facilitator-protocol.md`
|
|
17
|
+
* for the protocol reference).
|
|
18
|
+
*
|
|
19
|
+
* Consumers wire their choice into the middleware:
|
|
20
|
+
*
|
|
21
|
+
* x402Middleware({
|
|
22
|
+
* payTo, network, asset, priceUnits,
|
|
23
|
+
* facilitator: httpFacilitator({ url: 'https://...', apiKey }),
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* `verifyAndSettle` is the one mandatory operation — it covers the
|
|
27
|
+
* entire 1.decode → 2.validate → 3.nonce → 4.settle → 5.onAccepted
|
|
28
|
+
* pipeline. `supported()` is an optional discovery hook used by
|
|
29
|
+
* tooling / health checks (no middleware path consumes it yet).
|
|
30
|
+
*/
|
|
31
|
+
import type { ProcessResult } from './verify';
|
|
32
|
+
import type { AssetTransferMethod, PaymentClaim, PaymentRequirementsBody } from '../core/types';
|
|
33
|
+
export interface FacilitatorVerifyAndSettleArgs {
|
|
34
|
+
/** Raw `PAYMENT-SIGNATURE` header value (string, array or undefined). */
|
|
35
|
+
paymentHeader: string | string[] | undefined;
|
|
36
|
+
/** 402 body the validator checks the payment against (`accepts[0]`). */
|
|
37
|
+
requirementsBody: PaymentRequirementsBody;
|
|
38
|
+
/** Settle poll budget (ms). Default 60_000. */
|
|
39
|
+
settlePollBudgetMs?: number;
|
|
40
|
+
/** Allow txs without a validity-range upper bound. Default false. */
|
|
41
|
+
allowNoTtl?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Best-effort audit callback. Invoked exactly once on `accepted`.
|
|
44
|
+
* **Not transmittable over HTTP** — the http facilitator wrapper
|
|
45
|
+
* invokes it locally after the remote call returns.
|
|
46
|
+
*/
|
|
47
|
+
onAccepted?: (claim: PaymentClaim) => void | Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
/** Identical to the legacy `ProcessResult` — kept as a type alias for now. */
|
|
50
|
+
export type FacilitatorResult = ProcessResult;
|
|
51
|
+
/** Discovery response — what this facilitator can handle. */
|
|
52
|
+
export interface FacilitatorSupportedResult {
|
|
53
|
+
networks: string[];
|
|
54
|
+
assetTransferMethods: AssetTransferMethod[];
|
|
55
|
+
}
|
|
56
|
+
export interface Facilitator {
|
|
57
|
+
verifyAndSettle(args: FacilitatorVerifyAndSettleArgs): Promise<FacilitatorResult>;
|
|
58
|
+
/** Optional discovery hook. May be omitted by minimal facilitators. */
|
|
59
|
+
supported?(): Promise<FacilitatorSupportedResult>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Default in-process facilitator. Verify+settle runs locally using the
|
|
63
|
+
* `@odatano/core` bridge.
|
|
64
|
+
*
|
|
65
|
+
* Stateless — call `localFacilitator()` once per service (or inline per
|
|
66
|
+
* middleware mount); the returned object holds no per-instance state.
|
|
67
|
+
*/
|
|
68
|
+
export declare function localFacilitator(): Facilitator;
|
|
69
|
+
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Facilitator adapter pattern.
|
|
4
|
+
*
|
|
5
|
+
* In v0.1 the verify+settle pipeline was hard-wired into the middlewares
|
|
6
|
+
* — they imported `process()` from `verify.ts` directly. That made it
|
|
7
|
+
* impossible to swap the in-process facilitator for a hosted one
|
|
8
|
+
* (the pattern Coinbase uses via `@coinbase/x402`).
|
|
9
|
+
*
|
|
10
|
+
* v0.2 introduces this `Facilitator` interface as the single
|
|
11
|
+
* extension point. Two implementations ship in-box:
|
|
12
|
+
*
|
|
13
|
+
* - `localFacilitator()` — runs verify+settle in-process via
|
|
14
|
+
* `@odatano/core`. Default everywhere.
|
|
15
|
+
* - `httpFacilitator()` — POSTs to a remote service (see
|
|
16
|
+
* `srv/facilitator/http.ts` for the wire
|
|
17
|
+
* format and `docs/facilitator-protocol.md`
|
|
18
|
+
* for the protocol reference).
|
|
19
|
+
*
|
|
20
|
+
* Consumers wire their choice into the middleware:
|
|
21
|
+
*
|
|
22
|
+
* x402Middleware({
|
|
23
|
+
* payTo, network, asset, priceUnits,
|
|
24
|
+
* facilitator: httpFacilitator({ url: 'https://...', apiKey }),
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* `verifyAndSettle` is the one mandatory operation — it covers the
|
|
28
|
+
* entire 1.decode → 2.validate → 3.nonce → 4.settle → 5.onAccepted
|
|
29
|
+
* pipeline. `supported()` is an optional discovery hook used by
|
|
30
|
+
* tooling / health checks (no middleware path consumes it yet).
|
|
31
|
+
*/
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.localFacilitator = localFacilitator;
|
|
34
|
+
const verify_1 = require("./verify");
|
|
35
|
+
/**
|
|
36
|
+
* Default in-process facilitator. Verify+settle runs locally using the
|
|
37
|
+
* `@odatano/core` bridge.
|
|
38
|
+
*
|
|
39
|
+
* Stateless — call `localFacilitator()` once per service (or inline per
|
|
40
|
+
* middleware mount); the returned object holds no per-instance state.
|
|
41
|
+
*/
|
|
42
|
+
function localFacilitator() {
|
|
43
|
+
return {
|
|
44
|
+
verifyAndSettle: (args) => (0, verify_1.process)(args),
|
|
45
|
+
async supported() {
|
|
46
|
+
return {
|
|
47
|
+
networks: ['cardano:mainnet', 'cardano:preprod', 'cardano:preview'],
|
|
48
|
+
assetTransferMethods: ['default'],
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `httpFacilitator` — delegates verify+settle to a remote HTTP service.
|
|
3
|
+
*
|
|
4
|
+
* Wire format (see `docs/facilitator-protocol.md` for the full reference):
|
|
5
|
+
*
|
|
6
|
+
* POST <url>/verify-settle
|
|
7
|
+
* body: { paymentHeader, requirementsBody, settlePollBudgetMs?, allowNoTtl? }
|
|
8
|
+
* 200 → FacilitatorResult (accepted | rejected | pending)
|
|
9
|
+
* ≥400 → throws Error (the middleware translates to 500 to the buyer)
|
|
10
|
+
*
|
|
11
|
+
* GET <url>/supported
|
|
12
|
+
* 200 → { networks: string[], assetTransferMethods: string[] }
|
|
13
|
+
*
|
|
14
|
+
* Auth: optional `apiKey` sent as `Authorization: Bearer <key>`. For
|
|
15
|
+
* custom schemes (mTLS, OAuth, HMAC), pass a `headers()` builder.
|
|
16
|
+
*
|
|
17
|
+
* `onAccepted` cannot cross HTTP — the wrapper strips it from the wire
|
|
18
|
+
* payload and invokes it locally after the remote returns `accepted`.
|
|
19
|
+
* This preserves the local-facilitator semantics exactly.
|
|
20
|
+
*/
|
|
21
|
+
import type { Facilitator } from './adapter';
|
|
22
|
+
type FetchFn = typeof globalThis.fetch;
|
|
23
|
+
export interface HttpFacilitatorConfig {
|
|
24
|
+
/** Base URL of the remote facilitator (no trailing slash required). */
|
|
25
|
+
url: string;
|
|
26
|
+
/** Optional API key — sent as `Authorization: Bearer <apiKey>`. */
|
|
27
|
+
apiKey?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Optional custom header builder, merged onto the defaults. Use for
|
|
30
|
+
* mTLS, OAuth, signed-request auth, request IDs, etc.
|
|
31
|
+
*/
|
|
32
|
+
headers?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
33
|
+
/** Override the underlying fetch (testing, custom agents). */
|
|
34
|
+
fetch?: FetchFn;
|
|
35
|
+
/**
|
|
36
|
+
* Per-request timeout in ms. Default 90_000 — needs to be longer than
|
|
37
|
+
* the facilitator's settle-poll budget plus chain-confirmation latency.
|
|
38
|
+
*/
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
}
|
|
41
|
+
export declare function httpFacilitator(config: HttpFacilitatorConfig): Facilitator;
|
|
42
|
+
export {};
|
|
43
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `httpFacilitator` — delegates verify+settle to a remote HTTP service.
|
|
4
|
+
*
|
|
5
|
+
* Wire format (see `docs/facilitator-protocol.md` for the full reference):
|
|
6
|
+
*
|
|
7
|
+
* POST <url>/verify-settle
|
|
8
|
+
* body: { paymentHeader, requirementsBody, settlePollBudgetMs?, allowNoTtl? }
|
|
9
|
+
* 200 → FacilitatorResult (accepted | rejected | pending)
|
|
10
|
+
* ≥400 → throws Error (the middleware translates to 500 to the buyer)
|
|
11
|
+
*
|
|
12
|
+
* GET <url>/supported
|
|
13
|
+
* 200 → { networks: string[], assetTransferMethods: string[] }
|
|
14
|
+
*
|
|
15
|
+
* Auth: optional `apiKey` sent as `Authorization: Bearer <key>`. For
|
|
16
|
+
* custom schemes (mTLS, OAuth, HMAC), pass a `headers()` builder.
|
|
17
|
+
*
|
|
18
|
+
* `onAccepted` cannot cross HTTP — the wrapper strips it from the wire
|
|
19
|
+
* payload and invokes it locally after the remote returns `accepted`.
|
|
20
|
+
* This preserves the local-facilitator semantics exactly.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.httpFacilitator = httpFacilitator;
|
|
24
|
+
function trimTrailingSlash(url) {
|
|
25
|
+
return url.replace(/\/+$/, '');
|
|
26
|
+
}
|
|
27
|
+
function httpFacilitator(config) {
|
|
28
|
+
if (!config.url) {
|
|
29
|
+
throw new TypeError('httpFacilitator: url is required');
|
|
30
|
+
}
|
|
31
|
+
const baseFetch = config.fetch ?? globalThis.fetch;
|
|
32
|
+
if (typeof baseFetch !== 'function') {
|
|
33
|
+
throw new TypeError('httpFacilitator: no fetch implementation available (Node ≥18 or pass config.fetch)');
|
|
34
|
+
}
|
|
35
|
+
const timeoutMs = config.timeoutMs ?? 90_000;
|
|
36
|
+
const baseUrl = trimTrailingSlash(config.url);
|
|
37
|
+
async function buildHeaders(extra) {
|
|
38
|
+
const h = { 'content-type': 'application/json', ...extra };
|
|
39
|
+
if (config.apiKey)
|
|
40
|
+
h.authorization = `Bearer ${config.apiKey}`;
|
|
41
|
+
if (config.headers) {
|
|
42
|
+
const custom = await config.headers();
|
|
43
|
+
Object.assign(h, custom);
|
|
44
|
+
}
|
|
45
|
+
return h;
|
|
46
|
+
}
|
|
47
|
+
async function withTimeout(op) {
|
|
48
|
+
const ctrl = new AbortController();
|
|
49
|
+
const tid = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
50
|
+
try {
|
|
51
|
+
return await op(ctrl.signal);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
clearTimeout(tid);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
async verifyAndSettle(args) {
|
|
59
|
+
// Strip `onAccepted` — not transmittable. Invoke locally after the
|
|
60
|
+
// remote settles, preserving local-facilitator semantics.
|
|
61
|
+
const { onAccepted, ...wire } = args;
|
|
62
|
+
const result = await withTimeout(async (signal) => {
|
|
63
|
+
const res = await baseFetch(`${baseUrl}/verify-settle`, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: await buildHeaders(),
|
|
66
|
+
body: JSON.stringify(wire),
|
|
67
|
+
signal,
|
|
68
|
+
});
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
throw new Error(`httpFacilitator: POST /verify-settle returned ${res.status} ${res.statusText}`);
|
|
71
|
+
}
|
|
72
|
+
return (await res.json());
|
|
73
|
+
});
|
|
74
|
+
if (result.kind === 'accepted' && onAccepted) {
|
|
75
|
+
// Same best-effort semantics as the local facilitator —
|
|
76
|
+
// swallow errors so accepted payments are never lost to a
|
|
77
|
+
// failing audit callback.
|
|
78
|
+
try {
|
|
79
|
+
await onAccepted(result.payment);
|
|
80
|
+
}
|
|
81
|
+
catch { /* deliberately ignored — payment already on chain */ }
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
},
|
|
85
|
+
async supported() {
|
|
86
|
+
return withTimeout(async (signal) => {
|
|
87
|
+
const res = await baseFetch(`${baseUrl}/supported`, {
|
|
88
|
+
method: 'GET',
|
|
89
|
+
headers: await buildHeaders(),
|
|
90
|
+
signal,
|
|
91
|
+
});
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
throw new Error(`httpFacilitator: GET /supported returned ${res.status} ${res.statusText}`);
|
|
94
|
+
}
|
|
95
|
+
return (await res.json());
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
package/srv/index.d.ts
CHANGED
|
@@ -43,9 +43,16 @@ export type { AssetTransferMethod, ResourceDescriptor, PaymentRequirementEntry,
|
|
|
43
43
|
export { process as verifyPayment, type ProcessArgs, type ProcessResult, type ProcessKind, } from './facilitator/verify';
|
|
44
44
|
export { settle, type SettleArgs, type SettleResult } from './facilitator/settle';
|
|
45
45
|
export { checkNonceUnspent, type NonceCheckArgs, type NonceResult } from './facilitator/nonce';
|
|
46
|
+
export { localFacilitator, type Facilitator, type FacilitatorVerifyAndSettleArgs, type FacilitatorResult, type FacilitatorSupportedResult, } from './facilitator/adapter';
|
|
47
|
+
export { httpFacilitator, type HttpFacilitatorConfig, } from './facilitator/http';
|
|
46
48
|
export { verifyConfirmedPayment, type VerifyConfirmedArgs, type VerifyConfirmedResult, } from './helpers/verify-confirmed';
|
|
47
49
|
export { buildUnsignedPaymentTx, type BuildUnsignedTxArgs, type UnsignedTxResult, } from './helpers/build-unsigned-tx';
|
|
48
50
|
export { x402Middleware, type X402MiddlewareOptions } from './middleware/express';
|
|
49
51
|
export { gateService, type X402CapOptions } from './middleware/cap';
|
|
52
|
+
export { x402Fetch, type X402FetchOptions } from './client/fetch';
|
|
53
|
+
export { x402Axios } from './client/axios';
|
|
54
|
+
export { encodePaymentEnvelope, type EncodeEnvelopeArgs, } from './client/envelope';
|
|
55
|
+
export { createBridgePayHandler, type BridgePayHandlerOptions, } from './client/pay-handlers';
|
|
56
|
+
export type { PayHandler, PayHandlerResult, AcceptsSelector, X402ClientOptions, } from './client/types';
|
|
50
57
|
export * as bridge from './bridge';
|
|
51
58
|
//# sourceMappingURL=index.d.ts.map
|
package/srv/index.js
CHANGED
|
@@ -68,7 +68,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
68
68
|
};
|
|
69
69
|
})();
|
|
70
70
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
71
|
-
exports.bridge = exports.gateService = exports.x402Middleware = exports.buildUnsignedPaymentTx = exports.verifyConfirmedPayment = exports.checkNonceUnspent = exports.settle = exports.verifyPayment = exports.Codes = exports.X402Error = exports.networksMatch = exports.isNetwork = exports.parseNetwork = exports.buildAssetString = exports.parseAsset = exports.validatePayment = exports.decode = exports.flatRequirements = exports.buildEntry = exports.buildPaymentRequirements = void 0;
|
|
71
|
+
exports.bridge = exports.createBridgePayHandler = exports.encodePaymentEnvelope = exports.x402Axios = exports.x402Fetch = exports.gateService = exports.x402Middleware = exports.buildUnsignedPaymentTx = exports.verifyConfirmedPayment = exports.httpFacilitator = exports.localFacilitator = exports.checkNonceUnspent = exports.settle = exports.verifyPayment = exports.Codes = exports.X402Error = exports.networksMatch = exports.isNetwork = exports.parseNetwork = exports.buildAssetString = exports.parseAsset = exports.validatePayment = exports.decode = exports.flatRequirements = exports.buildEntry = exports.buildPaymentRequirements = void 0;
|
|
72
72
|
// ─── Core builders / validators (pure) ────────────────────────────────
|
|
73
73
|
var requirements_1 = require("./core/requirements");
|
|
74
74
|
Object.defineProperty(exports, "buildPaymentRequirements", { enumerable: true, get: function () { return requirements_1.buildPaymentRequirements; } });
|
|
@@ -97,6 +97,11 @@ var settle_1 = require("./facilitator/settle");
|
|
|
97
97
|
Object.defineProperty(exports, "settle", { enumerable: true, get: function () { return settle_1.settle; } });
|
|
98
98
|
var nonce_1 = require("./facilitator/nonce");
|
|
99
99
|
Object.defineProperty(exports, "checkNonceUnspent", { enumerable: true, get: function () { return nonce_1.checkNonceUnspent; } });
|
|
100
|
+
// ─── Facilitator adapter (pluggable local vs hosted) ──────────────────
|
|
101
|
+
var adapter_1 = require("./facilitator/adapter");
|
|
102
|
+
Object.defineProperty(exports, "localFacilitator", { enumerable: true, get: function () { return adapter_1.localFacilitator; } });
|
|
103
|
+
var http_1 = require("./facilitator/http");
|
|
104
|
+
Object.defineProperty(exports, "httpFacilitator", { enumerable: true, get: function () { return http_1.httpFacilitator; } });
|
|
100
105
|
// ─── Helpers ──────────────────────────────────────────────────────────
|
|
101
106
|
var verify_confirmed_1 = require("./helpers/verify-confirmed");
|
|
102
107
|
Object.defineProperty(exports, "verifyConfirmedPayment", { enumerable: true, get: function () { return verify_confirmed_1.verifyConfirmedPayment; } });
|
|
@@ -107,5 +112,14 @@ var express_1 = require("./middleware/express");
|
|
|
107
112
|
Object.defineProperty(exports, "x402Middleware", { enumerable: true, get: function () { return express_1.x402Middleware; } });
|
|
108
113
|
var cap_1 = require("./middleware/cap");
|
|
109
114
|
Object.defineProperty(exports, "gateService", { enumerable: true, get: function () { return cap_1.gateService; } });
|
|
115
|
+
// ─── Client (HTTP wrappers that auto-handle 402) ──────────────────────
|
|
116
|
+
var fetch_1 = require("./client/fetch");
|
|
117
|
+
Object.defineProperty(exports, "x402Fetch", { enumerable: true, get: function () { return fetch_1.x402Fetch; } });
|
|
118
|
+
var axios_1 = require("./client/axios");
|
|
119
|
+
Object.defineProperty(exports, "x402Axios", { enumerable: true, get: function () { return axios_1.x402Axios; } });
|
|
120
|
+
var envelope_1 = require("./client/envelope");
|
|
121
|
+
Object.defineProperty(exports, "encodePaymentEnvelope", { enumerable: true, get: function () { return envelope_1.encodePaymentEnvelope; } });
|
|
122
|
+
var pay_handlers_1 = require("./client/pay-handlers");
|
|
123
|
+
Object.defineProperty(exports, "createBridgePayHandler", { enumerable: true, get: function () { return pay_handlers_1.createBridgePayHandler; } });
|
|
110
124
|
// ─── Bridge (lower-level: exposed for advanced consumers) ─────────────
|
|
111
125
|
exports.bridge = __importStar(require("./bridge"));
|
package/srv/middleware/cap.d.ts
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* the request passes through unmodified.
|
|
26
26
|
*/
|
|
27
27
|
import cds from '@sap/cds';
|
|
28
|
+
import { type Facilitator } from '../facilitator/adapter';
|
|
28
29
|
import type { AssetTransferMethod, Network, PaymentClaim } from '../core/types';
|
|
29
30
|
export interface X402CapOptions {
|
|
30
31
|
payTo: string;
|
|
@@ -51,6 +52,12 @@ export interface X402CapOptions {
|
|
|
51
52
|
* builder to embed pair / entity id in the resource string.
|
|
52
53
|
*/
|
|
53
54
|
resourceUrl?: (req: cds.Request) => string;
|
|
55
|
+
/**
|
|
56
|
+
* Facilitator implementation handling verify+settle. Default
|
|
57
|
+
* `localFacilitator()` — in-process via `@odatano/core`. Use
|
|
58
|
+
* `httpFacilitator({ url, apiKey })` to delegate to a hosted service.
|
|
59
|
+
*/
|
|
60
|
+
facilitator?: Facilitator;
|
|
54
61
|
}
|
|
55
62
|
/**
|
|
56
63
|
* Attach the x402 gate to a CAP ApplicationService. Returns the service
|