@parallel-protocol/x402 0.10.0 → 0.10.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/README.md CHANGED
@@ -40,11 +40,12 @@ Agent Merchant (this SDK) Facilitator
40
40
  - The handler only runs after the signature is verified.
41
41
  - The on-chain settlement happens after the handler produces a 2xx response — the agent is not charged on errors.
42
42
  - The settle is bound to the merchant that verified: `POST /x402/verify` hands the middleware a settle token that `POST /x402/settle` must present. The token stays in memory for the life of the request and never reaches the agent.
43
- - If settlement fails, the middleware discards the handler's response and returns `402` with `{ error: "PAYMENT_SETTLEMENT_FAILED", reason, message }`: `reason` is the facilitator's code (`INSUFFICIENT_BALANCE`, `AUTHORIZATION_USED`, `PAYMENT_EXPIRED`, ...) or this SDK's (`SETTLE_TIMEOUT`, `FACILITATOR_UNAVAILABLE`, `FACILITATOR_INVALID_RESPONSE`, `AMOUNT_MISMATCH`, `UNKNOWN`), `message` the facilitator's text or this SDK's, and the facilitator's `Retry-After` is copied when it sent one. A settle that timed out or could not reach the facilitator is retried once first, with the same settle token: once the first attempt has finished, the facilitator answers its recorded outcome, and any other answer to the retry tells the agent to check with the merchant.
43
+ - If settlement fails, the middleware discards the handler's response, its headers, cookies and trailers included (for the `cookies()` of Next.js, see [Next.js](#nextjs-app-router)), and returns `402` with `{ error: "PAYMENT_SETTLEMENT_FAILED", reason, message }`: `reason` is the facilitator's code (`INSUFFICIENT_BALANCE`, `AUTHORIZATION_USED`, `PAYMENT_EXPIRED`, ...) or this SDK's (`SETTLE_TIMEOUT`, `FACILITATOR_UNAVAILABLE`, `FACILITATOR_INVALID_RESPONSE`, `AMOUNT_MISMATCH`, `UNKNOWN`), `message` the facilitator's text or this SDK's, and the facilitator's `Retry-After` is copied when it sent one. A settle that timed out or could not reach the facilitator is retried once first, with the same settle token: once the first attempt has finished, the facilitator answers its recorded outcome, and any other answer to the retry tells the agent to check with the merchant.
44
44
  - A metered settle must settle the amount the handler asked for: a confirmation for another amount is refused and the response discarded.
45
45
  - Handlers are bounded by a 30-second timeout on Express, Hono and Next.js — a slow handler returns `504` and the agent is not charged. (Fastify manages handler execution itself, so no timeout is applied there.) Metered routes get their announced window minus two minutes kept for the settlement and its retry.
46
+ - On paid Express and Fastify routes, the handler does not see the conditional request headers (`If-Match`, `If-None-Match`, `If-Modified-Since`, `If-Unmodified-Since`): Express, or a plugin such as `@fastify/etag`, would turn the paid response into a `304` or a `412`, which is not settled. A paid request always gets the resource.
46
47
  - On paid Fastify routes, writing to `reply.raw` (the SSE/streaming pattern) fails loudly instead of sending: a raw response skips the settlement hooks and would go out unpaid. Streaming is not supported on paid routes.
47
- - CORS preflight (`OPTIONS`) is always passed through without challenge.
48
+ - CORS preflight (`OPTIONS`) is passed through without challenge, unless its path has `.` or `..` segments. On a paid route the handler still answers it, but only its status code and its `Access-Control-*`, `Vary` and `Allow` headers go out, with the standard reason phrase: no body, no other header, no trailer and no early hints. A handler that answers every method does not serve the paid content. On Next.js the handler does not run, see [Next.js](#nextjs-app-router).
48
49
 
49
50
  ---
50
51
 
@@ -61,8 +62,8 @@ Install your framework separately (all are optional peer dependencies):
61
62
  ```bash
62
63
  npm install express # Express 4 or 5
63
64
  npm install next # Next.js 14 or 15
64
- npm install fastify # Fastify 4 or 5
65
- npm install hono # Hono 4+
65
+ npm install fastify # Fastify 5.12.7 or a later 5.x
66
+ npm install hono # Hono 4
66
67
  ```
67
68
 
68
69
  TypeScript consumers should also install `viem` (peer dependency) — the public types use its `Address` type. Node ≥ 18 is required.
@@ -123,6 +124,8 @@ export const GET = withPayment(config, async (req) => {
123
124
  });
124
125
  ```
125
126
 
127
+ On a paid route, a CORS preflight (`OPTIONS`) gets an empty `204` without running the handler, so set the route's CORS headers in the `headers` of `next.config.js`. Next.js adds the cookies set with `cookies()` from `next/headers` to the response the route returns, even to the `402` of a failed settlement. Set the cookies of a paid response on the `NextResponse` the handler returns instead: they leave with it only.
128
+
126
129
  ### Fastify
127
130
 
128
131
  ```typescript
@@ -149,6 +152,8 @@ app.get("/api/data", async () => {
149
152
  });
150
153
  ```
151
154
 
155
+ On a refusal after the handler, only the cookies already set on the reply before the handler go out. Cookies that a plugin such as `@fastify/cookie` writes from its own `onSend` hook are left out of the `402`, even those set with `reply.setCookie` before the handler.
156
+
152
157
  ### Hono
153
158
 
154
159
  ```typescript
@@ -207,7 +212,7 @@ interface PaymentMiddlewareConfig {
207
212
  | `meter` | `(ctx) => string \| bigint \| undefined` | No | `upto` routes only: reads the amount to charge from the handler's response, see [Reading the amount from the response](#reading-the-amount-from-the-response-meter). |
208
213
  | `maxTimeoutSeconds` | `number` | No | Settlement window announced to the payer (default 300). `@parallel-protocol/x402-fetch` signs for it on `upto` routes, and for five minutes on `exact` routes. Metered routes announce how long their handler may take. On an `upto` route it is 3600 (one hour) at most: a larger value raises `X402ConfigError` at construction, and the facilitator refuses an authorization valid for longer with `WINDOW_TOO_LONG`. On metered routes, Express, Hono and Next.js derive the handler timeout from it, minus two minutes kept for the settlement and its retry: a 300 s window leaves the handler 180 s, and a handler always gets at least 30 s. Fastify does not time handlers out, set the server's `connectionTimeout` / `requestTimeout` yourself. |
209
214
 
210
- Route matching uses **longest prefix wins**: `/api/v1/users` will match a `/api/v1` route config before a `/api` one.
215
+ Route matching uses **longest prefix wins**: `/api/v1/users` will match a `/api/v1` route config before a `/api` one. The request path and the route keys are compared once normalized: case, percent-encoding, repeated or trailing slashes, a fragment, `;` path parameters and an absolute-form origin make no difference, so another form of a paid path is charged like the path itself. A key with a trailing slash, such as `/api/`, covers the paths below it as `/api` does. Routers and proxies do not all resolve `.` and `..` segments, so a path that has some and reaches a paid route is refused with a `400` and `INVALID_PATH`. Under an Express mount prefix, such as `app.use("/api", ...)`, a key can be written with the full path or relative to the mount, and the key `/` covers only the mount's root: write `/api` to cover every path below it. A Next.js route handler sees the path without the `basePath`, so write keys without it.
211
216
 
212
217
  ---
213
218
 
@@ -495,7 +500,7 @@ routes: {
495
500
  },
496
501
  ```
497
502
 
498
- The function gets `{ method, path, url, header(name), body }`. `body` is the request body as the framework parsed it (`undefined` when there is none), so it is available where your framework parses it:
503
+ The function gets `{ method, path, routePath, url, header(name), body }`. `path` is the request path as the framework gives it, and `routePath` the path the route matched, in the form routes are matched on. Under an Express mount, that is the full path when the route key is written with it. To price by path, compare `routePath`, so that another form of the same path, such as `/API/premium`, gets the same price. `body` is the request body as the framework parsed it (`undefined` when there is none), so it is available where your framework parses it:
499
504
 
500
505
  | Framework | `body` |
501
506
  |-----------|--------|
@@ -676,6 +681,7 @@ import {
676
681
 
677
682
  // Utils
678
683
  encodeBase64,
684
+ normalizeRoutePath,
679
685
  parsePrice,
680
686
  toEip155Network,
681
687
  validateAddress,
@@ -688,8 +694,8 @@ import {
688
694
  |-------------|--------|-----------|
689
695
  | `@parallel-protocol/x402/express` | `paymentMiddleware(config)` | Express 4/5 |
690
696
  | `@parallel-protocol/x402/next` | `withPayment(config, handler)` | Next.js 14/15 |
691
- | `@parallel-protocol/x402/fastify` | `paymentMiddleware(config)` | Fastify 4/5 |
692
- | `@parallel-protocol/x402/hono` | `paymentMiddleware(config)` | Hono 4+ |
697
+ | `@parallel-protocol/x402/fastify` | `paymentMiddleware(config)` | Fastify 5 (5.12.7 or later) |
698
+ | `@parallel-protocol/x402/hono` | `paymentMiddleware(config)` | Hono 4 |
693
699
 
694
700
  Each subpath also exports its adapter class (`ExpressAdapter`, `NextAdapter`, `FastifyAdapter`, `HonoAdapter`) for use with [`createPaymentGate`](#advanced-createpaymentgate).
695
701
 
@@ -743,7 +749,9 @@ const result = await gate({
743
749
  });
744
750
 
745
751
  if (result.type === "pass") {
746
- // Route not configured for payment, proceed normally
752
+ // Route not configured for payment, proceed normally.
753
+ // result.preflight: an OPTIONS on a paid route, send only the handler's
754
+ // status code and its Access-Control-*, Vary and Allow headers
747
755
  } else if (result.type === "error") {
748
756
  // Send result.result.status (402, or the status of a price refusal)
749
757
  // with result.result.headers and result.result.body
@@ -820,12 +828,13 @@ What the middleware answers on a paid route instead of the handler's response:
820
828
  | 402 | `{ error: "INVALID_SETTLE_AMOUNT", message }` | A metered route named an amount the middleware cannot settle, see [Metered routes](#metered-routes-upto) |
821
829
  | 504 | `{ error: "HANDLER_TIMEOUT", message }` on Express and Hono, an empty body on Next.js | The handler did not answer within its timeout, and nothing is settled. Next.js also answers this empty 504 when the handler throws |
822
830
  | 400, or the status the price function chose | `{ error, message }` | A price function refused the request with `X402PriceRejectedError`, and nothing was invoiced |
831
+ | 400 | `{ error: "INVALID_PATH", message }` | The path has `.` or `..` segments, and reaches a paid route. Nothing was invoiced |
823
832
 
824
833
  ### Thrown by the SDK
825
834
 
826
835
  | Error class | When |
827
836
  |-------------|------|
828
- | `X402ConfigError` | Invalid config at construction, the message naming the first field at fault: a bad `payTo`, a facilitator URL that is neither https nor plain http on `localhost`, `127.0.0.1` or `[::1]`, empty `routes`, a `network` of the catalog other than `ethereum`, `base`, `avalanche` or `hyperevm`, a malformed or non-positive `price`, a `meter` on a route that is not `upto`, or an `upto` route with `maxTimeoutSeconds` above 3600, a token other than USDp and the network's USDC paid through Permit2, or a bigint price or an explicit `decimals` while it accepts tokens with different decimals. Then, on a request to a route: an unknown network with no `acceptedTokens`, an accepted token whose decimals are neither in the catalog nor set via `decimals`, a price finer than the token's precision, or a price function that throws, returns something that is not a price, or returns a bigint on an `upto` route that accepts tokens with different decimals |
837
+ | `X402ConfigError` | Invalid config at construction, the message naming the first field at fault: a bad `payTo`, a facilitator URL that is neither https nor plain http on `localhost`, `127.0.0.1` or `[::1]`, empty `routes`, a `network` of the catalog other than `ethereum`, `base`, `avalanche` or `hyperevm`, a malformed or non-positive `price`, two routes that are the same path once normalized with different configs, a `meter` on a route that is not `upto`, or an `upto` route with `maxTimeoutSeconds` above 3600, a token other than USDp and the network's USDC paid through Permit2, or a bigint price or an explicit `decimals` while it accepts tokens with different decimals. Then, on a request to a route: an unknown network with no `acceptedTokens`, an accepted token whose decimals are neither in the catalog nor set via `decimals`, a price finer than the token's precision, or a price function that throws, returns something that is not a price, or returns a bigint on an `upto` route that accepts tokens with different decimals |
829
838
  | `X402RuntimeError` | Runtime failure; `.code` is one of the SDK codes below, or a facilitator error code propagated verbatim (see next table) |
830
839
  | `X402PriceRejectedError` | Thrown by a price function to refuse a request before anything is invoiced; the client gets `.status` (400 by default) with `{ error: .code, message }`. `isPriceRejection(err)` recognises one by shape. |
831
840
 
@@ -948,6 +957,7 @@ import {
948
957
  | `validateAddress` | `(address: string) => address is Address` | Check 0x + 40-hex-char format |
949
958
  | `toEip155Network` | `(network: string) => string` | `"base"` → `"eip155:8453"` |
950
959
  | `encodeBase64` | `(obj: unknown) => string` | `JSON.stringify` → base64 |
960
+ | `normalizeRoutePath` | `(path: string) => string` | The form routes are matched on: `normalizeRoutePath("/API//Data/")` is `"/api/data"` |
951
961
  | `getDefaultAcceptedTokens` | `(network: string) => [Address, Address, Address] \| undefined` | `[USDp, USDC, sUSDp]` for known networks |
952
962
 
953
963
  ---
@@ -5,6 +5,8 @@ var X402_ERROR_CODES = {
5
5
  FACILITATOR_UNAVAILABLE: "FACILITATOR_UNAVAILABLE",
6
6
  FACILITATOR_INVALID_RESPONSE: "FACILITATOR_INVALID_RESPONSE",
7
7
  INVALID_PAYMENT: "INVALID_PAYMENT",
8
+ /** A path with `.` or `..` segments on a paid route, refused with 400. */
9
+ INVALID_PATH: "INVALID_PATH",
8
10
  /** The facilitator did not answer a settle within the client's timeout: the payment may still settle. */
9
11
  SETTLE_TIMEOUT: "SETTLE_TIMEOUT"
10
12
  };
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkKZZY5LS6_cjs = require('./chunk-KZZY5LS6.cjs');
3
+ var chunk4CEMNRCZ_cjs = require('./chunk-4CEMNRCZ.cjs');
4
4
  var chains = require('@parallel-protocol/chains');
5
5
  var paymentCore = require('@parallel-protocol/payment-core');
6
6
  var zod = require('zod');
@@ -116,18 +116,144 @@ function encodeBase64(obj) {
116
116
  function decodeBase64(str) {
117
117
  return JSON.parse(Buffer.from(str, "base64").toString("utf8"));
118
118
  }
119
- function matchRoute(path, routes) {
120
- if (routes[path]) return routes[path];
119
+ var CONDITIONAL_HEADERS = [
120
+ "if-match",
121
+ "if-none-match",
122
+ "if-modified-since",
123
+ "if-unmodified-since"
124
+ ];
125
+ function dropConditionalHeaders(headers) {
126
+ for (const name of CONDITIONAL_HEADERS) delete headers[name];
127
+ }
128
+ var UTF8 = new TextDecoder();
129
+ var DECODE_ROUNDS = 8;
130
+ function decodePercent(path) {
131
+ try {
132
+ return decodeURIComponent(path);
133
+ } catch {
134
+ return path.replace(
135
+ /(?:%[\da-f]{2})+/gi,
136
+ (run) => UTF8.decode(
137
+ Uint8Array.from(
138
+ run.slice(1).split("%"),
139
+ (hex) => Number.parseInt(hex, 16)
140
+ )
141
+ )
142
+ );
143
+ }
144
+ }
145
+ function hasLongMarkRun(segment) {
146
+ for (const [run] of segment.matchAll(/\p{M}+/gu))
147
+ if (run.length > 8) return true;
148
+ return false;
149
+ }
150
+ function foldCase(segment) {
151
+ const composed = hasLongMarkRun(segment) ? segment : segment.normalize("NFC");
152
+ return composed.toLowerCase().toUpperCase().toLowerCase();
153
+ }
154
+ function pathSegments(path) {
155
+ let decoded = path;
156
+ for (let round = 0; round < DECODE_ROUNDS && decoded.includes("%"); round++) {
157
+ const next = decodePercent(decoded);
158
+ if (next === decoded) break;
159
+ decoded = next;
160
+ }
161
+ const target = decoded.replaceAll("\\", "/").replace(/^[a-z][\d+.a-z-]*:\/\/[^/]*/i, "").split(/[?#]/, 1)[0] ?? "";
162
+ return target.split("/").map((segment) => foldCase(segment).split(";", 1)[0] ?? "").filter((segment) => segment !== "");
163
+ }
164
+ function resolveDots(segments) {
165
+ const resolved = [];
166
+ for (const segment of segments) {
167
+ if (segment === "..") resolved.pop();
168
+ else if (segment !== ".") resolved.push(segment);
169
+ }
170
+ return resolved;
171
+ }
172
+ function normalizeRoutePath(path) {
173
+ return `/${resolveDots(pathSegments(path)).join("/")}`;
174
+ }
175
+ function routeTable(routes) {
176
+ return Object.entries(routes).map(([key, route]) => [
177
+ normalizeRoutePath(key),
178
+ route
179
+ ]);
180
+ }
181
+ function longestMatch(target, table) {
121
182
  let bestMatch;
122
183
  let bestLength = 0;
123
- for (const [pattern, config] of Object.entries(routes)) {
124
- if (path.startsWith(`${pattern}/`) && pattern.length > bestLength) {
125
- bestMatch = config;
126
- bestLength = pattern.length;
184
+ for (const [key, route] of table) {
185
+ if (key === target) return route;
186
+ if (target.startsWith(`${key}/`) && key.length > bestLength) {
187
+ bestMatch = route;
188
+ bestLength = key.length;
127
189
  }
128
190
  }
129
191
  return bestMatch;
130
192
  }
193
+ function findRoute(path, table) {
194
+ const segments = pathSegments(path);
195
+ const dotSegments = segments.some(
196
+ (segment) => segment === "." || segment === ".."
197
+ );
198
+ const routePath = `/${resolveDots(segments).join("/")}`;
199
+ const route = longestMatch(routePath, table) ?? (dotSegments ? longestMatch(`/${segments.join("/")}`, table) : void 0);
200
+ return { route, dotSegments, routePath };
201
+ }
202
+ function isPreflightHeader(name) {
203
+ return /^(?:access-control-|vary$|allow$)/i.test(String(name));
204
+ }
205
+ function preflightHeaders(headers) {
206
+ if (Array.isArray(headers)) {
207
+ if (Array.isArray(headers[0]))
208
+ return headers.filter((pair) => isPreflightHeader(pair[0]));
209
+ return headers.flatMap(
210
+ (item, i) => i % 2 === 0 && isPreflightHeader(item) ? [item, headers[i + 1]] : []
211
+ );
212
+ }
213
+ if (headers !== null && typeof headers === "object")
214
+ return Object.fromEntries(
215
+ Object.entries(headers).filter(([name]) => isPreflightHeader(name))
216
+ );
217
+ return headers;
218
+ }
219
+ function answerAsPreflight(res) {
220
+ const writeHead = res.writeHead.bind(res);
221
+ const end = res.end.bind(res);
222
+ const callbackOf = (args) => args.find((arg) => typeof arg === "function");
223
+ const dropOtherHeaders = () => {
224
+ for (const name of res.getHeaderNames())
225
+ if (!isPreflightHeader(name)) res.removeHeader(name);
226
+ };
227
+ res.writeHead = ((statusCode, ...rest) => {
228
+ dropOtherHeaders();
229
+ res.statusMessage = "";
230
+ const headers = rest.find((arg) => typeof arg !== "string");
231
+ return headers === void 0 ? writeHead(statusCode) : writeHead(statusCode, preflightHeaders(headers));
232
+ });
233
+ res.write = ((...args) => {
234
+ const done = callbackOf(args);
235
+ if (done) queueMicrotask(done);
236
+ return true;
237
+ });
238
+ res.end = ((...args) => {
239
+ if (!res.headersSent) dropOtherHeaders();
240
+ const done = callbackOf(args);
241
+ return done ? end(done) : end();
242
+ });
243
+ res.addTrailers = () => {
244
+ };
245
+ res.writeEarlyHints = ((_hints, done) => {
246
+ if (done) queueMicrotask(done);
247
+ });
248
+ }
249
+ function preflightInit(res) {
250
+ res.body?.cancel().catch(() => {
251
+ });
252
+ const headers = new Headers();
253
+ for (const [name, value] of res.headers)
254
+ if (isPreflightHeader(name)) headers.append(name, value);
255
+ return { status: res.status, headers };
256
+ }
131
257
  function permit2Extra(network, asset) {
132
258
  const domain = USDC_EIP712_DOMAIN[network];
133
259
  const escrow = chains.getUptoEscrowAddress(network);
@@ -298,9 +424,31 @@ var routeConfigSchema = zod.z.object({
298
424
  (route) => route.scheme !== "upto" || !hasMixedDecimals(route.network, route.acceptedTokens) || typeof route.price !== "bigint" && route.decimals === void 0,
299
425
  'An upto route that accepts tokens with different decimals needs a price like "0.01", not a bigint or an explicit decimals'
300
426
  );
427
+ function sameValue(a, b) {
428
+ if (Object.is(a, b)) return true;
429
+ if (typeof a !== "object" || typeof b !== "object" || !a || !b) return false;
430
+ const entries = Object.entries(a);
431
+ return Array.isArray(a) === Array.isArray(b) && entries.length === Object.keys(b).length && entries.every(
432
+ ([key, value]) => sameValue(value, b[key])
433
+ );
434
+ }
435
+ function consistentRoutePaths(routes, ctx) {
436
+ const keyByPath = /* @__PURE__ */ new Map();
437
+ for (const [key, route] of Object.entries(routes)) {
438
+ const path = normalizeRoutePath(key);
439
+ const other = keyByPath.get(path);
440
+ if (other === void 0) keyByPath.set(path, key);
441
+ else if (!sameValue(routes[other], route))
442
+ ctx.addIssue({
443
+ code: "custom",
444
+ path: [key],
445
+ message: `Same path as "${other}" once normalized, with another config: keep one of the two routes`
446
+ });
447
+ }
448
+ }
301
449
  var paymentMiddlewareConfigSchema = zod.z.object({
302
450
  facilitator: facilitatorConfigSchema,
303
- routes: zod.z.record(zod.z.string(), routeConfigSchema).refine((r) => Object.keys(r).length > 0, "At least one route is required")
451
+ routes: zod.z.record(zod.z.string(), routeConfigSchema).refine((r) => Object.keys(r).length > 0, "At least one route is required").superRefine(consistentRoutePaths)
304
452
  });
305
453
  var facilitatorResponseSchema = zod.z.discriminatedUnion("success", [
306
454
  zod.z.object({
@@ -371,7 +519,7 @@ function throwIfRedirect(response) {
371
519
  if (!redirected) return;
372
520
  response.body?.cancel().catch(() => {
373
521
  });
374
- throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE, {
522
+ throw new chunk4CEMNRCZ_cjs.X402RuntimeError(chunk4CEMNRCZ_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE, {
375
523
  status: response.status
376
524
  });
377
525
  }
@@ -379,7 +527,7 @@ function decodePaymentHeader(paymentHeader, network) {
379
527
  try {
380
528
  return toFacilitatorPayload(decodeBase64(paymentHeader), network);
381
529
  } catch {
382
- throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.INVALID_PAYMENT);
530
+ throw new chunk4CEMNRCZ_cjs.X402RuntimeError(chunk4CEMNRCZ_cjs.X402_ERROR_CODES.INVALID_PAYMENT);
383
531
  }
384
532
  }
385
533
  function payerOf(paymentPayload) {
@@ -394,7 +542,7 @@ var FacilitatorClient = class {
394
542
  constructor(config) {
395
543
  const url = facilitatorUrlSchema.safeParse(config.url);
396
544
  if (!url.success) {
397
- throw new chunkKZZY5LS6_cjs.X402ConfigError(
545
+ throw new chunk4CEMNRCZ_cjs.X402ConfigError(
398
546
  url.error.issues[0]?.message ?? "Invalid facilitator URL",
399
547
  url.error
400
548
  );
@@ -419,8 +567,8 @@ var FacilitatorClient = class {
419
567
  throwIfRedirect(response);
420
568
  if (!response.ok) {
421
569
  const body = await readErrorBody(response);
422
- throw new chunkKZZY5LS6_cjs.X402RuntimeError(
423
- body.error ?? chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE,
570
+ throw new chunk4CEMNRCZ_cjs.X402RuntimeError(
571
+ body.error ?? chunk4CEMNRCZ_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE,
424
572
  {
425
573
  detail: body.message,
426
574
  status: response.status,
@@ -430,14 +578,14 @@ var FacilitatorClient = class {
430
578
  }
431
579
  json = await response.json();
432
580
  } catch (e) {
433
- if (e instanceof chunkKZZY5LS6_cjs.X402RuntimeError) throw e;
434
- throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
581
+ if (e instanceof chunk4CEMNRCZ_cjs.X402RuntimeError) throw e;
582
+ throw new chunk4CEMNRCZ_cjs.X402RuntimeError(chunk4CEMNRCZ_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
435
583
  }
436
584
  try {
437
585
  const { settleToken } = verifyResponseSchema.parse(json);
438
586
  return { settleToken };
439
587
  } catch {
440
- throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
588
+ throw new chunk4CEMNRCZ_cjs.X402RuntimeError(chunk4CEMNRCZ_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
441
589
  }
442
590
  }
443
591
  async settle(paymentHeader, paymentRequirements, options) {
@@ -469,7 +617,7 @@ var FacilitatorClient = class {
469
617
  if (!response.ok) {
470
618
  const body = await readErrorBody(response);
471
619
  if (body.error === void 0) {
472
- throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE, {
620
+ throw new chunk4CEMNRCZ_cjs.X402RuntimeError(chunk4CEMNRCZ_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE, {
473
621
  status: response.status
474
622
  });
475
623
  }
@@ -486,11 +634,11 @@ var FacilitatorClient = class {
486
634
  }
487
635
  json = await response.json();
488
636
  } catch (e) {
489
- if (e instanceof chunkKZZY5LS6_cjs.X402RuntimeError) throw e;
637
+ if (e instanceof chunk4CEMNRCZ_cjs.X402RuntimeError) throw e;
490
638
  if (e instanceof Error && e.name === "TimeoutError") {
491
- throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.SETTLE_TIMEOUT);
639
+ throw new chunk4CEMNRCZ_cjs.X402RuntimeError(chunk4CEMNRCZ_cjs.X402_ERROR_CODES.SETTLE_TIMEOUT);
492
640
  }
493
- throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
641
+ throw new chunk4CEMNRCZ_cjs.X402RuntimeError(chunk4CEMNRCZ_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
494
642
  }
495
643
  try {
496
644
  const parsed = settleResponseSchema.parse(json);
@@ -523,7 +671,7 @@ var FacilitatorClient = class {
523
671
  error: { code: parsed.error, message: parsed.message }
524
672
  };
525
673
  } catch {
526
- throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
674
+ throw new chunk4CEMNRCZ_cjs.X402RuntimeError(chunk4CEMNRCZ_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
527
675
  }
528
676
  }
529
677
  /**
@@ -543,17 +691,17 @@ var FacilitatorClient = class {
543
691
  });
544
692
  throwIfRedirect(response);
545
693
  if (!response.ok) {
546
- throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
694
+ throw new chunk4CEMNRCZ_cjs.X402RuntimeError(chunk4CEMNRCZ_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
547
695
  }
548
696
  json = await response.json();
549
697
  } catch (e) {
550
- if (e instanceof chunkKZZY5LS6_cjs.X402RuntimeError) throw e;
551
- throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
698
+ if (e instanceof chunk4CEMNRCZ_cjs.X402RuntimeError) throw e;
699
+ throw new chunk4CEMNRCZ_cjs.X402RuntimeError(chunk4CEMNRCZ_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
552
700
  }
553
701
  try {
554
702
  return facilitatorResponseSchema.parse(json);
555
703
  } catch {
556
- throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
704
+ throw new chunk4CEMNRCZ_cjs.X402RuntimeError(chunk4CEMNRCZ_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
557
705
  }
558
706
  }
559
707
  buildHeaders() {
@@ -585,10 +733,11 @@ async function raceTimeout(promise, ms = HANDLER_TIMEOUT_MS) {
585
733
 
586
734
  // src/middleware.ts
587
735
  var PRICE_FORMAT = /^\d+(?:\.\d+)?$/;
588
- async function requestContext(adapter) {
736
+ async function requestContext(adapter, routePath) {
589
737
  return {
590
738
  method: adapter.getMethod(),
591
739
  path: adapter.getPath(),
740
+ routePath,
592
741
  url: adapter.getUrl(),
593
742
  header: (name) => adapter.getHeader(name),
594
743
  body: adapter.getBody ? await adapter.getBody() : void 0
@@ -596,27 +745,27 @@ async function requestContext(adapter) {
596
745
  }
597
746
  async function resolvePrice(config, ctx, tokens) {
598
747
  if (typeof config.price !== "function") return config.price;
599
- if (!ctx) throw new chunkKZZY5LS6_cjs.X402ConfigError("A price function needs the request");
748
+ if (!ctx) throw new chunk4CEMNRCZ_cjs.X402ConfigError("A price function needs the request");
600
749
  let price;
601
750
  try {
602
751
  price = await config.price(ctx);
603
752
  } catch (cause) {
604
- if (chunkKZZY5LS6_cjs.isPriceRejection(cause)) throw cause;
605
- throw new chunkKZZY5LS6_cjs.X402ConfigError(
753
+ if (chunk4CEMNRCZ_cjs.isPriceRejection(cause)) throw cause;
754
+ throw new chunk4CEMNRCZ_cjs.X402ConfigError(
606
755
  `The price function of route "${config.network}${ctx.path}" threw`,
607
756
  cause
608
757
  );
609
758
  }
610
759
  const valid = typeof price === "bigint" ? price > 0n : typeof price === "string" && PRICE_FORMAT.test(price) && Number(price) > 0;
611
760
  if (!valid) {
612
- throw new chunkKZZY5LS6_cjs.X402ConfigError(
761
+ throw new chunk4CEMNRCZ_cjs.X402ConfigError(
613
762
  `The price function of route "${config.network}${ctx.path}" returned ${JSON.stringify(
614
763
  typeof price === "bigint" ? price.toString() : price
615
764
  )} \u2014 expected a positive amount like "0.01" or a bigint`
616
765
  );
617
766
  }
618
767
  if (typeof price === "bigint" && config.scheme === "upto" && hasMixedDecimals(config.network, tokens)) {
619
- throw new chunkKZZY5LS6_cjs.X402ConfigError(
768
+ throw new chunk4CEMNRCZ_cjs.X402ConfigError(
620
769
  `The price function of route "${config.network}${ctx.path}" returned a bigint, but the route accepts tokens with different decimals: return a price like "0.01"`
621
770
  );
622
771
  }
@@ -675,7 +824,7 @@ function refusingBigints(meter) {
675
824
  return async (ctx) => {
676
825
  const amount = await meter(ctx);
677
826
  if (typeof amount === "bigint") {
678
- throw new chunkKZZY5LS6_cjs.X402ConfigError(
827
+ throw new chunk4CEMNRCZ_cjs.X402ConfigError(
679
828
  'The meter returned a bigint, but the route accepts tokens with different decimals: return an amount like "0.75"'
680
829
  );
681
830
  }
@@ -686,7 +835,7 @@ var AMOUNT_FORMAT = /^\d+(?:\.\d+)?$/;
686
835
  async function resolveCharge(verified, settleHeader, response) {
687
836
  if (verified.meter) {
688
837
  if (!verified.request) {
689
- throw new chunkKZZY5LS6_cjs.X402ConfigError(
838
+ throw new chunk4CEMNRCZ_cjs.X402ConfigError(
690
839
  "A metered payment carries a meter but no request context"
691
840
  );
692
841
  }
@@ -737,14 +886,14 @@ function resolveAcceptedTokens(config) {
737
886
  }
738
887
  const defaults = config.scheme === "upto" ? getMeteredAcceptedTokens(config.network) : getDefaultAcceptedTokens(config.network);
739
888
  if (defaults) return defaults;
740
- throw new chunkKZZY5LS6_cjs.X402ConfigError(
889
+ throw new chunk4CEMNRCZ_cjs.X402ConfigError(
741
890
  `acceptedTokens is required for network "${config.network}" \u2014 no defaults available for this chain`
742
891
  );
743
892
  }
744
893
  function resolveDecimals(config, asset) {
745
894
  const decimals = config.decimals ?? getTokenDecimals(config.network, asset);
746
895
  if (decimals === void 0) {
747
- throw new chunkKZZY5LS6_cjs.X402ConfigError(
896
+ throw new chunk4CEMNRCZ_cjs.X402ConfigError(
748
897
  `Unknown decimals for token ${asset} on "${config.network}" \u2014 add it to the @parallel-protocol/chains catalog or set the route's \`decimals\` explicitly`
749
898
  );
750
899
  }
@@ -753,7 +902,7 @@ function resolveDecimals(config, asset) {
753
902
  function priceInUnits(price, decimals, asset) {
754
903
  const units = parsePrice(price, decimals);
755
904
  if (units === 0n) {
756
- throw new chunkKZZY5LS6_cjs.X402ConfigError(
905
+ throw new chunk4CEMNRCZ_cjs.X402ConfigError(
757
906
  `Price ${typeof price === "bigint" ? price.toString() : `"${price}"`} is below the precision of token ${asset} (${decimals} decimals)`
758
907
  );
759
908
  }
@@ -861,7 +1010,7 @@ function paymentRequiredResponse(url, config, resolvedTokens, price, refusal) {
861
1010
  price
862
1011
  );
863
1012
  if (refusal) {
864
- paymentRequired.error = shapedCode(refusal.code) ?? chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE;
1013
+ paymentRequired.error = shapedCode(refusal.code) ?? chunk4CEMNRCZ_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE;
865
1014
  const message = boundedMessage(refusal.detail);
866
1015
  if (message) paymentRequired.message = message;
867
1016
  }
@@ -878,24 +1027,39 @@ function createPaymentGate(config) {
878
1027
  if (!checked.success) {
879
1028
  const issue = checked.error.issues[0];
880
1029
  const where = issue?.path.length ? ` at ${issue.path.join(".")}` : "";
881
- throw new chunkKZZY5LS6_cjs.X402ConfigError(
1030
+ throw new chunk4CEMNRCZ_cjs.X402ConfigError(
882
1031
  `Invalid paymentMiddleware config${where}: ${issue?.message ?? "check your routes, facilitator URL and payTo addresses"}`,
883
1032
  checked.error
884
1033
  );
885
1034
  }
886
1035
  const client = new FacilitatorClient(config.facilitator);
1036
+ const routes = routeTable(config.routes);
887
1037
  return async (adapter) => {
888
- if (adapter.getMethod().toUpperCase() === "OPTIONS")
889
- return { type: "pass" };
890
- const routeConfig = matchRoute(adapter.getPath(), config.routes);
1038
+ const path = adapter.getPath();
1039
+ const fullPath = adapter.getFullPath?.();
1040
+ let match = findRoute(path, routes);
1041
+ if (!match.route && fullPath !== void 0 && fullPath !== path)
1042
+ match = findRoute(fullPath, routes);
1043
+ const { route: routeConfig, dotSegments, routePath } = match;
891
1044
  if (!routeConfig) return { type: "pass" };
1045
+ if (dotSegments) {
1046
+ return {
1047
+ type: "error",
1048
+ result: jsonResult(400, {
1049
+ error: chunk4CEMNRCZ_cjs.X402_ERROR_CODES.INVALID_PATH,
1050
+ message: "A paid route is not served on a path with . or .. segments: send the path resolved"
1051
+ })
1052
+ };
1053
+ }
1054
+ if (adapter.getMethod().toUpperCase() === "OPTIONS")
1055
+ return { type: "pass", preflight: true };
892
1056
  const resolvedTokens = resolveAcceptedTokens(routeConfig);
893
- let request = typeof routeConfig.price === "function" ? await requestContext(adapter) : void 0;
1057
+ let request = typeof routeConfig.price === "function" ? await requestContext(adapter, routePath) : void 0;
894
1058
  let price;
895
1059
  try {
896
1060
  price = await resolvePrice(routeConfig, request, resolvedTokens);
897
1061
  } catch (err) {
898
- if (!chunkKZZY5LS6_cjs.isPriceRejection(err)) throw err;
1062
+ if (!chunk4CEMNRCZ_cjs.isPriceRejection(err)) throw err;
899
1063
  const status = Number.isInteger(err.status) && err.status >= 400 && err.status <= 599 ? err.status : 400;
900
1064
  return {
901
1065
  type: "error",
@@ -932,7 +1096,7 @@ function createPaymentGate(config) {
932
1096
  try {
933
1097
  ({ settleToken } = await client.verify(paymentHeader, requirements));
934
1098
  } catch (e) {
935
- const refusal = e instanceof chunkKZZY5LS6_cjs.X402RuntimeError ? { code: e.code, detail: e.detail, retryAfter: e.retryAfter } : { code: chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE };
1099
+ const refusal = e instanceof chunk4CEMNRCZ_cjs.X402RuntimeError ? { code: e.code, detail: e.detail, retryAfter: e.retryAfter } : { code: chunk4CEMNRCZ_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE };
936
1100
  return {
937
1101
  type: "error",
938
1102
  result: paymentRequiredResponse(
@@ -945,7 +1109,7 @@ function createPaymentGate(config) {
945
1109
  };
946
1110
  }
947
1111
  const meter = routeConfig.meter && hasMixedDecimals(routeConfig.network, resolvedTokens) ? refusingBigints(routeConfig.meter) : routeConfig.meter;
948
- if (meter && !request) request = await requestContext(adapter);
1112
+ if (meter && !request) request = await requestContext(adapter, routePath);
949
1113
  return {
950
1114
  type: "verified",
951
1115
  scheme: routeConfig.scheme ?? "exact",
@@ -961,9 +1125,9 @@ var DEFAULT_SETTLEMENT_MESSAGE = "The payment could not be settled.";
961
1125
  var MERCHANT_SETTLEMENT_MESSAGE = "The merchant could not complete the settlement of this payment. The payment was not taken: do not sign another payment for this request before checking with the merchant.";
962
1126
  var UNAVAILABLE_SETTLEMENT_MESSAGE = "The facilitator could not settle the payment for now. It may still settle until its authorization expires, or an earlier attempt may already have settled it: do not sign another payment for this request before checking with the merchant.";
963
1127
  var AGENT_MESSAGES = {
964
- [chunkKZZY5LS6_cjs.X402_ERROR_CODES.SETTLE_TIMEOUT]: "The facilitator did not confirm the settlement in time. The payment may still settle: do not sign another payment for this request before checking with the merchant.",
965
- [chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE]: "The facilitator could not be reached to settle the payment. It may still have settled: do not sign another payment for this request before checking with the merchant.",
966
- [chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE]: "The facilitator answered the settlement with an unexpected response. The payment may have settled: do not sign another payment for this request before checking with the merchant.",
1128
+ [chunk4CEMNRCZ_cjs.X402_ERROR_CODES.SETTLE_TIMEOUT]: "The facilitator did not confirm the settlement in time. The payment may still settle: do not sign another payment for this request before checking with the merchant.",
1129
+ [chunk4CEMNRCZ_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE]: "The facilitator could not be reached to settle the payment. It may still have settled: do not sign another payment for this request before checking with the merchant.",
1130
+ [chunk4CEMNRCZ_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE]: "The facilitator answered the settlement with an unexpected response. The payment may have settled: do not sign another payment for this request before checking with the merchant.",
967
1131
  SETTLE_IN_PROGRESS: "The facilitator is still settling this payment. Do not sign another payment for this request before checking with the merchant.",
968
1132
  RECEIPT_UNAVAILABLE: "The facilitator sent the transaction but could not read its confirmation. The payment may have settled: do not sign another payment for this request before checking with the merchant.",
969
1133
  SETTLE_TOKEN_INVALID: MERCHANT_SETTLEMENT_MESSAGE,
@@ -988,8 +1152,8 @@ function settlementFailure(code, failure) {
988
1152
  };
989
1153
  }
990
1154
  var RETRY_ONCE_CODES = /* @__PURE__ */ new Set([
991
- chunkKZZY5LS6_cjs.X402_ERROR_CODES.SETTLE_TIMEOUT,
992
- chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE
1155
+ chunk4CEMNRCZ_cjs.X402_ERROR_CODES.SETTLE_TIMEOUT,
1156
+ chunk4CEMNRCZ_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE
993
1157
  ]);
994
1158
  async function settleVerified(verified, options) {
995
1159
  let response;
@@ -997,7 +1161,7 @@ async function settleVerified(verified, options) {
997
1161
  try {
998
1162
  response = await verified.settle(options);
999
1163
  } catch (e) {
1000
- const code = e instanceof chunkKZZY5LS6_cjs.X402RuntimeError ? e.code : "UNKNOWN";
1164
+ const code = e instanceof chunk4CEMNRCZ_cjs.X402RuntimeError ? e.code : "UNKNOWN";
1001
1165
  if (attempt > 0 || !RETRY_ONCE_CODES.has(code)) {
1002
1166
  return settlementFailure(code);
1003
1167
  }
@@ -1027,7 +1191,8 @@ function createPaymentMiddleware(config) {
1027
1191
  const gate = createPaymentGate(config);
1028
1192
  return async (adapter) => {
1029
1193
  const gateResult = await gate(adapter);
1030
- if (gateResult.type === "pass") return { pass: true };
1194
+ if (gateResult.type === "pass")
1195
+ return gateResult.preflight ? { pass: true, preflight: true } : { pass: true };
1031
1196
  if (gateResult.type === "error") return gateResult.result;
1032
1197
  let handlerResult;
1033
1198
  try {
@@ -1083,19 +1248,24 @@ exports.FacilitatorClient = FacilitatorClient;
1083
1248
  exports.HANDLER_TIMEOUT = HANDLER_TIMEOUT;
1084
1249
  exports.PARALLEL_TOKENS = PARALLEL_TOKENS;
1085
1250
  exports.SETTLE_AMOUNT_HEADER = SETTLE_AMOUNT_HEADER;
1251
+ exports.answerAsPreflight = answerAsPreflight;
1086
1252
  exports.createPaymentGate = createPaymentGate;
1087
1253
  exports.createPaymentMiddleware = createPaymentMiddleware;
1254
+ exports.dropConditionalHeaders = dropConditionalHeaders;
1088
1255
  exports.encodeBase64 = encodeBase64;
1256
+ exports.findRoute = findRoute;
1089
1257
  exports.getDefaultAcceptedTokens = getDefaultAcceptedTokens;
1090
1258
  exports.invalidSettleAmountResponse = invalidSettleAmountResponse;
1091
1259
  exports.isJsonContentType = isJsonContentType;
1092
1260
  exports.jsonFromResponse = jsonFromResponse;
1093
- exports.matchRoute = matchRoute;
1261
+ exports.normalizeRoutePath = normalizeRoutePath;
1094
1262
  exports.parseJsonBody = parseJsonBody;
1095
1263
  exports.parsePrice = parsePrice;
1264
+ exports.preflightInit = preflightInit;
1096
1265
  exports.raceTimeout = raceTimeout;
1097
1266
  exports.resolveCharge = resolveCharge;
1098
1267
  exports.resolveSettleAmount = resolveSettleAmount;
1268
+ exports.routeTable = routeTable;
1099
1269
  exports.settleVerified = settleVerified;
1100
1270
  exports.settlementFailureResponse = settlementFailureResponse;
1101
1271
  exports.toEip155Network = toEip155Network;