@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,7 +1,8 @@
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
+ var paymentCore = require('@parallel-protocol/payment-core');
5
6
  var zod = require('zod');
6
7
 
7
8
  var PARALLEL_TOKENS = {
@@ -35,6 +36,9 @@ var PARALLEL_TOKENS = {
35
36
  sUSDe: "0x211Cc4DD073734dA055fbF44a2b4667d5E5fE5d2"
36
37
  }
37
38
  };
39
+ var USDC_EIP712_DOMAIN = {
40
+ base: { name: "USD Coin", version: "2" }
41
+ };
38
42
  function getDefaultAcceptedTokens(network) {
39
43
  const entry = PARALLEL_TOKENS[network];
40
44
  if (!entry) return void 0;
@@ -48,6 +52,12 @@ function getMeteredAcceptedTokens(network) {
48
52
  function getTokenDecimals(network, address) {
49
53
  return chains.getTokenDecimals(network, address);
50
54
  }
55
+ function hasMixedDecimals(network, tokens) {
56
+ const decimals = new Set(
57
+ tokens?.map((token) => chains.getTokenDecimals(network, token))
58
+ );
59
+ return decimals.size > 1;
60
+ }
51
61
 
52
62
  // src/utils.ts
53
63
  var EIP155_NETWORKS = {
@@ -106,18 +116,252 @@ function encodeBase64(obj) {
106
116
  function decodeBase64(str) {
107
117
  return JSON.parse(Buffer.from(str, "base64").toString("utf8"));
108
118
  }
109
- function matchRoute(path, routes) {
110
- 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) {
111
182
  let bestMatch;
112
183
  let bestLength = 0;
113
- for (const [pattern, config] of Object.entries(routes)) {
114
- if (path.startsWith(`${pattern}/`) && pattern.length > bestLength) {
115
- bestMatch = config;
116
- 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;
117
189
  }
118
190
  }
119
191
  return bestMatch;
120
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
+ }
257
+ function permit2Extra(network, asset) {
258
+ const domain = USDC_EIP712_DOMAIN[network];
259
+ const escrow = chains.getUptoEscrowAddress(network);
260
+ if (!domain || !escrow) return void 0;
261
+ const usdc = PARALLEL_TOKENS[network].usdc;
262
+ if (usdc.toLowerCase() !== asset.toLowerCase()) return void 0;
263
+ return { ...domain, facilitatorAddress: escrow };
264
+ }
265
+ var EIP2612_GAS_SPONSORING_EXTENSION = {
266
+ eip2612GasSponsoring: {
267
+ info: {
268
+ description: "The facilitator accepts EIP-2612 gasless Permit to `Permit2` canonical contract.",
269
+ version: "1"
270
+ },
271
+ schema: {
272
+ $schema: "https://json-schema.org/draft/2020-12/schema",
273
+ type: "object",
274
+ properties: {
275
+ from: {
276
+ type: "string",
277
+ pattern: "^0x[a-fA-F0-9]{40}$",
278
+ description: "The address of the sender."
279
+ },
280
+ asset: {
281
+ type: "string",
282
+ pattern: "^0x[a-fA-F0-9]{40}$",
283
+ description: "The address of the ERC-20 token contract."
284
+ },
285
+ spender: {
286
+ type: "string",
287
+ pattern: "^0x[a-fA-F0-9]{40}$",
288
+ description: "The address of the spender (Canonical Permit2)."
289
+ },
290
+ amount: {
291
+ type: "string",
292
+ pattern: "^[0-9]+$",
293
+ description: "The amount to approve (uint256). Typically MaxUint."
294
+ },
295
+ nonce: {
296
+ type: "string",
297
+ pattern: "^[0-9]+$",
298
+ description: "The current nonce of the sender."
299
+ },
300
+ deadline: {
301
+ type: "string",
302
+ pattern: "^[0-9]+$",
303
+ description: "The timestamp at which the signature expires."
304
+ },
305
+ signature: {
306
+ type: "string",
307
+ pattern: "^0x[a-fA-F0-9]+$",
308
+ description: "The 65-byte concatenated signature (r, s, v) as a hex string."
309
+ },
310
+ version: {
311
+ type: "string",
312
+ pattern: "^[0-9]+(\\.[0-9]+)*$",
313
+ description: "Schema version identifier."
314
+ }
315
+ },
316
+ required: [
317
+ "from",
318
+ "asset",
319
+ "spender",
320
+ "amount",
321
+ "nonce",
322
+ "deadline",
323
+ "signature",
324
+ "version"
325
+ ]
326
+ }
327
+ }
328
+ };
329
+ function signedPermit(info = {}) {
330
+ const { amount, deadline, signature } = info;
331
+ return typeof amount === "string" && typeof deadline === "string" && typeof signature === "string" ? { value: amount, deadline, signature } : void 0;
332
+ }
333
+ function isStandardPermit2Payment(decoded) {
334
+ const payment = decoded;
335
+ return typeof payment?.payload?.permit2Authorization === "object" && payment.payload.permit2Authorization !== null && payment.accepted?.scheme === "upto";
336
+ }
337
+ function toFacilitatorPayload(decoded, network) {
338
+ if (!isStandardPermit2Payment(decoded)) return decoded;
339
+ const { signature, permit2Authorization: auth } = decoded.payload;
340
+ const permit = signedPermit(decoded.extensions?.eip2612GasSponsoring?.info);
341
+ return {
342
+ scheme: "upto",
343
+ network,
344
+ method: "uptoPermit2",
345
+ tokenIn: auth.permitted.token,
346
+ payload: {
347
+ signature,
348
+ authorization: {
349
+ from: auth.from,
350
+ to: auth.witness.to,
351
+ value: auth.permitted.amount,
352
+ validAfter: auth.witness.validAfter,
353
+ validBefore: auth.deadline,
354
+ nonce: paymentCore.permit2NonceToBytes32(auth.nonce)
355
+ },
356
+ ...permit && { permit2612: permit }
357
+ },
358
+ uptoParams: { payTo: auth.witness.to },
359
+ permit2Params: {
360
+ spender: auth.spender,
361
+ facilitator: auth.witness.facilitator
362
+ }
363
+ };
364
+ }
121
365
  var ethereumAddressSchema = zod.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "Invalid Ethereum address");
122
366
  var txHashSchema = zod.z.string().regex(/^0x[0-9a-fA-F]{64}$/);
123
367
  var priceSchema = zod.z.union([
@@ -152,6 +396,7 @@ var routeConfigSchema = zod.z.object({
152
396
  description: zod.z.string().optional(),
153
397
  scheme: zod.z.enum(["exact", "upto"]).optional(),
154
398
  maxTimeoutSeconds: zod.z.number().int().positive().optional(),
399
+ decimals: zod.z.number().int().nonnegative().optional(),
155
400
  meter: zod.z.custom(
156
401
  (value) => typeof value === "function",
157
402
  "meter must be a function of the response"
@@ -173,12 +418,37 @@ var routeConfigSchema = zod.z.object({
173
418
  const usdp = PARALLEL_TOKENS[route.network]?.usdp;
174
419
  if (!usdp) return true;
175
420
  return route.acceptedTokens.every(
176
- (token) => token.toLowerCase() === usdp.toLowerCase()
421
+ (token) => token.toLowerCase() === usdp.toLowerCase() || permit2Extra(route.network, token) !== void 0
177
422
  );
178
- }, "acceptedTokens on an upto route may only list the network's USDp");
423
+ }, "acceptedTokens on an upto route may only list the network's USDp, and its USDC where the escrow takes Permit2 payments").refine(
424
+ (route) => route.scheme !== "upto" || !hasMixedDecimals(route.network, route.acceptedTokens) || typeof route.price !== "bigint" && route.decimals === void 0,
425
+ 'An upto route that accepts tokens with different decimals needs a price like "0.01", not a bigint or an explicit decimals'
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
+ }
179
449
  var paymentMiddlewareConfigSchema = zod.z.object({
180
450
  facilitator: facilitatorConfigSchema,
181
- 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)
182
452
  });
183
453
  var facilitatorResponseSchema = zod.z.discriminatedUnion("success", [
184
454
  zod.z.object({
@@ -212,6 +482,9 @@ var settleSuccessSchema = zod.z.object({
212
482
  gasSponsored: zod.z.boolean(),
213
483
  // Metered routes: the amount actually charged (smallest unit).
214
484
  amountSettled: zod.z.string().optional(),
485
+ // A standard x402 Permit2 payment: the USDp the merchant received for
486
+ // amountSettled (smallest unit), when the facilitator read it.
487
+ amountDelivered: zod.z.string().optional(),
215
488
  // The facilitator answered from a recorded outcome, nothing was broadcast.
216
489
  alreadySettled: zod.z.boolean().optional()
217
490
  });
@@ -246,17 +519,21 @@ function throwIfRedirect(response) {
246
519
  if (!redirected) return;
247
520
  response.body?.cancel().catch(() => {
248
521
  });
249
- 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, {
250
523
  status: response.status
251
524
  });
252
525
  }
253
- function decodePaymentHeader(paymentHeader) {
526
+ function decodePaymentHeader(paymentHeader, network) {
254
527
  try {
255
- return decodeBase64(paymentHeader);
528
+ return toFacilitatorPayload(decodeBase64(paymentHeader), network);
256
529
  } catch {
257
- 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);
258
531
  }
259
532
  }
533
+ function payerOf(paymentPayload) {
534
+ const from = paymentPayload?.payload?.authorization?.from;
535
+ return typeof from === "string" ? from : void 0;
536
+ }
260
537
  var FacilitatorClient = class {
261
538
  // Checked and kept when the client is built: a config changed afterwards
262
539
  // does not move the calls elsewhere.
@@ -265,7 +542,7 @@ var FacilitatorClient = class {
265
542
  constructor(config) {
266
543
  const url = facilitatorUrlSchema.safeParse(config.url);
267
544
  if (!url.success) {
268
- throw new chunkKZZY5LS6_cjs.X402ConfigError(
545
+ throw new chunk4CEMNRCZ_cjs.X402ConfigError(
269
546
  url.error.issues[0]?.message ?? "Invalid facilitator URL",
270
547
  url.error
271
548
  );
@@ -274,7 +551,10 @@ var FacilitatorClient = class {
274
551
  this.apiKey = config.apiKey;
275
552
  }
276
553
  async verify(paymentHeader, paymentRequirements) {
277
- const paymentPayload = decodePaymentHeader(paymentHeader);
554
+ const paymentPayload = decodePaymentHeader(
555
+ paymentHeader,
556
+ paymentRequirements.network
557
+ );
278
558
  let json;
279
559
  try {
280
560
  const response = await fetch(`${this.url}/x402/verify`, {
@@ -287,8 +567,8 @@ var FacilitatorClient = class {
287
567
  throwIfRedirect(response);
288
568
  if (!response.ok) {
289
569
  const body = await readErrorBody(response);
290
- throw new chunkKZZY5LS6_cjs.X402RuntimeError(
291
- 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,
292
572
  {
293
573
  detail: body.message,
294
574
  status: response.status,
@@ -298,18 +578,21 @@ var FacilitatorClient = class {
298
578
  }
299
579
  json = await response.json();
300
580
  } catch (e) {
301
- if (e instanceof chunkKZZY5LS6_cjs.X402RuntimeError) throw e;
302
- 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);
303
583
  }
304
584
  try {
305
585
  const { settleToken } = verifyResponseSchema.parse(json);
306
586
  return { settleToken };
307
587
  } catch {
308
- 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);
309
589
  }
310
590
  }
311
591
  async settle(paymentHeader, paymentRequirements, options) {
312
- const paymentPayload = decodePaymentHeader(paymentHeader);
592
+ const paymentPayload = decodePaymentHeader(
593
+ paymentHeader,
594
+ paymentRequirements.network
595
+ );
313
596
  let json;
314
597
  try {
315
598
  const response = await fetch(`${this.url}/x402/settle`, {
@@ -334,7 +617,7 @@ var FacilitatorClient = class {
334
617
  if (!response.ok) {
335
618
  const body = await readErrorBody(response);
336
619
  if (body.error === void 0) {
337
- 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, {
338
621
  status: response.status
339
622
  });
340
623
  }
@@ -351,26 +634,36 @@ var FacilitatorClient = class {
351
634
  }
352
635
  json = await response.json();
353
636
  } catch (e) {
354
- if (e instanceof chunkKZZY5LS6_cjs.X402RuntimeError) throw e;
637
+ if (e instanceof chunk4CEMNRCZ_cjs.X402RuntimeError) throw e;
355
638
  if (e instanceof Error && e.name === "TimeoutError") {
356
- 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);
357
640
  }
358
- 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);
359
642
  }
360
643
  try {
361
644
  const parsed = settleResponseSchema.parse(json);
362
645
  if ("txHash" in parsed) {
646
+ const txHash = parsed.txHash;
647
+ const networkId = toEip155Network(paymentRequirements.network);
648
+ const payer = payerOf(paymentPayload);
363
649
  return {
364
650
  success: true,
365
- txHash: parsed.txHash,
651
+ txHash,
366
652
  route: parsed.route,
367
653
  chain: paymentRequirements.network,
368
654
  gasSponsored: parsed.gasSponsored,
369
- networkId: toEip155Network(paymentRequirements.network),
655
+ networkId,
370
656
  ...parsed.amountSettled !== void 0 && {
371
657
  amountSettled: parsed.amountSettled
372
658
  },
373
- ...parsed.alreadySettled && { alreadySettled: true }
659
+ ...parsed.amountDelivered !== void 0 && {
660
+ amountDelivered: parsed.amountDelivered
661
+ },
662
+ ...parsed.alreadySettled && { alreadySettled: true },
663
+ transaction: txHash,
664
+ network: networkId,
665
+ amount: parsed.amountSettled ?? paymentRequirements.maxAmountRequired,
666
+ ...payer && { payer }
374
667
  };
375
668
  }
376
669
  return {
@@ -378,7 +671,7 @@ var FacilitatorClient = class {
378
671
  error: { code: parsed.error, message: parsed.message }
379
672
  };
380
673
  } catch {
381
- 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);
382
675
  }
383
676
  }
384
677
  /**
@@ -398,17 +691,17 @@ var FacilitatorClient = class {
398
691
  });
399
692
  throwIfRedirect(response);
400
693
  if (!response.ok) {
401
- 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);
402
695
  }
403
696
  json = await response.json();
404
697
  } catch (e) {
405
- if (e instanceof chunkKZZY5LS6_cjs.X402RuntimeError) throw e;
406
- 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);
407
700
  }
408
701
  try {
409
702
  return facilitatorResponseSchema.parse(json);
410
703
  } catch {
411
- 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);
412
705
  }
413
706
  }
414
707
  buildHeaders() {
@@ -440,36 +733,42 @@ async function raceTimeout(promise, ms = HANDLER_TIMEOUT_MS) {
440
733
 
441
734
  // src/middleware.ts
442
735
  var PRICE_FORMAT = /^\d+(?:\.\d+)?$/;
443
- async function requestContext(adapter) {
736
+ async function requestContext(adapter, routePath) {
444
737
  return {
445
738
  method: adapter.getMethod(),
446
739
  path: adapter.getPath(),
740
+ routePath,
447
741
  url: adapter.getUrl(),
448
742
  header: (name) => adapter.getHeader(name),
449
743
  body: adapter.getBody ? await adapter.getBody() : void 0
450
744
  };
451
745
  }
452
- async function resolvePrice(config, ctx) {
746
+ async function resolvePrice(config, ctx, tokens) {
453
747
  if (typeof config.price !== "function") return config.price;
454
- 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");
455
749
  let price;
456
750
  try {
457
751
  price = await config.price(ctx);
458
752
  } catch (cause) {
459
- if (chunkKZZY5LS6_cjs.isPriceRejection(cause)) throw cause;
460
- throw new chunkKZZY5LS6_cjs.X402ConfigError(
753
+ if (chunk4CEMNRCZ_cjs.isPriceRejection(cause)) throw cause;
754
+ throw new chunk4CEMNRCZ_cjs.X402ConfigError(
461
755
  `The price function of route "${config.network}${ctx.path}" threw`,
462
756
  cause
463
757
  );
464
758
  }
465
759
  const valid = typeof price === "bigint" ? price > 0n : typeof price === "string" && PRICE_FORMAT.test(price) && Number(price) > 0;
466
760
  if (!valid) {
467
- throw new chunkKZZY5LS6_cjs.X402ConfigError(
761
+ throw new chunk4CEMNRCZ_cjs.X402ConfigError(
468
762
  `The price function of route "${config.network}${ctx.path}" returned ${JSON.stringify(
469
763
  typeof price === "bigint" ? price.toString() : price
470
764
  )} \u2014 expected a positive amount like "0.01" or a bigint`
471
765
  );
472
766
  }
767
+ if (typeof price === "bigint" && config.scheme === "upto" && hasMixedDecimals(config.network, tokens)) {
768
+ throw new chunk4CEMNRCZ_cjs.X402ConfigError(
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"`
770
+ );
771
+ }
473
772
  return price;
474
773
  }
475
774
  var SETTLE_AMOUNT_HEADER = "payment-settle-amount";
@@ -521,11 +820,22 @@ function invalidSettleAmountResponse(reason) {
521
820
  reason === "meter" ? INVALID_METER_RESPONSE : INVALID_SETTLE_AMOUNT_RESPONSE
522
821
  );
523
822
  }
823
+ function refusingBigints(meter) {
824
+ return async (ctx) => {
825
+ const amount = await meter(ctx);
826
+ if (typeof amount === "bigint") {
827
+ throw new chunk4CEMNRCZ_cjs.X402ConfigError(
828
+ 'The meter returned a bigint, but the route accepts tokens with different decimals: return an amount like "0.75"'
829
+ );
830
+ }
831
+ return amount;
832
+ };
833
+ }
524
834
  var AMOUNT_FORMAT = /^\d+(?:\.\d+)?$/;
525
835
  async function resolveCharge(verified, settleHeader, response) {
526
836
  if (verified.meter) {
527
837
  if (!verified.request) {
528
- throw new chunkKZZY5LS6_cjs.X402ConfigError(
838
+ throw new chunk4CEMNRCZ_cjs.X402ConfigError(
529
839
  "A metered payment carries a meter but no request context"
530
840
  );
531
841
  }
@@ -576,14 +886,14 @@ function resolveAcceptedTokens(config) {
576
886
  }
577
887
  const defaults = config.scheme === "upto" ? getMeteredAcceptedTokens(config.network) : getDefaultAcceptedTokens(config.network);
578
888
  if (defaults) return defaults;
579
- throw new chunkKZZY5LS6_cjs.X402ConfigError(
889
+ throw new chunk4CEMNRCZ_cjs.X402ConfigError(
580
890
  `acceptedTokens is required for network "${config.network}" \u2014 no defaults available for this chain`
581
891
  );
582
892
  }
583
893
  function resolveDecimals(config, asset) {
584
894
  const decimals = config.decimals ?? getTokenDecimals(config.network, asset);
585
895
  if (decimals === void 0) {
586
- throw new chunkKZZY5LS6_cjs.X402ConfigError(
896
+ throw new chunk4CEMNRCZ_cjs.X402ConfigError(
587
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`
588
898
  );
589
899
  }
@@ -592,7 +902,7 @@ function resolveDecimals(config, asset) {
592
902
  function priceInUnits(price, decimals, asset) {
593
903
  const units = parsePrice(price, decimals);
594
904
  if (units === 0n) {
595
- throw new chunkKZZY5LS6_cjs.X402ConfigError(
905
+ throw new chunk4CEMNRCZ_cjs.X402ConfigError(
596
906
  `Price ${typeof price === "bigint" ? price.toString() : `"${price}"`} is below the precision of token ${asset} (${decimals} decimals)`
597
907
  );
598
908
  }
@@ -600,18 +910,27 @@ function priceInUnits(price, decimals, asset) {
600
910
  }
601
911
  function buildPaymentRequired(url, config, resolvedTokens, price) {
602
912
  const network = toEip155Network(config.network);
603
- const accepts = resolvedTokens.map((asset) => {
913
+ const scheme = config.scheme ?? "exact";
914
+ const entries = resolvedTokens.map((asset) => {
604
915
  const decimals = resolveDecimals(config, asset);
605
916
  return {
606
- scheme: config.scheme ?? "exact",
917
+ scheme,
607
918
  network,
608
919
  asset,
609
920
  amount: priceInUnits(price, decimals, asset),
610
921
  payTo: config.payTo,
611
922
  maxTimeoutSeconds: config.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS,
612
- extra: { decimals }
923
+ extra: {
924
+ decimals,
925
+ ...scheme === "upto" && permit2Extra(config.network, asset)
926
+ }
613
927
  };
614
928
  });
929
+ const viaPermit2 = (entry) => "facilitatorAddress" in entry.extra;
930
+ const accepts = [
931
+ ...entries.filter(viaPermit2),
932
+ ...entries.filter((entry) => !viaPermit2(entry))
933
+ ];
615
934
  return {
616
935
  x402Version: 2,
617
936
  resource: {
@@ -619,13 +938,17 @@ function buildPaymentRequired(url, config, resolvedTokens, price) {
619
938
  description: config.description,
620
939
  mimeType: "application/json"
621
940
  },
622
- accepts
941
+ accepts,
942
+ ...accepts.some(viaPermit2) && {
943
+ extensions: EIP2612_GAS_SPONSORING_EXTENSION
944
+ }
623
945
  };
624
946
  }
625
- function decodePaymentPayload(header) {
947
+ function decodePaymentPayload(header, network) {
626
948
  try {
627
- const parsed = JSON.parse(
628
- Buffer.from(header, "base64").toString("utf8")
949
+ const parsed = toFacilitatorPayload(
950
+ JSON.parse(Buffer.from(header, "base64").toString("utf8")),
951
+ network
629
952
  );
630
953
  return typeof parsed === "object" && parsed !== null ? parsed : void 0;
631
954
  } catch {
@@ -644,7 +967,7 @@ function deliveredToken(payload, chainTokens) {
644
967
  return payload.tokenIn ?? chainTokens?.usdp;
645
968
  }
646
969
  function resolveDeliveredAsset(header, config, resolvedTokens) {
647
- const payload = decodePaymentPayload(header);
970
+ const payload = decodePaymentPayload(header, config.network);
648
971
  const target = payload ? deliveredToken(
649
972
  payload,
650
973
  PARALLEL_TOKENS[config.network]
@@ -687,7 +1010,7 @@ function paymentRequiredResponse(url, config, resolvedTokens, price, refusal) {
687
1010
  price
688
1011
  );
689
1012
  if (refusal) {
690
- 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;
691
1014
  const message = boundedMessage(refusal.detail);
692
1015
  if (message) paymentRequired.message = message;
693
1016
  }
@@ -704,24 +1027,39 @@ function createPaymentGate(config) {
704
1027
  if (!checked.success) {
705
1028
  const issue = checked.error.issues[0];
706
1029
  const where = issue?.path.length ? ` at ${issue.path.join(".")}` : "";
707
- throw new chunkKZZY5LS6_cjs.X402ConfigError(
1030
+ throw new chunk4CEMNRCZ_cjs.X402ConfigError(
708
1031
  `Invalid paymentMiddleware config${where}: ${issue?.message ?? "check your routes, facilitator URL and payTo addresses"}`,
709
1032
  checked.error
710
1033
  );
711
1034
  }
712
1035
  const client = new FacilitatorClient(config.facilitator);
1036
+ const routes = routeTable(config.routes);
713
1037
  return async (adapter) => {
714
- if (adapter.getMethod().toUpperCase() === "OPTIONS")
715
- return { type: "pass" };
716
- 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;
717
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 };
718
1056
  const resolvedTokens = resolveAcceptedTokens(routeConfig);
719
- let request = typeof routeConfig.price === "function" ? await requestContext(adapter) : void 0;
1057
+ let request = typeof routeConfig.price === "function" ? await requestContext(adapter, routePath) : void 0;
720
1058
  let price;
721
1059
  try {
722
- price = await resolvePrice(routeConfig, request);
1060
+ price = await resolvePrice(routeConfig, request, resolvedTokens);
723
1061
  } catch (err) {
724
- if (!chunkKZZY5LS6_cjs.isPriceRejection(err)) throw err;
1062
+ if (!chunk4CEMNRCZ_cjs.isPriceRejection(err)) throw err;
725
1063
  const status = Number.isInteger(err.status) && err.status >= 400 && err.status <= 599 ? err.status : 400;
726
1064
  return {
727
1065
  type: "error",
@@ -758,7 +1096,7 @@ function createPaymentGate(config) {
758
1096
  try {
759
1097
  ({ settleToken } = await client.verify(paymentHeader, requirements));
760
1098
  } catch (e) {
761
- 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 };
762
1100
  return {
763
1101
  type: "error",
764
1102
  result: paymentRequiredResponse(
@@ -770,7 +1108,8 @@ function createPaymentGate(config) {
770
1108
  )
771
1109
  };
772
1110
  }
773
- if (routeConfig.meter && !request) request = await requestContext(adapter);
1111
+ const meter = routeConfig.meter && hasMixedDecimals(routeConfig.network, resolvedTokens) ? refusingBigints(routeConfig.meter) : routeConfig.meter;
1112
+ if (meter && !request) request = await requestContext(adapter, routePath);
774
1113
  return {
775
1114
  type: "verified",
776
1115
  scheme: routeConfig.scheme ?? "exact",
@@ -778,7 +1117,7 @@ function createPaymentGate(config) {
778
1117
  decimals: resolveDecimals(routeConfig, asset),
779
1118
  handlerTimeoutMs: handlerTimeoutFor(routeConfig),
780
1119
  settle: (options) => client.settle(paymentHeader, requirements, { ...options, settleToken }),
781
- ...routeConfig.meter && { meter: routeConfig.meter, request }
1120
+ ...meter && { meter, request }
782
1121
  };
783
1122
  };
784
1123
  }
@@ -786,9 +1125,9 @@ var DEFAULT_SETTLEMENT_MESSAGE = "The payment could not be settled.";
786
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.";
787
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.";
788
1127
  var AGENT_MESSAGES = {
789
- [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.",
790
- [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.",
791
- [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.",
792
1131
  SETTLE_IN_PROGRESS: "The facilitator is still settling this payment. Do not sign another payment for this request before checking with the merchant.",
793
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.",
794
1133
  SETTLE_TOKEN_INVALID: MERCHANT_SETTLEMENT_MESSAGE,
@@ -813,8 +1152,8 @@ function settlementFailure(code, failure) {
813
1152
  };
814
1153
  }
815
1154
  var RETRY_ONCE_CODES = /* @__PURE__ */ new Set([
816
- chunkKZZY5LS6_cjs.X402_ERROR_CODES.SETTLE_TIMEOUT,
817
- chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE
1155
+ chunk4CEMNRCZ_cjs.X402_ERROR_CODES.SETTLE_TIMEOUT,
1156
+ chunk4CEMNRCZ_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE
818
1157
  ]);
819
1158
  async function settleVerified(verified, options) {
820
1159
  let response;
@@ -822,7 +1161,7 @@ async function settleVerified(verified, options) {
822
1161
  try {
823
1162
  response = await verified.settle(options);
824
1163
  } catch (e) {
825
- const code = e instanceof chunkKZZY5LS6_cjs.X402RuntimeError ? e.code : "UNKNOWN";
1164
+ const code = e instanceof chunk4CEMNRCZ_cjs.X402RuntimeError ? e.code : "UNKNOWN";
826
1165
  if (attempt > 0 || !RETRY_ONCE_CODES.has(code)) {
827
1166
  return settlementFailure(code);
828
1167
  }
@@ -852,7 +1191,8 @@ function createPaymentMiddleware(config) {
852
1191
  const gate = createPaymentGate(config);
853
1192
  return async (adapter) => {
854
1193
  const gateResult = await gate(adapter);
855
- if (gateResult.type === "pass") return { pass: true };
1194
+ if (gateResult.type === "pass")
1195
+ return gateResult.preflight ? { pass: true, preflight: true } : { pass: true };
856
1196
  if (gateResult.type === "error") return gateResult.result;
857
1197
  let handlerResult;
858
1198
  try {
@@ -908,19 +1248,24 @@ exports.FacilitatorClient = FacilitatorClient;
908
1248
  exports.HANDLER_TIMEOUT = HANDLER_TIMEOUT;
909
1249
  exports.PARALLEL_TOKENS = PARALLEL_TOKENS;
910
1250
  exports.SETTLE_AMOUNT_HEADER = SETTLE_AMOUNT_HEADER;
1251
+ exports.answerAsPreflight = answerAsPreflight;
911
1252
  exports.createPaymentGate = createPaymentGate;
912
1253
  exports.createPaymentMiddleware = createPaymentMiddleware;
1254
+ exports.dropConditionalHeaders = dropConditionalHeaders;
913
1255
  exports.encodeBase64 = encodeBase64;
1256
+ exports.findRoute = findRoute;
914
1257
  exports.getDefaultAcceptedTokens = getDefaultAcceptedTokens;
915
1258
  exports.invalidSettleAmountResponse = invalidSettleAmountResponse;
916
1259
  exports.isJsonContentType = isJsonContentType;
917
1260
  exports.jsonFromResponse = jsonFromResponse;
918
- exports.matchRoute = matchRoute;
1261
+ exports.normalizeRoutePath = normalizeRoutePath;
919
1262
  exports.parseJsonBody = parseJsonBody;
920
1263
  exports.parsePrice = parsePrice;
1264
+ exports.preflightInit = preflightInit;
921
1265
  exports.raceTimeout = raceTimeout;
922
1266
  exports.resolveCharge = resolveCharge;
923
1267
  exports.resolveSettleAmount = resolveSettleAmount;
1268
+ exports.routeTable = routeTable;
924
1269
  exports.settleVerified = settleVerified;
925
1270
  exports.settlementFailureResponse = settlementFailureResponse;
926
1271
  exports.toEip155Network = toEip155Network;