@odatano/x402 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +5 -0
  3. package/cds-plugin.js +2 -0
  4. package/db/x402-grants.cds +49 -0
  5. package/db/x402-receipts.cds +44 -0
  6. package/package.json +4 -3
  7. package/srv/bridge.d.ts +9 -12
  8. package/srv/bridge.js +10 -13
  9. package/srv/client/axios.d.ts +1 -1
  10. package/srv/client/axios.js +47 -8
  11. package/srv/client/envelope.d.ts +1 -1
  12. package/srv/client/envelope.js +1 -1
  13. package/srv/client/errors.d.ts +107 -0
  14. package/srv/client/errors.js +144 -0
  15. package/srv/client/fetch.d.ts +2 -2
  16. package/srv/client/fetch.js +76 -11
  17. package/srv/client/pay-handlers.d.ts +4 -4
  18. package/srv/client/pay-handlers.js +3 -3
  19. package/srv/client/types.d.ts +19 -7
  20. package/srv/client/types.js +3 -3
  21. package/srv/core/asset.d.ts +1 -1
  22. package/srv/core/decode.d.ts +2 -2
  23. package/srv/core/decode.js +5 -5
  24. package/srv/core/errors.js +3 -3
  25. package/srv/core/network.d.ts +1 -1
  26. package/srv/core/network.js +1 -1
  27. package/srv/core/requirements.d.ts +37 -5
  28. package/srv/core/requirements.js +43 -4
  29. package/srv/core/types.d.ts +68 -6
  30. package/srv/core/types.js +3 -3
  31. package/srv/core/validate.d.ts +31 -7
  32. package/srv/core/validate.js +84 -9
  33. package/srv/facilitator/adapter.d.ts +8 -8
  34. package/srv/facilitator/adapter.js +5 -5
  35. package/srv/facilitator/http.d.ts +4 -4
  36. package/srv/facilitator/http.js +5 -5
  37. package/srv/facilitator/nonce.d.ts +4 -4
  38. package/srv/facilitator/nonce.js +4 -4
  39. package/srv/facilitator/server.d.ts +68 -0
  40. package/srv/facilitator/server.js +167 -0
  41. package/srv/facilitator/settle.d.ts +2 -2
  42. package/srv/facilitator/settle.js +4 -4
  43. package/srv/facilitator/verify.d.ts +5 -5
  44. package/srv/facilitator/verify.js +19 -5
  45. package/srv/helpers/build-unsigned-tx.d.ts +5 -5
  46. package/srv/helpers/build-unsigned-tx.js +3 -3
  47. package/srv/helpers/verify-confirmed.d.ts +1 -1
  48. package/srv/helpers/verify-confirmed.js +1 -1
  49. package/srv/index.d.ts +4 -2
  50. package/srv/index.js +9 -3
  51. package/srv/middleware/cap.d.ts +47 -9
  52. package/srv/middleware/cap.js +84 -41
  53. package/srv/middleware/express.d.ts +15 -10
  54. package/srv/middleware/express.js +18 -19
  55. package/srv/middleware/grants.d.ts +64 -0
  56. package/srv/middleware/grants.js +113 -0
  57. package/srv/middleware/pricing.d.ts +41 -0
  58. package/srv/middleware/pricing.js +78 -0
  59. package/srv/middleware/receipts.d.ts +38 -0
  60. package/srv/middleware/receipts.js +68 -0
  61. package/srv/plugin.d.ts +2 -2
  62. package/srv/plugin.js +2 -2
@@ -5,16 +5,16 @@
5
5
  * Cardano-x402-v2:
6
6
  *
7
7
  * 1. Network validation
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
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 tx.validity_range.upper_bound in future
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) checking the UTxO is unspent and (5b) live in
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 v2 spec recommends a TTL. Callers that
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
@@ -6,16 +6,16 @@
6
6
  * Cardano-x402-v2:
7
7
  *
8
8
  * 1. Network validation
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
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 tx.validity_range.upper_bound in future
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) checking the UTxO is unspent and (5b) live in
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 also normalises the requirement's
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 UTxO is unspent runs in facilitator/nonce.ts after we've
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 so the tx must be submitted before that slot. We require
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 we don't have the
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
- * they imported `process()` from `verify.ts` directly. That made it
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()` runs verify+settle in-process via
12
+ * - `localFacilitator()` , runs verify+settle in-process via
13
13
  * `@odatano/core`. Default everywhere.
14
- * - `httpFacilitator()` POSTs to a remote service (see
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 it covers the
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** the http facilitator wrapper
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` kept as a type alias for now. */
49
+ /** Identical to the legacy `ProcessResult`, kept as a type alias for now. */
50
50
  export type FacilitatorResult = ProcessResult;
51
- /** Discovery response what this facilitator can handle. */
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 call `localFacilitator()` once per service (or inline per
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
- * they imported `process()` from `verify.ts` directly. That made it
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()` runs verify+settle in-process via
13
+ * - `localFacilitator()` , runs verify+settle in-process via
14
14
  * `@odatano/core`. Default everywhere.
15
- * - `httpFacilitator()` POSTs to a remote service (see
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 it covers the
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 call `localFacilitator()` once per service (or inline per
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` delegates verify+settle to a remote HTTP service.
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 the wrapper strips it from the wire
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 sent as `Authorization: Bearer <apiKey>`. */
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 needs to be longer than
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;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * `httpFacilitator` delegates verify+settle to a remote HTTP service.
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 the wrapper strips it from the wire
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` not transmittable. Invoke locally after the
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 payment already on chain */ }
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 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)
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` both translate to REPLAY.
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 {
@@ -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 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)
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` both translate to REPLAY.
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