@parallel-protocol/x402 0.9.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.
@@ -1,5 +1,6 @@
1
- import { X402_ERROR_CODES, X402ConfigError, X402RuntimeError, isPriceRejection } from './chunk-QAECRSWF.js';
2
- import { isKnownChain, isActiveParallelizerChain, ACTIVE_PARALLELIZER_CHAINS, getTokenDecimals as getTokenDecimals$1 } from '@parallel-protocol/chains';
1
+ import { X402_ERROR_CODES, X402ConfigError, X402RuntimeError, isPriceRejection } from './chunk-MZCHBBUN.js';
2
+ import { isKnownChain, isActiveParallelizerChain, ACTIVE_PARALLELIZER_CHAINS, getUptoEscrowAddress, getTokenDecimals as getTokenDecimals$1 } from '@parallel-protocol/chains';
3
+ import { permit2NonceToBytes32 } from '@parallel-protocol/payment-core';
3
4
  import { z } from 'zod';
4
5
 
5
6
  var PARALLEL_TOKENS = {
@@ -33,6 +34,9 @@ var PARALLEL_TOKENS = {
33
34
  sUSDe: "0x211Cc4DD073734dA055fbF44a2b4667d5E5fE5d2"
34
35
  }
35
36
  };
37
+ var USDC_EIP712_DOMAIN = {
38
+ base: { name: "USD Coin", version: "2" }
39
+ };
36
40
  function getDefaultAcceptedTokens(network) {
37
41
  const entry = PARALLEL_TOKENS[network];
38
42
  if (!entry) return void 0;
@@ -46,6 +50,12 @@ function getMeteredAcceptedTokens(network) {
46
50
  function getTokenDecimals(network, address) {
47
51
  return getTokenDecimals$1(network, address);
48
52
  }
53
+ function hasMixedDecimals(network, tokens) {
54
+ const decimals = new Set(
55
+ tokens?.map((token) => getTokenDecimals$1(network, token))
56
+ );
57
+ return decimals.size > 1;
58
+ }
49
59
 
50
60
  // src/utils.ts
51
61
  var EIP155_NETWORKS = {
@@ -104,18 +114,252 @@ function encodeBase64(obj) {
104
114
  function decodeBase64(str) {
105
115
  return JSON.parse(Buffer.from(str, "base64").toString("utf8"));
106
116
  }
107
- function matchRoute(path, routes) {
108
- if (routes[path]) return routes[path];
117
+ var CONDITIONAL_HEADERS = [
118
+ "if-match",
119
+ "if-none-match",
120
+ "if-modified-since",
121
+ "if-unmodified-since"
122
+ ];
123
+ function dropConditionalHeaders(headers) {
124
+ for (const name of CONDITIONAL_HEADERS) delete headers[name];
125
+ }
126
+ var UTF8 = new TextDecoder();
127
+ var DECODE_ROUNDS = 8;
128
+ function decodePercent(path) {
129
+ try {
130
+ return decodeURIComponent(path);
131
+ } catch {
132
+ return path.replace(
133
+ /(?:%[\da-f]{2})+/gi,
134
+ (run) => UTF8.decode(
135
+ Uint8Array.from(
136
+ run.slice(1).split("%"),
137
+ (hex) => Number.parseInt(hex, 16)
138
+ )
139
+ )
140
+ );
141
+ }
142
+ }
143
+ function hasLongMarkRun(segment) {
144
+ for (const [run] of segment.matchAll(/\p{M}+/gu))
145
+ if (run.length > 8) return true;
146
+ return false;
147
+ }
148
+ function foldCase(segment) {
149
+ const composed = hasLongMarkRun(segment) ? segment : segment.normalize("NFC");
150
+ return composed.toLowerCase().toUpperCase().toLowerCase();
151
+ }
152
+ function pathSegments(path) {
153
+ let decoded = path;
154
+ for (let round = 0; round < DECODE_ROUNDS && decoded.includes("%"); round++) {
155
+ const next = decodePercent(decoded);
156
+ if (next === decoded) break;
157
+ decoded = next;
158
+ }
159
+ const target = decoded.replaceAll("\\", "/").replace(/^[a-z][\d+.a-z-]*:\/\/[^/]*/i, "").split(/[?#]/, 1)[0] ?? "";
160
+ return target.split("/").map((segment) => foldCase(segment).split(";", 1)[0] ?? "").filter((segment) => segment !== "");
161
+ }
162
+ function resolveDots(segments) {
163
+ const resolved = [];
164
+ for (const segment of segments) {
165
+ if (segment === "..") resolved.pop();
166
+ else if (segment !== ".") resolved.push(segment);
167
+ }
168
+ return resolved;
169
+ }
170
+ function normalizeRoutePath(path) {
171
+ return `/${resolveDots(pathSegments(path)).join("/")}`;
172
+ }
173
+ function routeTable(routes) {
174
+ return Object.entries(routes).map(([key, route]) => [
175
+ normalizeRoutePath(key),
176
+ route
177
+ ]);
178
+ }
179
+ function longestMatch(target, table) {
109
180
  let bestMatch;
110
181
  let bestLength = 0;
111
- for (const [pattern, config] of Object.entries(routes)) {
112
- if (path.startsWith(`${pattern}/`) && pattern.length > bestLength) {
113
- bestMatch = config;
114
- bestLength = pattern.length;
182
+ for (const [key, route] of table) {
183
+ if (key === target) return route;
184
+ if (target.startsWith(`${key}/`) && key.length > bestLength) {
185
+ bestMatch = route;
186
+ bestLength = key.length;
115
187
  }
116
188
  }
117
189
  return bestMatch;
118
190
  }
191
+ function findRoute(path, table) {
192
+ const segments = pathSegments(path);
193
+ const dotSegments = segments.some(
194
+ (segment) => segment === "." || segment === ".."
195
+ );
196
+ const routePath = `/${resolveDots(segments).join("/")}`;
197
+ const route = longestMatch(routePath, table) ?? (dotSegments ? longestMatch(`/${segments.join("/")}`, table) : void 0);
198
+ return { route, dotSegments, routePath };
199
+ }
200
+ function isPreflightHeader(name) {
201
+ return /^(?:access-control-|vary$|allow$)/i.test(String(name));
202
+ }
203
+ function preflightHeaders(headers) {
204
+ if (Array.isArray(headers)) {
205
+ if (Array.isArray(headers[0]))
206
+ return headers.filter((pair) => isPreflightHeader(pair[0]));
207
+ return headers.flatMap(
208
+ (item, i) => i % 2 === 0 && isPreflightHeader(item) ? [item, headers[i + 1]] : []
209
+ );
210
+ }
211
+ if (headers !== null && typeof headers === "object")
212
+ return Object.fromEntries(
213
+ Object.entries(headers).filter(([name]) => isPreflightHeader(name))
214
+ );
215
+ return headers;
216
+ }
217
+ function answerAsPreflight(res) {
218
+ const writeHead = res.writeHead.bind(res);
219
+ const end = res.end.bind(res);
220
+ const callbackOf = (args) => args.find((arg) => typeof arg === "function");
221
+ const dropOtherHeaders = () => {
222
+ for (const name of res.getHeaderNames())
223
+ if (!isPreflightHeader(name)) res.removeHeader(name);
224
+ };
225
+ res.writeHead = ((statusCode, ...rest) => {
226
+ dropOtherHeaders();
227
+ res.statusMessage = "";
228
+ const headers = rest.find((arg) => typeof arg !== "string");
229
+ return headers === void 0 ? writeHead(statusCode) : writeHead(statusCode, preflightHeaders(headers));
230
+ });
231
+ res.write = ((...args) => {
232
+ const done = callbackOf(args);
233
+ if (done) queueMicrotask(done);
234
+ return true;
235
+ });
236
+ res.end = ((...args) => {
237
+ if (!res.headersSent) dropOtherHeaders();
238
+ const done = callbackOf(args);
239
+ return done ? end(done) : end();
240
+ });
241
+ res.addTrailers = () => {
242
+ };
243
+ res.writeEarlyHints = ((_hints, done) => {
244
+ if (done) queueMicrotask(done);
245
+ });
246
+ }
247
+ function preflightInit(res) {
248
+ res.body?.cancel().catch(() => {
249
+ });
250
+ const headers = new Headers();
251
+ for (const [name, value] of res.headers)
252
+ if (isPreflightHeader(name)) headers.append(name, value);
253
+ return { status: res.status, headers };
254
+ }
255
+ function permit2Extra(network, asset) {
256
+ const domain = USDC_EIP712_DOMAIN[network];
257
+ const escrow = getUptoEscrowAddress(network);
258
+ if (!domain || !escrow) return void 0;
259
+ const usdc = PARALLEL_TOKENS[network].usdc;
260
+ if (usdc.toLowerCase() !== asset.toLowerCase()) return void 0;
261
+ return { ...domain, facilitatorAddress: escrow };
262
+ }
263
+ var EIP2612_GAS_SPONSORING_EXTENSION = {
264
+ eip2612GasSponsoring: {
265
+ info: {
266
+ description: "The facilitator accepts EIP-2612 gasless Permit to `Permit2` canonical contract.",
267
+ version: "1"
268
+ },
269
+ schema: {
270
+ $schema: "https://json-schema.org/draft/2020-12/schema",
271
+ type: "object",
272
+ properties: {
273
+ from: {
274
+ type: "string",
275
+ pattern: "^0x[a-fA-F0-9]{40}$",
276
+ description: "The address of the sender."
277
+ },
278
+ asset: {
279
+ type: "string",
280
+ pattern: "^0x[a-fA-F0-9]{40}$",
281
+ description: "The address of the ERC-20 token contract."
282
+ },
283
+ spender: {
284
+ type: "string",
285
+ pattern: "^0x[a-fA-F0-9]{40}$",
286
+ description: "The address of the spender (Canonical Permit2)."
287
+ },
288
+ amount: {
289
+ type: "string",
290
+ pattern: "^[0-9]+$",
291
+ description: "The amount to approve (uint256). Typically MaxUint."
292
+ },
293
+ nonce: {
294
+ type: "string",
295
+ pattern: "^[0-9]+$",
296
+ description: "The current nonce of the sender."
297
+ },
298
+ deadline: {
299
+ type: "string",
300
+ pattern: "^[0-9]+$",
301
+ description: "The timestamp at which the signature expires."
302
+ },
303
+ signature: {
304
+ type: "string",
305
+ pattern: "^0x[a-fA-F0-9]+$",
306
+ description: "The 65-byte concatenated signature (r, s, v) as a hex string."
307
+ },
308
+ version: {
309
+ type: "string",
310
+ pattern: "^[0-9]+(\\.[0-9]+)*$",
311
+ description: "Schema version identifier."
312
+ }
313
+ },
314
+ required: [
315
+ "from",
316
+ "asset",
317
+ "spender",
318
+ "amount",
319
+ "nonce",
320
+ "deadline",
321
+ "signature",
322
+ "version"
323
+ ]
324
+ }
325
+ }
326
+ };
327
+ function signedPermit(info = {}) {
328
+ const { amount, deadline, signature } = info;
329
+ return typeof amount === "string" && typeof deadline === "string" && typeof signature === "string" ? { value: amount, deadline, signature } : void 0;
330
+ }
331
+ function isStandardPermit2Payment(decoded) {
332
+ const payment = decoded;
333
+ return typeof payment?.payload?.permit2Authorization === "object" && payment.payload.permit2Authorization !== null && payment.accepted?.scheme === "upto";
334
+ }
335
+ function toFacilitatorPayload(decoded, network) {
336
+ if (!isStandardPermit2Payment(decoded)) return decoded;
337
+ const { signature, permit2Authorization: auth } = decoded.payload;
338
+ const permit = signedPermit(decoded.extensions?.eip2612GasSponsoring?.info);
339
+ return {
340
+ scheme: "upto",
341
+ network,
342
+ method: "uptoPermit2",
343
+ tokenIn: auth.permitted.token,
344
+ payload: {
345
+ signature,
346
+ authorization: {
347
+ from: auth.from,
348
+ to: auth.witness.to,
349
+ value: auth.permitted.amount,
350
+ validAfter: auth.witness.validAfter,
351
+ validBefore: auth.deadline,
352
+ nonce: permit2NonceToBytes32(auth.nonce)
353
+ },
354
+ ...permit && { permit2612: permit }
355
+ },
356
+ uptoParams: { payTo: auth.witness.to },
357
+ permit2Params: {
358
+ spender: auth.spender,
359
+ facilitator: auth.witness.facilitator
360
+ }
361
+ };
362
+ }
119
363
  var ethereumAddressSchema = z.string().regex(/^0x[0-9a-fA-F]{40}$/, "Invalid Ethereum address");
120
364
  var txHashSchema = z.string().regex(/^0x[0-9a-fA-F]{64}$/);
121
365
  var priceSchema = z.union([
@@ -150,6 +394,7 @@ var routeConfigSchema = z.object({
150
394
  description: z.string().optional(),
151
395
  scheme: z.enum(["exact", "upto"]).optional(),
152
396
  maxTimeoutSeconds: z.number().int().positive().optional(),
397
+ decimals: z.number().int().nonnegative().optional(),
153
398
  meter: z.custom(
154
399
  (value) => typeof value === "function",
155
400
  "meter must be a function of the response"
@@ -171,12 +416,37 @@ var routeConfigSchema = z.object({
171
416
  const usdp = PARALLEL_TOKENS[route.network]?.usdp;
172
417
  if (!usdp) return true;
173
418
  return route.acceptedTokens.every(
174
- (token) => token.toLowerCase() === usdp.toLowerCase()
419
+ (token) => token.toLowerCase() === usdp.toLowerCase() || permit2Extra(route.network, token) !== void 0
420
+ );
421
+ }, "acceptedTokens on an upto route may only list the network's USDp, and its USDC where the escrow takes Permit2 payments").refine(
422
+ (route) => route.scheme !== "upto" || !hasMixedDecimals(route.network, route.acceptedTokens) || typeof route.price !== "bigint" && route.decimals === void 0,
423
+ 'An upto route that accepts tokens with different decimals needs a price like "0.01", not a bigint or an explicit decimals'
424
+ );
425
+ function sameValue(a, b) {
426
+ if (Object.is(a, b)) return true;
427
+ if (typeof a !== "object" || typeof b !== "object" || !a || !b) return false;
428
+ const entries = Object.entries(a);
429
+ return Array.isArray(a) === Array.isArray(b) && entries.length === Object.keys(b).length && entries.every(
430
+ ([key, value]) => sameValue(value, b[key])
175
431
  );
176
- }, "acceptedTokens on an upto route may only list the network's USDp");
432
+ }
433
+ function consistentRoutePaths(routes, ctx) {
434
+ const keyByPath = /* @__PURE__ */ new Map();
435
+ for (const [key, route] of Object.entries(routes)) {
436
+ const path = normalizeRoutePath(key);
437
+ const other = keyByPath.get(path);
438
+ if (other === void 0) keyByPath.set(path, key);
439
+ else if (!sameValue(routes[other], route))
440
+ ctx.addIssue({
441
+ code: "custom",
442
+ path: [key],
443
+ message: `Same path as "${other}" once normalized, with another config: keep one of the two routes`
444
+ });
445
+ }
446
+ }
177
447
  var paymentMiddlewareConfigSchema = z.object({
178
448
  facilitator: facilitatorConfigSchema,
179
- routes: z.record(z.string(), routeConfigSchema).refine((r) => Object.keys(r).length > 0, "At least one route is required")
449
+ routes: z.record(z.string(), routeConfigSchema).refine((r) => Object.keys(r).length > 0, "At least one route is required").superRefine(consistentRoutePaths)
180
450
  });
181
451
  var facilitatorResponseSchema = z.discriminatedUnion("success", [
182
452
  z.object({
@@ -210,6 +480,9 @@ var settleSuccessSchema = z.object({
210
480
  gasSponsored: z.boolean(),
211
481
  // Metered routes: the amount actually charged (smallest unit).
212
482
  amountSettled: z.string().optional(),
483
+ // A standard x402 Permit2 payment: the USDp the merchant received for
484
+ // amountSettled (smallest unit), when the facilitator read it.
485
+ amountDelivered: z.string().optional(),
213
486
  // The facilitator answered from a recorded outcome, nothing was broadcast.
214
487
  alreadySettled: z.boolean().optional()
215
488
  });
@@ -248,13 +521,17 @@ function throwIfRedirect(response) {
248
521
  status: response.status
249
522
  });
250
523
  }
251
- function decodePaymentHeader(paymentHeader) {
524
+ function decodePaymentHeader(paymentHeader, network) {
252
525
  try {
253
- return decodeBase64(paymentHeader);
526
+ return toFacilitatorPayload(decodeBase64(paymentHeader), network);
254
527
  } catch {
255
528
  throw new X402RuntimeError(X402_ERROR_CODES.INVALID_PAYMENT);
256
529
  }
257
530
  }
531
+ function payerOf(paymentPayload) {
532
+ const from = paymentPayload?.payload?.authorization?.from;
533
+ return typeof from === "string" ? from : void 0;
534
+ }
258
535
  var FacilitatorClient = class {
259
536
  // Checked and kept when the client is built: a config changed afterwards
260
537
  // does not move the calls elsewhere.
@@ -272,7 +549,10 @@ var FacilitatorClient = class {
272
549
  this.apiKey = config.apiKey;
273
550
  }
274
551
  async verify(paymentHeader, paymentRequirements) {
275
- const paymentPayload = decodePaymentHeader(paymentHeader);
552
+ const paymentPayload = decodePaymentHeader(
553
+ paymentHeader,
554
+ paymentRequirements.network
555
+ );
276
556
  let json;
277
557
  try {
278
558
  const response = await fetch(`${this.url}/x402/verify`, {
@@ -307,7 +587,10 @@ var FacilitatorClient = class {
307
587
  }
308
588
  }
309
589
  async settle(paymentHeader, paymentRequirements, options) {
310
- const paymentPayload = decodePaymentHeader(paymentHeader);
590
+ const paymentPayload = decodePaymentHeader(
591
+ paymentHeader,
592
+ paymentRequirements.network
593
+ );
311
594
  let json;
312
595
  try {
313
596
  const response = await fetch(`${this.url}/x402/settle`, {
@@ -358,17 +641,27 @@ var FacilitatorClient = class {
358
641
  try {
359
642
  const parsed = settleResponseSchema.parse(json);
360
643
  if ("txHash" in parsed) {
644
+ const txHash = parsed.txHash;
645
+ const networkId = toEip155Network(paymentRequirements.network);
646
+ const payer = payerOf(paymentPayload);
361
647
  return {
362
648
  success: true,
363
- txHash: parsed.txHash,
649
+ txHash,
364
650
  route: parsed.route,
365
651
  chain: paymentRequirements.network,
366
652
  gasSponsored: parsed.gasSponsored,
367
- networkId: toEip155Network(paymentRequirements.network),
653
+ networkId,
368
654
  ...parsed.amountSettled !== void 0 && {
369
655
  amountSettled: parsed.amountSettled
370
656
  },
371
- ...parsed.alreadySettled && { alreadySettled: true }
657
+ ...parsed.amountDelivered !== void 0 && {
658
+ amountDelivered: parsed.amountDelivered
659
+ },
660
+ ...parsed.alreadySettled && { alreadySettled: true },
661
+ transaction: txHash,
662
+ network: networkId,
663
+ amount: parsed.amountSettled ?? paymentRequirements.maxAmountRequired,
664
+ ...payer && { payer }
372
665
  };
373
666
  }
374
667
  return {
@@ -438,16 +731,17 @@ async function raceTimeout(promise, ms = HANDLER_TIMEOUT_MS) {
438
731
 
439
732
  // src/middleware.ts
440
733
  var PRICE_FORMAT = /^\d+(?:\.\d+)?$/;
441
- async function requestContext(adapter) {
734
+ async function requestContext(adapter, routePath) {
442
735
  return {
443
736
  method: adapter.getMethod(),
444
737
  path: adapter.getPath(),
738
+ routePath,
445
739
  url: adapter.getUrl(),
446
740
  header: (name) => adapter.getHeader(name),
447
741
  body: adapter.getBody ? await adapter.getBody() : void 0
448
742
  };
449
743
  }
450
- async function resolvePrice(config, ctx) {
744
+ async function resolvePrice(config, ctx, tokens) {
451
745
  if (typeof config.price !== "function") return config.price;
452
746
  if (!ctx) throw new X402ConfigError("A price function needs the request");
453
747
  let price;
@@ -468,6 +762,11 @@ async function resolvePrice(config, ctx) {
468
762
  )} \u2014 expected a positive amount like "0.01" or a bigint`
469
763
  );
470
764
  }
765
+ if (typeof price === "bigint" && config.scheme === "upto" && hasMixedDecimals(config.network, tokens)) {
766
+ throw new X402ConfigError(
767
+ `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"`
768
+ );
769
+ }
471
770
  return price;
472
771
  }
473
772
  var SETTLE_AMOUNT_HEADER = "payment-settle-amount";
@@ -519,6 +818,17 @@ function invalidSettleAmountResponse(reason) {
519
818
  reason === "meter" ? INVALID_METER_RESPONSE : INVALID_SETTLE_AMOUNT_RESPONSE
520
819
  );
521
820
  }
821
+ function refusingBigints(meter) {
822
+ return async (ctx) => {
823
+ const amount = await meter(ctx);
824
+ if (typeof amount === "bigint") {
825
+ throw new X402ConfigError(
826
+ 'The meter returned a bigint, but the route accepts tokens with different decimals: return an amount like "0.75"'
827
+ );
828
+ }
829
+ return amount;
830
+ };
831
+ }
522
832
  var AMOUNT_FORMAT = /^\d+(?:\.\d+)?$/;
523
833
  async function resolveCharge(verified, settleHeader, response) {
524
834
  if (verified.meter) {
@@ -598,18 +908,27 @@ function priceInUnits(price, decimals, asset) {
598
908
  }
599
909
  function buildPaymentRequired(url, config, resolvedTokens, price) {
600
910
  const network = toEip155Network(config.network);
601
- const accepts = resolvedTokens.map((asset) => {
911
+ const scheme = config.scheme ?? "exact";
912
+ const entries = resolvedTokens.map((asset) => {
602
913
  const decimals = resolveDecimals(config, asset);
603
914
  return {
604
- scheme: config.scheme ?? "exact",
915
+ scheme,
605
916
  network,
606
917
  asset,
607
918
  amount: priceInUnits(price, decimals, asset),
608
919
  payTo: config.payTo,
609
920
  maxTimeoutSeconds: config.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS,
610
- extra: { decimals }
921
+ extra: {
922
+ decimals,
923
+ ...scheme === "upto" && permit2Extra(config.network, asset)
924
+ }
611
925
  };
612
926
  });
927
+ const viaPermit2 = (entry) => "facilitatorAddress" in entry.extra;
928
+ const accepts = [
929
+ ...entries.filter(viaPermit2),
930
+ ...entries.filter((entry) => !viaPermit2(entry))
931
+ ];
613
932
  return {
614
933
  x402Version: 2,
615
934
  resource: {
@@ -617,13 +936,17 @@ function buildPaymentRequired(url, config, resolvedTokens, price) {
617
936
  description: config.description,
618
937
  mimeType: "application/json"
619
938
  },
620
- accepts
939
+ accepts,
940
+ ...accepts.some(viaPermit2) && {
941
+ extensions: EIP2612_GAS_SPONSORING_EXTENSION
942
+ }
621
943
  };
622
944
  }
623
- function decodePaymentPayload(header) {
945
+ function decodePaymentPayload(header, network) {
624
946
  try {
625
- const parsed = JSON.parse(
626
- Buffer.from(header, "base64").toString("utf8")
947
+ const parsed = toFacilitatorPayload(
948
+ JSON.parse(Buffer.from(header, "base64").toString("utf8")),
949
+ network
627
950
  );
628
951
  return typeof parsed === "object" && parsed !== null ? parsed : void 0;
629
952
  } catch {
@@ -642,7 +965,7 @@ function deliveredToken(payload, chainTokens) {
642
965
  return payload.tokenIn ?? chainTokens?.usdp;
643
966
  }
644
967
  function resolveDeliveredAsset(header, config, resolvedTokens) {
645
- const payload = decodePaymentPayload(header);
968
+ const payload = decodePaymentPayload(header, config.network);
646
969
  const target = payload ? deliveredToken(
647
970
  payload,
648
971
  PARALLEL_TOKENS[config.network]
@@ -708,16 +1031,31 @@ function createPaymentGate(config) {
708
1031
  );
709
1032
  }
710
1033
  const client = new FacilitatorClient(config.facilitator);
1034
+ const routes = routeTable(config.routes);
711
1035
  return async (adapter) => {
712
- if (adapter.getMethod().toUpperCase() === "OPTIONS")
713
- return { type: "pass" };
714
- const routeConfig = matchRoute(adapter.getPath(), config.routes);
1036
+ const path = adapter.getPath();
1037
+ const fullPath = adapter.getFullPath?.();
1038
+ let match = findRoute(path, routes);
1039
+ if (!match.route && fullPath !== void 0 && fullPath !== path)
1040
+ match = findRoute(fullPath, routes);
1041
+ const { route: routeConfig, dotSegments, routePath } = match;
715
1042
  if (!routeConfig) return { type: "pass" };
1043
+ if (dotSegments) {
1044
+ return {
1045
+ type: "error",
1046
+ result: jsonResult(400, {
1047
+ error: X402_ERROR_CODES.INVALID_PATH,
1048
+ message: "A paid route is not served on a path with . or .. segments: send the path resolved"
1049
+ })
1050
+ };
1051
+ }
1052
+ if (adapter.getMethod().toUpperCase() === "OPTIONS")
1053
+ return { type: "pass", preflight: true };
716
1054
  const resolvedTokens = resolveAcceptedTokens(routeConfig);
717
- let request = typeof routeConfig.price === "function" ? await requestContext(adapter) : void 0;
1055
+ let request = typeof routeConfig.price === "function" ? await requestContext(adapter, routePath) : void 0;
718
1056
  let price;
719
1057
  try {
720
- price = await resolvePrice(routeConfig, request);
1058
+ price = await resolvePrice(routeConfig, request, resolvedTokens);
721
1059
  } catch (err) {
722
1060
  if (!isPriceRejection(err)) throw err;
723
1061
  const status = Number.isInteger(err.status) && err.status >= 400 && err.status <= 599 ? err.status : 400;
@@ -768,7 +1106,8 @@ function createPaymentGate(config) {
768
1106
  )
769
1107
  };
770
1108
  }
771
- if (routeConfig.meter && !request) request = await requestContext(adapter);
1109
+ const meter = routeConfig.meter && hasMixedDecimals(routeConfig.network, resolvedTokens) ? refusingBigints(routeConfig.meter) : routeConfig.meter;
1110
+ if (meter && !request) request = await requestContext(adapter, routePath);
772
1111
  return {
773
1112
  type: "verified",
774
1113
  scheme: routeConfig.scheme ?? "exact",
@@ -776,7 +1115,7 @@ function createPaymentGate(config) {
776
1115
  decimals: resolveDecimals(routeConfig, asset),
777
1116
  handlerTimeoutMs: handlerTimeoutFor(routeConfig),
778
1117
  settle: (options) => client.settle(paymentHeader, requirements, { ...options, settleToken }),
779
- ...routeConfig.meter && { meter: routeConfig.meter, request }
1118
+ ...meter && { meter, request }
780
1119
  };
781
1120
  };
782
1121
  }
@@ -850,7 +1189,8 @@ function createPaymentMiddleware(config) {
850
1189
  const gate = createPaymentGate(config);
851
1190
  return async (adapter) => {
852
1191
  const gateResult = await gate(adapter);
853
- if (gateResult.type === "pass") return { pass: true };
1192
+ if (gateResult.type === "pass")
1193
+ return gateResult.preflight ? { pass: true, preflight: true } : { pass: true };
854
1194
  if (gateResult.type === "error") return gateResult.result;
855
1195
  let handlerResult;
856
1196
  try {
@@ -902,4 +1242,4 @@ function createPaymentMiddleware(config) {
902
1242
  };
903
1243
  }
904
1244
 
905
- export { FacilitatorClient, HANDLER_TIMEOUT, PARALLEL_TOKENS, SETTLE_AMOUNT_HEADER, createPaymentGate, createPaymentMiddleware, encodeBase64, getDefaultAcceptedTokens, invalidSettleAmountResponse, isJsonContentType, jsonFromResponse, matchRoute, parseJsonBody, parsePrice, raceTimeout, resolveCharge, resolveSettleAmount, settleVerified, settlementFailureResponse, toEip155Network, validateAddress };
1245
+ export { FacilitatorClient, HANDLER_TIMEOUT, PARALLEL_TOKENS, SETTLE_AMOUNT_HEADER, answerAsPreflight, createPaymentGate, createPaymentMiddleware, dropConditionalHeaders, encodeBase64, findRoute, getDefaultAcceptedTokens, invalidSettleAmountResponse, isJsonContentType, jsonFromResponse, normalizeRoutePath, parseJsonBody, parsePrice, preflightInit, raceTimeout, resolveCharge, resolveSettleAmount, routeTable, settleVerified, settlementFailureResponse, toEip155Network, validateAddress };
@@ -3,6 +3,8 @@ var X402_ERROR_CODES = {
3
3
  FACILITATOR_UNAVAILABLE: "FACILITATOR_UNAVAILABLE",
4
4
  FACILITATOR_INVALID_RESPONSE: "FACILITATOR_INVALID_RESPONSE",
5
5
  INVALID_PAYMENT: "INVALID_PAYMENT",
6
+ /** A path with `.` or `..` segments on a paid route, refused with 400. */
7
+ INVALID_PATH: "INVALID_PATH",
6
8
  /** The facilitator did not answer a settle within the client's timeout: the payment may still settle. */
7
9
  SETTLE_TIMEOUT: "SETTLE_TIMEOUT"
8
10
  };