@parallel-protocol/x402 0.7.1 → 0.8.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,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkJPMK5YBU_cjs = require('./chunk-JPMK5YBU.cjs');
3
+ var chunkKZZY5LS6_cjs = require('./chunk-KZZY5LS6.cjs');
4
4
  var chains = require('@parallel-protocol/chains');
5
5
  var zod = require('zod');
6
6
 
@@ -129,8 +129,18 @@ var priceSchema = zod.z.union([
129
129
  "Price must be a string, a bigint or a function of the request"
130
130
  )
131
131
  ]);
132
+ var LOCAL_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
133
+ function isFacilitatorUrl(value) {
134
+ if (!URL.canParse(value)) return false;
135
+ const { protocol, hostname } = new URL(value);
136
+ return protocol === "https:" || protocol === "http:" && LOCAL_HOSTNAMES.has(hostname);
137
+ }
138
+ var facilitatorUrlSchema = zod.z.string().refine(
139
+ isFacilitatorUrl,
140
+ "The facilitator URL must use https (plain http only on localhost, 127.0.0.1 or [::1])"
141
+ );
132
142
  var facilitatorConfigSchema = zod.z.object({
133
- url: zod.z.string().refine((v) => URL.canParse(v), "Invalid facilitator URL"),
143
+ url: facilitatorUrlSchema,
134
144
  apiKey: zod.z.string().optional()
135
145
  });
136
146
  var UPTO_MAX_TIMEOUT_SECONDS = 3600;
@@ -172,26 +182,32 @@ var facilitatorResponseSchema = zod.z.discriminatedUnion("success", [
172
182
  chain: zod.z.string(),
173
183
  gasSponsored: zod.z.boolean(),
174
184
  networkId: zod.z.string(),
175
- amountSettled: zod.z.string().optional()
185
+ amountSettled: zod.z.string().optional(),
186
+ alreadySettled: zod.z.boolean().optional()
176
187
  }),
177
188
  zod.z.object({
178
189
  success: zod.z.literal(false),
179
190
  error: zod.z.object({
180
191
  code: zod.z.string(),
181
192
  message: zod.z.string(),
182
- hint: zod.z.string().optional()
193
+ hint: zod.z.string().optional(),
194
+ status: zod.z.number().optional(),
195
+ retryAfter: zod.z.string().optional()
183
196
  })
184
197
  })
185
198
  ]);
186
199
  var verifyResponseSchema = zod.z.object({
187
- isValid: zod.z.literal(true)
200
+ isValid: zod.z.literal(true),
201
+ settleToken: zod.z.string().min(1)
188
202
  });
189
203
  var settleSuccessSchema = zod.z.object({
190
204
  txHash: txHashSchema,
191
205
  route: zod.z.string(),
192
206
  gasSponsored: zod.z.boolean(),
193
207
  // Metered routes: the amount actually charged (smallest unit).
194
- amountSettled: zod.z.string().optional()
208
+ amountSettled: zod.z.string().optional(),
209
+ // The facilitator answered from a recorded outcome, nothing was broadcast.
210
+ alreadySettled: zod.z.boolean().optional()
195
211
  });
196
212
  var settleErrorSchema = zod.z.object({
197
213
  error: zod.z.string(),
@@ -205,71 +221,133 @@ var settleResponseSchema = zod.z.union([
205
221
  // src/client.ts
206
222
  var FACILITATOR_TIMEOUT_MS = 1e4;
207
223
  var SETTLE_TIMEOUT_MS = 6e4;
224
+ var RETRY_AFTER_SHAPE = /^(?:\d{1,10}|[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT)$/;
225
+ async function readErrorBody(response) {
226
+ const json = await response.json().catch(() => null);
227
+ if (typeof json !== "object" || json === null) return {};
228
+ const { error, message } = json;
229
+ return {
230
+ ...typeof error === "string" && { error },
231
+ ...typeof message === "string" && { message }
232
+ };
233
+ }
234
+ function retryAfterOf(response) {
235
+ const value = response.headers.get("retry-after");
236
+ return value !== null && RETRY_AFTER_SHAPE.test(value) ? value : void 0;
237
+ }
238
+ function throwIfRedirect(response) {
239
+ const redirected = response.type === "opaqueredirect" || response.status >= 300 && response.status < 400;
240
+ if (!redirected) return;
241
+ response.body?.cancel().catch(() => {
242
+ });
243
+ throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE, {
244
+ status: response.status
245
+ });
246
+ }
208
247
  function decodePaymentHeader(paymentHeader) {
209
248
  try {
210
249
  return decodeBase64(paymentHeader);
211
250
  } catch {
212
- throw new chunkJPMK5YBU_cjs.X402RuntimeError(chunkJPMK5YBU_cjs.X402_ERROR_CODES.INVALID_PAYMENT);
251
+ throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.INVALID_PAYMENT);
213
252
  }
214
253
  }
215
254
  var FacilitatorClient = class {
255
+ // Checked and kept when the client is built: a config changed afterwards
256
+ // does not move the calls elsewhere.
257
+ url;
258
+ apiKey;
216
259
  constructor(config) {
217
- this.config = config;
260
+ const url = facilitatorUrlSchema.safeParse(config.url);
261
+ if (!url.success) {
262
+ throw new chunkKZZY5LS6_cjs.X402ConfigError(
263
+ url.error.issues[0]?.message ?? "Invalid facilitator URL",
264
+ url.error
265
+ );
266
+ }
267
+ this.url = new URL(url.data).href.replace(/\/+$/, "");
268
+ this.apiKey = config.apiKey;
218
269
  }
219
- config;
220
270
  async verify(paymentHeader, paymentRequirements) {
221
271
  const paymentPayload = decodePaymentHeader(paymentHeader);
222
272
  let json;
223
273
  try {
224
- const response = await fetch(`${this.config.url}/x402/verify`, {
274
+ const response = await fetch(`${this.url}/x402/verify`, {
225
275
  method: "POST",
226
276
  headers: this.buildHeaders(),
227
277
  body: JSON.stringify({ paymentPayload, paymentRequirements }),
278
+ redirect: "manual",
228
279
  signal: AbortSignal.timeout(FACILITATOR_TIMEOUT_MS)
229
280
  });
281
+ throwIfRedirect(response);
230
282
  if (!response.ok) {
231
- const errorBody = await response.json().catch(() => null);
232
- const code = errorBody?.error ?? chunkJPMK5YBU_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE;
233
- throw new chunkJPMK5YBU_cjs.X402RuntimeError(code);
283
+ const body = await readErrorBody(response);
284
+ throw new chunkKZZY5LS6_cjs.X402RuntimeError(
285
+ body.error ?? chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE,
286
+ {
287
+ detail: body.message,
288
+ status: response.status,
289
+ retryAfter: retryAfterOf(response)
290
+ }
291
+ );
234
292
  }
235
293
  json = await response.json();
236
294
  } catch (e) {
237
- if (e instanceof chunkJPMK5YBU_cjs.X402RuntimeError) throw e;
238
- throw new chunkJPMK5YBU_cjs.X402RuntimeError(chunkJPMK5YBU_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
295
+ if (e instanceof chunkKZZY5LS6_cjs.X402RuntimeError) throw e;
296
+ throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
239
297
  }
240
298
  try {
241
- verifyResponseSchema.parse(json);
299
+ const { settleToken } = verifyResponseSchema.parse(json);
300
+ return { settleToken };
242
301
  } catch {
243
- throw new chunkJPMK5YBU_cjs.X402RuntimeError(chunkJPMK5YBU_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
302
+ throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
244
303
  }
245
304
  }
246
305
  async settle(paymentHeader, paymentRequirements, options) {
247
306
  const paymentPayload = decodePaymentHeader(paymentHeader);
248
307
  let json;
249
308
  try {
250
- const response = await fetch(`${this.config.url}/x402/settle`, {
309
+ const response = await fetch(`${this.url}/x402/settle`, {
251
310
  method: "POST",
252
311
  headers: this.buildHeaders(),
253
- // settleAmount only travels for metered routes — exact bodies are unchanged.
312
+ // settleAmount only travels for metered routes; the settle token
313
+ // travels whenever verify issued one.
254
314
  body: JSON.stringify({
255
315
  paymentPayload,
256
316
  paymentRequirements,
257
317
  ...options?.amount !== void 0 && {
258
318
  settleAmount: options.amount
319
+ },
320
+ ...options?.settleToken !== void 0 && {
321
+ settleToken: options.settleToken
259
322
  }
260
323
  }),
324
+ redirect: "manual",
261
325
  signal: AbortSignal.timeout(SETTLE_TIMEOUT_MS)
262
326
  });
327
+ throwIfRedirect(response);
263
328
  if (!response.ok) {
264
- const errorBody = await response.json().catch(() => null);
265
- const code = errorBody?.error ?? "SUBMISSION_FAILED";
266
- const message = errorBody?.message ?? "Settlement failed";
267
- return { success: false, error: { code, message } };
329
+ const body = await readErrorBody(response);
330
+ if (body.error === void 0 && response.status >= 500) {
331
+ throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
332
+ }
333
+ const retryAfter = retryAfterOf(response);
334
+ return {
335
+ success: false,
336
+ error: {
337
+ code: body.error ?? "SUBMISSION_FAILED",
338
+ message: body.message ?? "Settlement failed",
339
+ status: response.status,
340
+ ...retryAfter !== void 0 && { retryAfter }
341
+ }
342
+ };
268
343
  }
269
344
  json = await response.json();
270
345
  } catch (e) {
271
- if (e instanceof chunkJPMK5YBU_cjs.X402RuntimeError) throw e;
272
- throw new chunkJPMK5YBU_cjs.X402RuntimeError(chunkJPMK5YBU_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
346
+ if (e instanceof chunkKZZY5LS6_cjs.X402RuntimeError) throw e;
347
+ if (e instanceof Error && e.name === "TimeoutError") {
348
+ throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.SETTLE_TIMEOUT);
349
+ }
350
+ throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
273
351
  }
274
352
  try {
275
353
  const parsed = settleResponseSchema.parse(json);
@@ -283,7 +361,8 @@ var FacilitatorClient = class {
283
361
  networkId: toEip155Network(paymentRequirements.network),
284
362
  ...parsed.amountSettled !== void 0 && {
285
363
  amountSettled: parsed.amountSettled
286
- }
364
+ },
365
+ ...parsed.alreadySettled && { alreadySettled: true }
287
366
  };
288
367
  }
289
368
  return {
@@ -291,39 +370,45 @@ var FacilitatorClient = class {
291
370
  error: { code: parsed.error, message: parsed.message }
292
371
  };
293
372
  } catch {
294
- throw new chunkJPMK5YBU_cjs.X402RuntimeError(chunkJPMK5YBU_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
373
+ throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
295
374
  }
296
375
  }
376
+ /**
377
+ * @deprecated The Parallel facilitator no longer serves `/x402/pay`, so this
378
+ * call fails with `FACILITATOR_UNAVAILABLE`. Use `verify`, then `settle`.
379
+ */
297
380
  async pay(paymentHeader) {
298
381
  const headers = this.buildHeaders();
299
382
  headers["X-PAYMENT"] = paymentHeader;
300
383
  let json;
301
384
  try {
302
- const response = await fetch(`${this.config.url}/x402/pay`, {
385
+ const response = await fetch(`${this.url}/x402/pay`, {
303
386
  method: "POST",
304
387
  headers,
388
+ redirect: "manual",
305
389
  signal: AbortSignal.timeout(FACILITATOR_TIMEOUT_MS)
306
390
  });
391
+ throwIfRedirect(response);
307
392
  if (!response.ok) {
308
- throw new chunkJPMK5YBU_cjs.X402RuntimeError(chunkJPMK5YBU_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
393
+ throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
309
394
  }
310
395
  json = await response.json();
311
396
  } catch (e) {
312
- if (e instanceof chunkJPMK5YBU_cjs.X402RuntimeError) throw e;
313
- throw new chunkJPMK5YBU_cjs.X402RuntimeError(chunkJPMK5YBU_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
397
+ if (e instanceof chunkKZZY5LS6_cjs.X402RuntimeError) throw e;
398
+ throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
314
399
  }
315
400
  try {
316
401
  return facilitatorResponseSchema.parse(json);
317
402
  } catch {
318
- throw new chunkJPMK5YBU_cjs.X402RuntimeError(chunkJPMK5YBU_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
403
+ throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
319
404
  }
320
405
  }
321
406
  buildHeaders() {
322
407
  const headers = {
323
408
  "Content-Type": "application/json"
324
409
  };
325
- if (this.config.apiKey) {
326
- headers["X-API-Key"] = this.config.apiKey;
410
+ if (this.apiKey) {
411
+ headers["X-API-Key"] = this.apiKey;
327
412
  }
328
413
  return headers;
329
414
  }
@@ -358,20 +443,20 @@ async function requestContext(adapter) {
358
443
  }
359
444
  async function resolvePrice(config, ctx) {
360
445
  if (typeof config.price !== "function") return config.price;
361
- if (!ctx) throw new chunkJPMK5YBU_cjs.X402ConfigError("A price function needs the request");
446
+ if (!ctx) throw new chunkKZZY5LS6_cjs.X402ConfigError("A price function needs the request");
362
447
  let price;
363
448
  try {
364
449
  price = await config.price(ctx);
365
450
  } catch (cause) {
366
- if (chunkJPMK5YBU_cjs.isPriceRejection(cause)) throw cause;
367
- throw new chunkJPMK5YBU_cjs.X402ConfigError(
451
+ if (chunkKZZY5LS6_cjs.isPriceRejection(cause)) throw cause;
452
+ throw new chunkKZZY5LS6_cjs.X402ConfigError(
368
453
  `The price function of route "${config.network}${ctx.path}" threw`,
369
454
  cause
370
455
  );
371
456
  }
372
457
  const valid = typeof price === "bigint" ? price > 0n : typeof price === "string" && PRICE_FORMAT.test(price) && Number(price) > 0;
373
458
  if (!valid) {
374
- throw new chunkJPMK5YBU_cjs.X402ConfigError(
459
+ throw new chunkKZZY5LS6_cjs.X402ConfigError(
375
460
  `The price function of route "${config.network}${ctx.path}" returned ${JSON.stringify(
376
461
  typeof price === "bigint" ? price.toString() : price
377
462
  )} \u2014 expected a positive amount like "0.01" or a bigint`
@@ -381,10 +466,21 @@ async function resolvePrice(config, ctx) {
381
466
  }
382
467
  var SETTLE_AMOUNT_HEADER = "payment-settle-amount";
383
468
  var DEFAULT_MAX_TIMEOUT_SECONDS = 300;
469
+ var SETTLEMENT_RESERVE_SECONDS = 120;
384
470
  function handlerTimeoutFor(config) {
385
471
  if ((config.scheme ?? "exact") !== "upto") return HANDLER_TIMEOUT_MS;
386
472
  const seconds = config.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS;
387
- return Math.max(HANDLER_TIMEOUT_MS, (seconds - 60) * 1e3);
473
+ return Math.max(
474
+ HANDLER_TIMEOUT_MS,
475
+ (seconds - SETTLEMENT_RESERVE_SECONDS) * 1e3
476
+ );
477
+ }
478
+ function jsonResult(status, body, headers = {}) {
479
+ return {
480
+ status,
481
+ headers: { "Content-Type": "application/json", ...headers },
482
+ body
483
+ };
388
484
  }
389
485
  function resolveSettleAmount(raw, maxAmount, decimals) {
390
486
  if (raw === void 0)
@@ -412,13 +508,16 @@ var INVALID_METER_RESPONSE = {
412
508
  message: "The route's meter function failed or returned something that is not an amount; the response was discarded and nothing was charged."
413
509
  };
414
510
  function invalidSettleAmountResponse(reason) {
415
- return reason === "meter" ? INVALID_METER_RESPONSE : INVALID_SETTLE_AMOUNT_RESPONSE;
511
+ return jsonResult(
512
+ 402,
513
+ reason === "meter" ? INVALID_METER_RESPONSE : INVALID_SETTLE_AMOUNT_RESPONSE
514
+ );
416
515
  }
417
516
  var AMOUNT_FORMAT = /^\d+(?:\.\d+)?$/;
418
517
  async function resolveCharge(verified, settleHeader, response) {
419
518
  if (verified.meter) {
420
519
  if (!verified.request) {
421
- throw new chunkJPMK5YBU_cjs.X402ConfigError(
520
+ throw new chunkKZZY5LS6_cjs.X402ConfigError(
422
521
  "A metered payment carries a meter but no request context"
423
522
  );
424
523
  }
@@ -469,14 +568,14 @@ function resolveAcceptedTokens(config) {
469
568
  }
470
569
  const defaults = config.scheme === "upto" ? getMeteredAcceptedTokens(config.network) : getDefaultAcceptedTokens(config.network);
471
570
  if (defaults) return defaults;
472
- throw new chunkJPMK5YBU_cjs.X402ConfigError(
571
+ throw new chunkKZZY5LS6_cjs.X402ConfigError(
473
572
  `acceptedTokens is required for network "${config.network}" \u2014 no defaults available for this chain`
474
573
  );
475
574
  }
476
575
  function resolveDecimals(config, asset) {
477
576
  const decimals = config.decimals ?? getTokenDecimals(config.network, asset);
478
577
  if (decimals === void 0) {
479
- throw new chunkJPMK5YBU_cjs.X402ConfigError(
578
+ throw new chunkKZZY5LS6_cjs.X402ConfigError(
480
579
  `Unknown decimals for token ${asset} on "${config.network}" \u2014 add it to the @parallel-protocol/chains catalog or set the route's \`decimals\` explicitly`
481
580
  );
482
581
  }
@@ -485,7 +584,7 @@ function resolveDecimals(config, asset) {
485
584
  function priceInUnits(price, decimals, asset) {
486
585
  const units = parsePrice(price, decimals);
487
586
  if (units === 0n) {
488
- throw new chunkJPMK5YBU_cjs.X402ConfigError(
587
+ throw new chunkKZZY5LS6_cjs.X402ConfigError(
489
588
  `Price ${typeof price === "bigint" ? price.toString() : `"${price}"`} is below the precision of token ${asset} (${decimals} decimals)`
490
589
  );
491
590
  }
@@ -560,31 +659,46 @@ function buildFacilitatorRequirements(config, asset, price) {
560
659
  }
561
660
  };
562
661
  }
563
- function paymentRequiredResponse(url, config, resolvedTokens, price, error) {
662
+ var CODE_SHAPE = /^[A-Z][A-Z0-9_]{2,63}$/;
663
+ var MESSAGE_MAX_LENGTH = 300;
664
+ function shapedCode(code) {
665
+ return typeof code === "string" && CODE_SHAPE.test(code) ? code : void 0;
666
+ }
667
+ function boundedMessage(message) {
668
+ if (typeof message !== "string") return void 0;
669
+ const oneLine = message.replace(/[\p{Cc}\p{Cf}]+/gu, " ").replace(/\s+/g, " ").trim();
670
+ if (!oneLine) return void 0;
671
+ const chars = Array.from(oneLine);
672
+ return chars.length > MESSAGE_MAX_LENGTH ? `${chars.slice(0, MESSAGE_MAX_LENGTH).join("")}\u2026` : oneLine;
673
+ }
674
+ function paymentRequiredResponse(url, config, resolvedTokens, price, refusal) {
564
675
  const paymentRequired = buildPaymentRequired(
565
676
  url,
566
677
  config,
567
678
  resolvedTokens,
568
679
  price
569
680
  );
570
- if (error) paymentRequired.error = error;
571
- return {
572
- status: 402,
573
- headers: {
574
- "Content-Type": "application/json",
575
- "payment-required": encodeBase64(paymentRequired),
576
- "access-control-expose-headers": "payment-required"
577
- },
578
- body: paymentRequired
579
- };
681
+ if (refusal) {
682
+ paymentRequired.error = shapedCode(refusal.code) ?? chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE;
683
+ const message = boundedMessage(refusal.detail);
684
+ if (message) paymentRequired.message = message;
685
+ }
686
+ return jsonResult(402, paymentRequired, {
687
+ "payment-required": encodeBase64(paymentRequired),
688
+ "access-control-expose-headers": "payment-required",
689
+ ...refusal?.retryAfter !== void 0 && {
690
+ "Retry-After": refusal.retryAfter
691
+ }
692
+ });
580
693
  }
581
694
  function createPaymentGate(config) {
582
- try {
583
- paymentMiddlewareConfigSchema.parse(config);
584
- } catch (cause) {
585
- throw new chunkJPMK5YBU_cjs.X402ConfigError(
586
- "Invalid paymentMiddleware config \u2014 check your routes, facilitator URL and payTo addresses",
587
- cause
695
+ const checked = paymentMiddlewareConfigSchema.safeParse(config);
696
+ if (!checked.success) {
697
+ const issue = checked.error.issues[0];
698
+ const where = issue?.path.length ? ` at ${issue.path.join(".")}` : "";
699
+ throw new chunkKZZY5LS6_cjs.X402ConfigError(
700
+ `Invalid paymentMiddleware config${where}: ${issue?.message ?? "check your routes, facilitator URL and payTo addresses"}`,
701
+ checked.error
588
702
  );
589
703
  }
590
704
  const client = new FacilitatorClient(config.facilitator);
@@ -599,7 +713,7 @@ function createPaymentGate(config) {
599
713
  try {
600
714
  price = await resolvePrice(routeConfig, request);
601
715
  } catch (err) {
602
- if (!chunkJPMK5YBU_cjs.isPriceRejection(err)) throw err;
716
+ if (!chunkKZZY5LS6_cjs.isPriceRejection(err)) throw err;
603
717
  const status = Number.isInteger(err.status) && err.status >= 400 && err.status <= 599 ? err.status : 400;
604
718
  return {
605
719
  type: "error",
@@ -632,10 +746,11 @@ function createPaymentGate(config) {
632
746
  asset,
633
747
  price
634
748
  );
749
+ let settleToken;
635
750
  try {
636
- await client.verify(paymentHeader, requirements);
751
+ ({ settleToken } = await client.verify(paymentHeader, requirements));
637
752
  } catch (e) {
638
- const code = e instanceof chunkJPMK5YBU_cjs.X402RuntimeError ? e.code : chunkJPMK5YBU_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE;
753
+ const refusal = e instanceof chunkKZZY5LS6_cjs.X402RuntimeError ? { code: e.code, detail: e.detail, retryAfter: e.retryAfter } : { code: chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE };
639
754
  return {
640
755
  type: "error",
641
756
  result: paymentRequiredResponse(
@@ -643,7 +758,7 @@ function createPaymentGate(config) {
643
758
  routeConfig,
644
759
  resolvedTokens,
645
760
  price,
646
- code
761
+ refusal
647
762
  )
648
763
  };
649
764
  }
@@ -654,12 +769,71 @@ function createPaymentGate(config) {
654
769
  maxAmount: BigInt(requirements.maxAmountRequired),
655
770
  decimals: resolveDecimals(routeConfig, asset),
656
771
  handlerTimeoutMs: handlerTimeoutFor(routeConfig),
657
- // Exact routes keep the historical two-argument client call.
658
- settle: (options) => options ? client.settle(paymentHeader, requirements, options) : client.settle(paymentHeader, requirements),
772
+ settle: (options) => client.settle(paymentHeader, requirements, { ...options, settleToken }),
659
773
  ...routeConfig.meter && { meter: routeConfig.meter, request }
660
774
  };
661
775
  };
662
776
  }
777
+ var UNKNOWN_SETTLEMENT_MESSAGE = "The payment could not be settled.";
778
+ 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.";
779
+ var AGENT_MESSAGES = {
780
+ [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.",
781
+ [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.",
782
+ [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.",
783
+ SETTLE_IN_PROGRESS: "The facilitator is still settling this payment. Do not sign another payment for this request before checking with the merchant.",
784
+ 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.",
785
+ SETTLE_TOKEN_INVALID: MERCHANT_SETTLEMENT_MESSAGE,
786
+ SETTLE_TOKEN_REQUIRED: MERCHANT_SETTLEMENT_MESSAGE,
787
+ AMOUNT_MISMATCH: "The facilitator settled a different amount than this request asked for. The response was not served: do not sign another payment for this request before checking with the merchant.",
788
+ UNKNOWN: UNKNOWN_SETTLEMENT_MESSAGE
789
+ };
790
+ function settlementFailure(code, failure) {
791
+ const reason = shapedCode(code) ?? "UNKNOWN";
792
+ return {
793
+ settled: false,
794
+ reason,
795
+ message: AGENT_MESSAGES[reason] ?? boundedMessage(failure?.message) ?? UNKNOWN_SETTLEMENT_MESSAGE,
796
+ ...failure?.retryAfter !== void 0 && {
797
+ retryAfter: failure.retryAfter
798
+ }
799
+ };
800
+ }
801
+ var RETRY_ONCE_CODES = /* @__PURE__ */ new Set([
802
+ chunkKZZY5LS6_cjs.X402_ERROR_CODES.SETTLE_TIMEOUT,
803
+ chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE
804
+ ]);
805
+ async function settleVerified(verified, options) {
806
+ let response;
807
+ for (let attempt = 0; response === void 0; attempt++) {
808
+ try {
809
+ response = await verified.settle(options);
810
+ } catch (e) {
811
+ const code = e instanceof chunkKZZY5LS6_cjs.X402RuntimeError ? e.code : "UNKNOWN";
812
+ if (attempt > 0 || !RETRY_ONCE_CODES.has(code)) {
813
+ return settlementFailure(code);
814
+ }
815
+ }
816
+ }
817
+ if (!response.success) {
818
+ return settlementFailure(response.error.code, response.error);
819
+ }
820
+ if (options?.amount !== void 0 && response.amountSettled !== options.amount) {
821
+ return settlementFailure("AMOUNT_MISMATCH");
822
+ }
823
+ return { settled: true, confirmation: response };
824
+ }
825
+ function settlementFailureResponse(failure) {
826
+ const body = {
827
+ error: "PAYMENT_SETTLEMENT_FAILED",
828
+ reason: failure.reason,
829
+ message: failure.message
830
+ };
831
+ return jsonResult(402, body, {
832
+ ...failure.retryAfter !== void 0 && {
833
+ "Retry-After": failure.retryAfter
834
+ }
835
+ });
836
+ }
663
837
  function createPaymentMiddleware(config) {
664
838
  const gate = createPaymentGate(config);
665
839
  return async (adapter) => {
@@ -689,11 +863,7 @@ function createPaymentMiddleware(config) {
689
863
  });
690
864
  if (resolved.kind === "invalid") {
691
865
  handlerResult.discardResponse();
692
- return {
693
- status: 402,
694
- headers: { "Content-Type": "application/json" },
695
- body: invalidSettleAmountResponse(resolved.reason)
696
- };
866
+ return invalidSettleAmountResponse(resolved.reason);
697
867
  }
698
868
  if (resolved.kind === "skip") {
699
869
  handlerResult.sendResponse();
@@ -701,33 +871,19 @@ function createPaymentMiddleware(config) {
701
871
  }
702
872
  settleOptions = { amount: resolved.amount };
703
873
  }
704
- let settled = false;
705
- try {
706
- const settleResponse = settleOptions ? await gateResult.settle(settleOptions) : await gateResult.settle();
707
- if (settleResponse.success) {
708
- handlerResult.setResponseHeader(
709
- "payment-response",
710
- encodeBase64(settleResponse)
711
- );
712
- handlerResult.setResponseHeader(
713
- "access-control-expose-headers",
714
- "payment-response"
715
- );
716
- settled = true;
717
- }
718
- } catch {
719
- }
720
- if (!settled) {
874
+ const outcome = await settleVerified(gateResult, settleOptions);
875
+ if (!outcome.settled) {
721
876
  handlerResult.discardResponse();
722
- return {
723
- status: 402,
724
- headers: { "Content-Type": "application/json" },
725
- body: {
726
- error: "PAYMENT_SETTLEMENT_FAILED",
727
- message: "Payment could not be confirmed after multiple attempts. Please retry."
728
- }
729
- };
877
+ return settlementFailureResponse(outcome);
730
878
  }
879
+ handlerResult.setResponseHeader(
880
+ "payment-response",
881
+ encodeBase64(outcome.confirmation)
882
+ );
883
+ handlerResult.setResponseHeader(
884
+ "access-control-expose-headers",
885
+ "payment-response"
886
+ );
731
887
  }
732
888
  handlerResult.sendResponse();
733
889
  return { pass: true, handlerRan: true };
@@ -751,5 +907,7 @@ exports.parsePrice = parsePrice;
751
907
  exports.raceTimeout = raceTimeout;
752
908
  exports.resolveCharge = resolveCharge;
753
909
  exports.resolveSettleAmount = resolveSettleAmount;
910
+ exports.settleVerified = settleVerified;
911
+ exports.settlementFailureResponse = settlementFailureResponse;
754
912
  exports.toEip155Network = toEip155Network;
755
913
  exports.validateAddress = validateAddress;
package/dist/express.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var chunkPOU7EH3E_cjs = require('./chunk-POU7EH3E.cjs');
4
- require('./chunk-JPMK5YBU.cjs');
3
+ var chunkSNW5Z5HB_cjs = require('./chunk-SNW5Z5HB.cjs');
4
+ require('./chunk-KZZY5LS6.cjs');
5
5
 
6
6
  // src/express/adapter.ts
7
7
  function chunkBytes(args) {
@@ -108,8 +108,8 @@ var ExpressAdapter = class {
108
108
  return originalEnd(...args);
109
109
  });
110
110
  next();
111
- const outcome = await chunkPOU7EH3E_cjs.raceTimeout(endPromise, options?.timeoutMs);
112
- if (outcome === chunkPOU7EH3E_cjs.HANDLER_TIMEOUT) {
111
+ const outcome = await chunkSNW5Z5HB_cjs.raceTimeout(endPromise, options?.timeoutMs);
112
+ if (outcome === chunkSNW5Z5HB_cjs.HANDLER_TIMEOUT) {
113
113
  discarded = true;
114
114
  return {
115
115
  status: 504,
@@ -146,7 +146,7 @@ var ExpressAdapter = class {
146
146
  const chunk = chunkBytes(args);
147
147
  if (chunk) bytes.push(chunk);
148
148
  }
149
- return chunkPOU7EH3E_cjs.parseJsonBody(
149
+ return chunkSNW5Z5HB_cjs.parseJsonBody(
150
150
  contentType,
151
151
  Buffer.concat(bytes).toString("utf8")
152
152
  );
@@ -170,7 +170,7 @@ var ExpressAdapter = class {
170
170
 
171
171
  // src/express/index.ts
172
172
  function paymentMiddleware(config) {
173
- const middleware = chunkPOU7EH3E_cjs.createPaymentMiddleware(config);
173
+ const middleware = chunkSNW5Z5HB_cjs.createPaymentMiddleware(config);
174
174
  return async (req, res, next) => {
175
175
  const result = await middleware(new ExpressAdapter(req, res, next));
176
176
  if ("pass" in result) {
@@ -1,5 +1,5 @@
1
1
  import { Request, Response, NextFunction } from 'express';
2
- import { H as HTTPAdapter, R as RunHandlerOptions, a as RunHandlerResult, P as PaymentMiddlewareConfig } from './types-q_gu5wjz.cjs';
2
+ import { H as HTTPAdapter, R as RunHandlerOptions, a as RunHandlerResult, P as PaymentMiddlewareConfig } from './types-BkGQ2lPA.cjs';
3
3
  import 'viem';
4
4
 
5
5
  declare class ExpressAdapter implements HTTPAdapter {
package/dist/express.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Request, Response, NextFunction } from 'express';
2
- import { H as HTTPAdapter, R as RunHandlerOptions, a as RunHandlerResult, P as PaymentMiddlewareConfig } from './types-q_gu5wjz.js';
2
+ import { H as HTTPAdapter, R as RunHandlerOptions, a as RunHandlerResult, P as PaymentMiddlewareConfig } from './types-BkGQ2lPA.js';
3
3
  import 'viem';
4
4
 
5
5
  declare class ExpressAdapter implements HTTPAdapter {
package/dist/express.js CHANGED
@@ -1,5 +1,5 @@
1
- import { raceTimeout, HANDLER_TIMEOUT, parseJsonBody, createPaymentMiddleware } from './chunk-AENXD4OC.js';
2
- import './chunk-AFRDHGDT.js';
1
+ import { raceTimeout, HANDLER_TIMEOUT, parseJsonBody, createPaymentMiddleware } from './chunk-GXPUE3ES.js';
2
+ import './chunk-QAECRSWF.js';
3
3
 
4
4
  // src/express/adapter.ts
5
5
  function chunkBytes(args) {