@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/core/validate.d.ts
CHANGED
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
* Cardano-x402-v2:
|
|
6
6
|
*
|
|
7
7
|
* 1. Network validation
|
|
8
|
-
* 2. Recipient verification
|
|
9
|
-
* 3. Amount verification
|
|
10
|
-
* 4. Asset verification
|
|
8
|
+
* 2. Recipient verification , ≥1 output to payTo
|
|
9
|
+
* 3. Amount verification , sum of payTo outputs for asset ≥ required
|
|
10
|
+
* 4. Asset verification , exact policy + name match
|
|
11
11
|
* 5. Nonce / replay prevention
|
|
12
12
|
* - 5a. UTxO referenced by `payload.nonce` appears as a tx input
|
|
13
13
|
* - 5b. that UTxO is still unspent on chain ← chain-touching, lives in `nonce.ts`
|
|
14
|
-
* 6. TTL / expiry
|
|
14
|
+
* 6. TTL / expiry , tx.validity_range.upper_bound in future
|
|
15
15
|
*
|
|
16
16
|
* This module covers (1), (2), (3), (4), (5a) and (6). The chain-touching
|
|
17
|
-
* part of (5)
|
|
17
|
+
* part of (5), checking the UTxO is unspent, and (5b) live in
|
|
18
18
|
* `facilitator/nonce.ts` and run after this. We also keep a sanity guard
|
|
19
19
|
* for "no vkey witnesses" so an unsigned CBOR is rejected with a precise
|
|
20
20
|
* code rather than blowing up at submit time.
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* Pure function. No I/O.
|
|
23
23
|
*/
|
|
24
24
|
import { type X402Code } from './errors';
|
|
25
|
-
import type { DecodedPayment, PaymentRequirementEntry, PaymentClaim } from './types';
|
|
25
|
+
import type { DecodedPayment, PaymentRequirementEntry, PaymentRequirementsBody, PaymentClaim } from './types';
|
|
26
26
|
export type ValidationResult = {
|
|
27
27
|
ok: true;
|
|
28
28
|
claim: PaymentClaim;
|
|
@@ -36,10 +36,34 @@ export interface ValidateOptions {
|
|
|
36
36
|
currentSlot: number;
|
|
37
37
|
/**
|
|
38
38
|
* If true, allow tx with no `ttl()` set (validity-range upper bound
|
|
39
|
-
* absent). Default false
|
|
39
|
+
* absent). Default false, v2 spec recommends a TTL. Callers that
|
|
40
40
|
* want to accept no-TTL txs (e.g. legacy wallets) opt-in.
|
|
41
41
|
*/
|
|
42
42
|
allowNoTtl?: boolean;
|
|
43
43
|
}
|
|
44
44
|
export declare function validatePayment(decoded: DecodedPayment, requirements: PaymentRequirementEntry, opts: ValidateOptions): ValidationResult;
|
|
45
|
+
export type PickResult = {
|
|
46
|
+
ok: true;
|
|
47
|
+
entry: PaymentRequirementEntry;
|
|
48
|
+
} | {
|
|
49
|
+
ok: false;
|
|
50
|
+
code: X402Code;
|
|
51
|
+
reason: string;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Select the `accepts[]` entry the buyer actually paid against.
|
|
55
|
+
*
|
|
56
|
+
* Algorithm (first-match wins, accepts[] is the seller's preference order):
|
|
57
|
+
* 1. Filter by network , the buyer's envelope declares one network and
|
|
58
|
+
* we discard entries that don't match.
|
|
59
|
+
* 2. For each remaining entry, look for an output to `entry.payTo`
|
|
60
|
+
* containing `entry.asset` with quantity ≥ 1. We don't enforce the
|
|
61
|
+
* full `amount` here, that's `validatePayment`'s job, we just need
|
|
62
|
+
* enough signal to know *which* entry the buyer chose.
|
|
63
|
+
* 3. First hit wins. No hit ⇒ `wrong_asset` with a composite reason.
|
|
64
|
+
*
|
|
65
|
+
* For a single-entry `accepts[]`, this returns `accepts[0]` if the
|
|
66
|
+
* network matches, mirroring the pre-multi-accept behaviour exactly.
|
|
67
|
+
*/
|
|
68
|
+
export declare function pickRequirement(decoded: DecodedPayment, body: PaymentRequirementsBody): PickResult;
|
|
45
69
|
//# sourceMappingURL=validate.d.ts.map
|
package/srv/core/validate.js
CHANGED
|
@@ -6,16 +6,16 @@
|
|
|
6
6
|
* Cardano-x402-v2:
|
|
7
7
|
*
|
|
8
8
|
* 1. Network validation
|
|
9
|
-
* 2. Recipient verification
|
|
10
|
-
* 3. Amount verification
|
|
11
|
-
* 4. Asset verification
|
|
9
|
+
* 2. Recipient verification , ≥1 output to payTo
|
|
10
|
+
* 3. Amount verification , sum of payTo outputs for asset ≥ required
|
|
11
|
+
* 4. Asset verification , exact policy + name match
|
|
12
12
|
* 5. Nonce / replay prevention
|
|
13
13
|
* - 5a. UTxO referenced by `payload.nonce` appears as a tx input
|
|
14
14
|
* - 5b. that UTxO is still unspent on chain ← chain-touching, lives in `nonce.ts`
|
|
15
|
-
* 6. TTL / expiry
|
|
15
|
+
* 6. TTL / expiry , tx.validity_range.upper_bound in future
|
|
16
16
|
*
|
|
17
17
|
* This module covers (1), (2), (3), (4), (5a) and (6). The chain-touching
|
|
18
|
-
* part of (5)
|
|
18
|
+
* part of (5), checking the UTxO is unspent, and (5b) live in
|
|
19
19
|
* `facilitator/nonce.ts` and run after this. We also keep a sanity guard
|
|
20
20
|
* for "no vkey witnesses" so an unsigned CBOR is rejected with a precise
|
|
21
21
|
* code rather than blowing up at submit time.
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
26
|
exports.validatePayment = validatePayment;
|
|
27
|
+
exports.pickRequirement = pickRequirement;
|
|
27
28
|
const errors_1 = require("./errors");
|
|
28
29
|
const network_1 = require("./network");
|
|
29
30
|
const asset_1 = require("./asset");
|
|
@@ -69,7 +70,7 @@ function validatePayment(decoded, requirements, opts) {
|
|
|
69
70
|
reason: `payment network '${decoded.envelope.network}' does not match requirements '${requirements.network}'`,
|
|
70
71
|
};
|
|
71
72
|
}
|
|
72
|
-
// Parse the asset string once
|
|
73
|
+
// Parse the asset string once, also normalises the requirement's
|
|
73
74
|
// unit key for output comparison.
|
|
74
75
|
const parsed = (0, asset_1.parseAsset)(requirements.asset);
|
|
75
76
|
const unit = parsed.unit; // empty when lovelace; checks short-circuit via isLovelace
|
|
@@ -101,7 +102,7 @@ function validatePayment(decoded, requirements, opts) {
|
|
|
101
102
|
};
|
|
102
103
|
}
|
|
103
104
|
// ─── Check 5a: nonce UTxO appears in tx inputs ─────────────────────
|
|
104
|
-
// (5b
|
|
105
|
+
// (5b, UTxO is unspent, runs in facilitator/nonce.ts after we've
|
|
105
106
|
// confirmed the buyer's structural intent here.)
|
|
106
107
|
const nonceInInputs = decoded.inputs.some(i => i.txHash === decoded.nonce.txHash && i.outputIndex === decoded.nonce.index);
|
|
107
108
|
if (!nonceInInputs) {
|
|
@@ -113,7 +114,7 @@ function validatePayment(decoded, requirements, opts) {
|
|
|
113
114
|
}
|
|
114
115
|
// ─── Check 6: TTL / expiry ─────────────────────────────────────────
|
|
115
116
|
// Slot semantics: `ttl_bignum` is the FIRST slot at which the tx is
|
|
116
|
-
// INVALID
|
|
117
|
+
// INVALID, so the tx must be submitted before that slot. We require
|
|
117
118
|
// `currentSlot < ttlSlot`; equality means the window just closed.
|
|
118
119
|
if (decoded.ttlSlot === null) {
|
|
119
120
|
if (!opts.allowNoTtl) {
|
|
@@ -132,7 +133,7 @@ function validatePayment(decoded, requirements, opts) {
|
|
|
132
133
|
};
|
|
133
134
|
}
|
|
134
135
|
// ─── All structural checks pass ────────────────────────────────────
|
|
135
|
-
// `payerAddr` is intentionally omitted here
|
|
136
|
+
// `payerAddr` is intentionally omitted here, we don't have the
|
|
136
137
|
// buyer's input addresses without resolving the referenced UTxOs.
|
|
137
138
|
// The facilitator can fill it in via `bridge.getTransactionByHash`
|
|
138
139
|
// on the nonce input, if the caller cares for audit purposes.
|
|
@@ -145,8 +146,82 @@ function validatePayment(decoded, requirements, opts) {
|
|
|
145
146
|
network,
|
|
146
147
|
unit,
|
|
147
148
|
asset: requirements.asset,
|
|
149
|
+
payTo: requirements.payTo,
|
|
148
150
|
resourceUrl: requirements.resource.url,
|
|
149
151
|
nonceRef: `${decoded.nonce.txHash}#${decoded.nonce.index}`,
|
|
150
152
|
},
|
|
151
153
|
};
|
|
152
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Select the `accepts[]` entry the buyer actually paid against.
|
|
157
|
+
*
|
|
158
|
+
* Algorithm (first-match wins, accepts[] is the seller's preference order):
|
|
159
|
+
* 1. Filter by network , the buyer's envelope declares one network and
|
|
160
|
+
* we discard entries that don't match.
|
|
161
|
+
* 2. For each remaining entry, look for an output to `entry.payTo`
|
|
162
|
+
* containing `entry.asset` with quantity ≥ 1. We don't enforce the
|
|
163
|
+
* full `amount` here, that's `validatePayment`'s job, we just need
|
|
164
|
+
* enough signal to know *which* entry the buyer chose.
|
|
165
|
+
* 3. First hit wins. No hit ⇒ `wrong_asset` with a composite reason.
|
|
166
|
+
*
|
|
167
|
+
* For a single-entry `accepts[]`, this returns `accepts[0]` if the
|
|
168
|
+
* network matches, mirroring the pre-multi-accept behaviour exactly.
|
|
169
|
+
*/
|
|
170
|
+
function pickRequirement(decoded, body) {
|
|
171
|
+
if (!body.accepts || body.accepts.length === 0) {
|
|
172
|
+
return { ok: false, code: errors_1.Codes.WRONG_ASSET, reason: 'PaymentRequirementsBody.accepts is empty' };
|
|
173
|
+
}
|
|
174
|
+
const networkOk = body.accepts.filter((e) => (0, network_1.networksMatch)(decoded.envelope.network, e.network));
|
|
175
|
+
if (networkOk.length === 0) {
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
code: errors_1.Codes.NETWORK_MISMATCH,
|
|
179
|
+
reason: `payment network '${decoded.envelope.network}' does not match any accepts[].network`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
// Single-entry shortcut: hand back the lone matching entry, the
|
|
183
|
+
// subsequent validatePayment will run the strict per-entry checks
|
|
184
|
+
// (recipient, amount, asset, ...) just as it did pre-multi-accept.
|
|
185
|
+
if (networkOk.length === 1) {
|
|
186
|
+
return { ok: true, entry: networkOk[0] };
|
|
187
|
+
}
|
|
188
|
+
// Non-lovelace assets are an unambiguous signal: an output that
|
|
189
|
+
// carries policy X is paying in policy X. Lovelace is ambiguous,
|
|
190
|
+
// EVERY native-asset output to the seller also carries min-ADA in
|
|
191
|
+
// lovelace just to be valid on chain. So we pass over the accepts[]
|
|
192
|
+
// in two passes:
|
|
193
|
+
// - Pass 1: match any entry whose non-lovelace asset appears in a
|
|
194
|
+
// payTo output. Strong signal, accept immediately.
|
|
195
|
+
// - Pass 2: only if no native-asset entry matched, consider
|
|
196
|
+
// lovelace entries, and only against outputs that are pure ADA
|
|
197
|
+
// (no native assets). That avoids charging a USDM-paying buyer
|
|
198
|
+
// against a lovelace entry just because the min-ADA happened to
|
|
199
|
+
// be in the output.
|
|
200
|
+
for (const entry of networkOk) {
|
|
201
|
+
const parsed = (0, asset_1.parseAsset)(entry.asset);
|
|
202
|
+
if (parsed.isLovelace)
|
|
203
|
+
continue;
|
|
204
|
+
const hit = decoded.outputs.some((o) => o.address === entry.payTo && o.assets.some((a) => a.unit === parsed.unit));
|
|
205
|
+
if (hit)
|
|
206
|
+
return { ok: true, entry };
|
|
207
|
+
}
|
|
208
|
+
for (const entry of networkOk) {
|
|
209
|
+
const parsed = (0, asset_1.parseAsset)(entry.asset);
|
|
210
|
+
if (!parsed.isLovelace)
|
|
211
|
+
continue;
|
|
212
|
+
const hit = decoded.outputs.some((o) => o.address === entry.payTo && o.assets.length === 0 && BigInt(o.lovelace) > 0n);
|
|
213
|
+
if (hit)
|
|
214
|
+
return { ok: true, entry };
|
|
215
|
+
}
|
|
216
|
+
// No entry matched. Compose a reason listing the (payTo, asset) pairs
|
|
217
|
+
// we expected, so the buyer's client can diagnose which option they
|
|
218
|
+
// tried to pay vs. what the server offered.
|
|
219
|
+
const expected = networkOk
|
|
220
|
+
.map((e) => `${e.asset}@${e.payTo}`)
|
|
221
|
+
.join(' | ');
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
code: errors_1.Codes.WRONG_ASSET,
|
|
225
|
+
reason: `no accepts[] entry matched the paid (payTo, asset); expected one of: ${expected}`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
* Facilitator adapter pattern.
|
|
3
3
|
*
|
|
4
4
|
* In v0.1 the verify+settle pipeline was hard-wired into the middlewares
|
|
5
|
-
|
|
5
|
+
*, they imported `process()` from `verify.ts` directly. That made it
|
|
6
6
|
* impossible to swap the in-process facilitator for a hosted one
|
|
7
7
|
* (the pattern Coinbase uses via `@coinbase/x402`).
|
|
8
8
|
*
|
|
9
9
|
* v0.2 introduces this `Facilitator` interface as the single
|
|
10
10
|
* extension point. Two implementations ship in-box:
|
|
11
11
|
*
|
|
12
|
-
* - `localFacilitator()`
|
|
12
|
+
* - `localFacilitator()` , runs verify+settle in-process via
|
|
13
13
|
* `@odatano/core`. Default everywhere.
|
|
14
|
-
* - `httpFacilitator()`
|
|
14
|
+
* - `httpFacilitator()` , POSTs to a remote service (see
|
|
15
15
|
* `srv/facilitator/http.ts` for the wire
|
|
16
16
|
* format and `docs/facilitator-protocol.md`
|
|
17
17
|
* for the protocol reference).
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* facilitator: httpFacilitator({ url: 'https://...', apiKey }),
|
|
24
24
|
* });
|
|
25
25
|
*
|
|
26
|
-
* `verifyAndSettle` is the one mandatory operation
|
|
26
|
+
* `verifyAndSettle` is the one mandatory operation, it covers the
|
|
27
27
|
* entire 1.decode → 2.validate → 3.nonce → 4.settle → 5.onAccepted
|
|
28
28
|
* pipeline. `supported()` is an optional discovery hook used by
|
|
29
29
|
* tooling / health checks (no middleware path consumes it yet).
|
|
@@ -41,14 +41,14 @@ export interface FacilitatorVerifyAndSettleArgs {
|
|
|
41
41
|
allowNoTtl?: boolean;
|
|
42
42
|
/**
|
|
43
43
|
* Best-effort audit callback. Invoked exactly once on `accepted`.
|
|
44
|
-
* **Not transmittable over HTTP
|
|
44
|
+
* **Not transmittable over HTTP**, the http facilitator wrapper
|
|
45
45
|
* invokes it locally after the remote call returns.
|
|
46
46
|
*/
|
|
47
47
|
onAccepted?: (claim: PaymentClaim) => void | Promise<void>;
|
|
48
48
|
}
|
|
49
|
-
/** Identical to the legacy `ProcessResult
|
|
49
|
+
/** Identical to the legacy `ProcessResult`, kept as a type alias for now. */
|
|
50
50
|
export type FacilitatorResult = ProcessResult;
|
|
51
|
-
/** Discovery response
|
|
51
|
+
/** Discovery response, what this facilitator can handle. */
|
|
52
52
|
export interface FacilitatorSupportedResult {
|
|
53
53
|
networks: string[];
|
|
54
54
|
assetTransferMethods: AssetTransferMethod[];
|
|
@@ -62,7 +62,7 @@ export interface Facilitator {
|
|
|
62
62
|
* Default in-process facilitator. Verify+settle runs locally using the
|
|
63
63
|
* `@odatano/core` bridge.
|
|
64
64
|
*
|
|
65
|
-
* Stateless
|
|
65
|
+
* Stateless, call `localFacilitator()` once per service (or inline per
|
|
66
66
|
* middleware mount); the returned object holds no per-instance state.
|
|
67
67
|
*/
|
|
68
68
|
export declare function localFacilitator(): Facilitator;
|
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
* Facilitator adapter pattern.
|
|
4
4
|
*
|
|
5
5
|
* In v0.1 the verify+settle pipeline was hard-wired into the middlewares
|
|
6
|
-
|
|
6
|
+
*, they imported `process()` from `verify.ts` directly. That made it
|
|
7
7
|
* impossible to swap the in-process facilitator for a hosted one
|
|
8
8
|
* (the pattern Coinbase uses via `@coinbase/x402`).
|
|
9
9
|
*
|
|
10
10
|
* v0.2 introduces this `Facilitator` interface as the single
|
|
11
11
|
* extension point. Two implementations ship in-box:
|
|
12
12
|
*
|
|
13
|
-
* - `localFacilitator()`
|
|
13
|
+
* - `localFacilitator()` , runs verify+settle in-process via
|
|
14
14
|
* `@odatano/core`. Default everywhere.
|
|
15
|
-
* - `httpFacilitator()`
|
|
15
|
+
* - `httpFacilitator()` , POSTs to a remote service (see
|
|
16
16
|
* `srv/facilitator/http.ts` for the wire
|
|
17
17
|
* format and `docs/facilitator-protocol.md`
|
|
18
18
|
* for the protocol reference).
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* facilitator: httpFacilitator({ url: 'https://...', apiKey }),
|
|
25
25
|
* });
|
|
26
26
|
*
|
|
27
|
-
* `verifyAndSettle` is the one mandatory operation
|
|
27
|
+
* `verifyAndSettle` is the one mandatory operation, it covers the
|
|
28
28
|
* entire 1.decode → 2.validate → 3.nonce → 4.settle → 5.onAccepted
|
|
29
29
|
* pipeline. `supported()` is an optional discovery hook used by
|
|
30
30
|
* tooling / health checks (no middleware path consumes it yet).
|
|
@@ -36,7 +36,7 @@ const verify_1 = require("./verify");
|
|
|
36
36
|
* Default in-process facilitator. Verify+settle runs locally using the
|
|
37
37
|
* `@odatano/core` bridge.
|
|
38
38
|
*
|
|
39
|
-
* Stateless
|
|
39
|
+
* Stateless, call `localFacilitator()` once per service (or inline per
|
|
40
40
|
* middleware mount); the returned object holds no per-instance state.
|
|
41
41
|
*/
|
|
42
42
|
function localFacilitator() {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `httpFacilitator
|
|
2
|
+
* `httpFacilitator`, delegates verify+settle to a remote HTTP service.
|
|
3
3
|
*
|
|
4
4
|
* Wire format (see `docs/facilitator-protocol.md` for the full reference):
|
|
5
5
|
*
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Auth: optional `apiKey` sent as `Authorization: Bearer <key>`. For
|
|
15
15
|
* custom schemes (mTLS, OAuth, HMAC), pass a `headers()` builder.
|
|
16
16
|
*
|
|
17
|
-
* `onAccepted` cannot cross HTTP
|
|
17
|
+
* `onAccepted` cannot cross HTTP, the wrapper strips it from the wire
|
|
18
18
|
* payload and invokes it locally after the remote returns `accepted`.
|
|
19
19
|
* This preserves the local-facilitator semantics exactly.
|
|
20
20
|
*/
|
|
@@ -23,7 +23,7 @@ type FetchFn = typeof globalThis.fetch;
|
|
|
23
23
|
export interface HttpFacilitatorConfig {
|
|
24
24
|
/** Base URL of the remote facilitator (no trailing slash required). */
|
|
25
25
|
url: string;
|
|
26
|
-
/** Optional API key
|
|
26
|
+
/** Optional API key, sent as `Authorization: Bearer <apiKey>`. */
|
|
27
27
|
apiKey?: string;
|
|
28
28
|
/**
|
|
29
29
|
* Optional custom header builder, merged onto the defaults. Use for
|
|
@@ -33,7 +33,7 @@ export interface HttpFacilitatorConfig {
|
|
|
33
33
|
/** Override the underlying fetch (testing, custom agents). */
|
|
34
34
|
fetch?: FetchFn;
|
|
35
35
|
/**
|
|
36
|
-
* Per-request timeout in ms. Default 90_000
|
|
36
|
+
* Per-request timeout in ms. Default 90_000, needs to be longer than
|
|
37
37
|
* the facilitator's settle-poll budget plus chain-confirmation latency.
|
|
38
38
|
*/
|
|
39
39
|
timeoutMs?: number;
|
package/srv/facilitator/http.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* `httpFacilitator
|
|
3
|
+
* `httpFacilitator`, delegates verify+settle to a remote HTTP service.
|
|
4
4
|
*
|
|
5
5
|
* Wire format (see `docs/facilitator-protocol.md` for the full reference):
|
|
6
6
|
*
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* Auth: optional `apiKey` sent as `Authorization: Bearer <key>`. For
|
|
16
16
|
* custom schemes (mTLS, OAuth, HMAC), pass a `headers()` builder.
|
|
17
17
|
*
|
|
18
|
-
* `onAccepted` cannot cross HTTP
|
|
18
|
+
* `onAccepted` cannot cross HTTP, the wrapper strips it from the wire
|
|
19
19
|
* payload and invokes it locally after the remote returns `accepted`.
|
|
20
20
|
* This preserves the local-facilitator semantics exactly.
|
|
21
21
|
*/
|
|
@@ -56,7 +56,7 @@ function httpFacilitator(config) {
|
|
|
56
56
|
}
|
|
57
57
|
return {
|
|
58
58
|
async verifyAndSettle(args) {
|
|
59
|
-
// Strip `onAccepted
|
|
59
|
+
// Strip `onAccepted`, not transmittable. Invoke locally after the
|
|
60
60
|
// remote settles, preserving local-facilitator semantics.
|
|
61
61
|
const { onAccepted, ...wire } = args;
|
|
62
62
|
const result = await withTimeout(async (signal) => {
|
|
@@ -72,13 +72,13 @@ function httpFacilitator(config) {
|
|
|
72
72
|
return (await res.json());
|
|
73
73
|
});
|
|
74
74
|
if (result.kind === 'accepted' && onAccepted) {
|
|
75
|
-
// Same best-effort semantics as the local facilitator
|
|
75
|
+
// Same best-effort semantics as the local facilitator ,
|
|
76
76
|
// swallow errors so accepted payments are never lost to a
|
|
77
77
|
// failing audit callback.
|
|
78
78
|
try {
|
|
79
79
|
await onAccepted(result.payment);
|
|
80
80
|
}
|
|
81
|
-
catch { /* deliberately ignored
|
|
81
|
+
catch { /* deliberately ignored, payment already on chain */ }
|
|
82
82
|
}
|
|
83
83
|
return result;
|
|
84
84
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cardano-x402-v2 replay-defense check (mandatory check #5).
|
|
3
3
|
*
|
|
4
|
-
* v1 had a CDS entity `X402PaymentNonces` with a UNIQUE on txHash
|
|
4
|
+
* v1 had a CDS entity `X402PaymentNonces` with a UNIQUE on txHash ,
|
|
5
5
|
* replay defense was a DB UNIQUE-constraint race. v2 moves replay
|
|
6
6
|
* defense **on-chain**: the buyer references a specific UTxO in the
|
|
7
7
|
* envelope (`payload.nonce = "<txHash>#<index>"`), that UTxO must
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
* UTxO is permanently consumed. No DB table needed.
|
|
10
10
|
*
|
|
11
11
|
* Check #5 has two parts:
|
|
12
|
-
* - 5a
|
|
13
|
-
* - 5b
|
|
12
|
+
* - 5a, the nonce UTxO appears in the tx inputs (in validate.ts, pure)
|
|
13
|
+
* - 5b, the nonce UTxO is still unspent on chain (here, chain-touching)
|
|
14
14
|
*
|
|
15
15
|
* Order in the pipeline: `validate.ts` (which runs 5a) MUST run before
|
|
16
16
|
* `checkNonceUnspent` here. The chain-touching call below is a single
|
|
17
17
|
* `bridge.isUtxoUnspent` round-trip, backed by Blockfrost `consumed_by`
|
|
18
18
|
* / Koios `is_spent` / Ogmios `queryLedgerState/utxo`. Spent and
|
|
19
|
-
* nonexistent UTxOs both surface as `false
|
|
19
|
+
* nonexistent UTxOs both surface as `false`, both translate to REPLAY.
|
|
20
20
|
*/
|
|
21
21
|
import { type X402Code } from '../core/errors';
|
|
22
22
|
export interface NonceCheckArgs {
|
package/srv/facilitator/nonce.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Cardano-x402-v2 replay-defense check (mandatory check #5).
|
|
4
4
|
*
|
|
5
|
-
* v1 had a CDS entity `X402PaymentNonces` with a UNIQUE on txHash
|
|
5
|
+
* v1 had a CDS entity `X402PaymentNonces` with a UNIQUE on txHash ,
|
|
6
6
|
* replay defense was a DB UNIQUE-constraint race. v2 moves replay
|
|
7
7
|
* defense **on-chain**: the buyer references a specific UTxO in the
|
|
8
8
|
* envelope (`payload.nonce = "<txHash>#<index>"`), that UTxO must
|
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
* UTxO is permanently consumed. No DB table needed.
|
|
11
11
|
*
|
|
12
12
|
* Check #5 has two parts:
|
|
13
|
-
* - 5a
|
|
14
|
-
* - 5b
|
|
13
|
+
* - 5a, the nonce UTxO appears in the tx inputs (in validate.ts, pure)
|
|
14
|
+
* - 5b, the nonce UTxO is still unspent on chain (here, chain-touching)
|
|
15
15
|
*
|
|
16
16
|
* Order in the pipeline: `validate.ts` (which runs 5a) MUST run before
|
|
17
17
|
* `checkNonceUnspent` here. The chain-touching call below is a single
|
|
18
18
|
* `bridge.isUtxoUnspent` round-trip, backed by Blockfrost `consumed_by`
|
|
19
19
|
* / Koios `is_spent` / Ogmios `queryLedgerState/utxo`. Spent and
|
|
20
|
-
* nonexistent UTxOs both surface as `false
|
|
20
|
+
* nonexistent UTxOs both surface as `false`, both translate to REPLAY.
|
|
21
21
|
*/
|
|
22
22
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
23
23
|
if (k2 === undefined) k2 = k;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference HTTP facilitator, the server side of `httpFacilitator()`.
|
|
3
|
+
*
|
|
4
|
+
* `httpFacilitator()` (in `./http.ts`) is the client wrapper that resource
|
|
5
|
+
* servers wire into their middleware via the `facilitator` option. This
|
|
6
|
+
* module is the matching server: a thin Express `Router` exposing the
|
|
7
|
+
* three endpoints documented in `docs/facilitator-protocol.md`:
|
|
8
|
+
*
|
|
9
|
+
* POST /verify-settle , runs the full pipeline through a `Facilitator`
|
|
10
|
+
* GET /supported , discovery
|
|
11
|
+
* GET /healthz , orchestrator health check
|
|
12
|
+
*
|
|
13
|
+
* The router is composable, consumers mount it under whatever path /
|
|
14
|
+
* port / TLS / CORS / rate-limiter they prefer, no opinions baked in.
|
|
15
|
+
* Auth is a single `auth(req)` hook, callers can implement bearer-token,
|
|
16
|
+
* mTLS, signed-request, OAuth, etc. There is NO default auth, an
|
|
17
|
+
* unconfigured router is open; document this loudly in deployment.
|
|
18
|
+
*
|
|
19
|
+
* The `onAccepted` callback CANNOT cross the HTTP boundary, the matching
|
|
20
|
+
* client (`httpFacilitator()`) invokes it locally after the remote returns
|
|
21
|
+
* `accepted`. To cover the facilitator-side audit need, this router
|
|
22
|
+
* exposes `onRejected` and `onPending` hooks, fired exactly once per
|
|
23
|
+
* non-accepted outcome, with the request available for context.
|
|
24
|
+
*/
|
|
25
|
+
import { type Router, type Request } from 'express';
|
|
26
|
+
import type { Facilitator, FacilitatorResult } from './adapter';
|
|
27
|
+
export interface FacilitatorServerLogger {
|
|
28
|
+
warn(message: string, err?: unknown): void;
|
|
29
|
+
error(message: string, err?: unknown): void;
|
|
30
|
+
}
|
|
31
|
+
export interface CreateFacilitatorRouterOptions {
|
|
32
|
+
/**
|
|
33
|
+
* Facilitator implementation. Default `localFacilitator()`, runs the
|
|
34
|
+
* pipeline in-process via `@odatano/core`. Swap in a custom adapter
|
|
35
|
+
* for testing or for chained facilitators.
|
|
36
|
+
*/
|
|
37
|
+
facilitator?: Facilitator;
|
|
38
|
+
/**
|
|
39
|
+
* Optional auth gate. Returns truthy to allow, falsy to reject with
|
|
40
|
+
* 401. Thrown errors are treated as 500. There is no default, an
|
|
41
|
+
* unset hook means the router is open; configure in production.
|
|
42
|
+
*/
|
|
43
|
+
auth?: (req: Request) => boolean | Promise<boolean>;
|
|
44
|
+
/**
|
|
45
|
+
* Body-size limit for `POST /verify-settle`. Default `'256kb'`,
|
|
46
|
+
* envelopes are ≤ ~50 kB in practice. Format matches `express.json`.
|
|
47
|
+
*/
|
|
48
|
+
jsonLimit?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Best-effort audit hook fired exactly once per `rejected` outcome,
|
|
51
|
+
* AFTER the response is sent. Errors are swallowed and logged.
|
|
52
|
+
* `onAccepted` cannot cross HTTP, this is the rejected-side analog.
|
|
53
|
+
*/
|
|
54
|
+
onRejected?: (result: Extract<FacilitatorResult, {
|
|
55
|
+
kind: 'rejected';
|
|
56
|
+
}>, req: Request) => void | Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Best-effort audit hook fired exactly once per `pending` outcome,
|
|
59
|
+
* AFTER the response is sent. Same semantics as `onRejected`.
|
|
60
|
+
*/
|
|
61
|
+
onPending?: (result: Extract<FacilitatorResult, {
|
|
62
|
+
kind: 'pending';
|
|
63
|
+
}>, req: Request) => void | Promise<void>;
|
|
64
|
+
/** Custom logger. Default uses `cds.log('x402')`. */
|
|
65
|
+
logger?: FacilitatorServerLogger;
|
|
66
|
+
}
|
|
67
|
+
export declare function createFacilitatorRouter(opts?: CreateFacilitatorRouterOptions): Router;
|
|
68
|
+
//# sourceMappingURL=server.d.ts.map
|