@parallel-protocol/x402 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
 
@@ -172,26 +172,32 @@ var facilitatorResponseSchema = zod.z.discriminatedUnion("success", [
172
172
  chain: zod.z.string(),
173
173
  gasSponsored: zod.z.boolean(),
174
174
  networkId: zod.z.string(),
175
- amountSettled: zod.z.string().optional()
175
+ amountSettled: zod.z.string().optional(),
176
+ alreadySettled: zod.z.boolean().optional()
176
177
  }),
177
178
  zod.z.object({
178
179
  success: zod.z.literal(false),
179
180
  error: zod.z.object({
180
181
  code: zod.z.string(),
181
182
  message: zod.z.string(),
182
- hint: zod.z.string().optional()
183
+ hint: zod.z.string().optional(),
184
+ status: zod.z.number().optional(),
185
+ retryAfter: zod.z.string().optional()
183
186
  })
184
187
  })
185
188
  ]);
186
189
  var verifyResponseSchema = zod.z.object({
187
- isValid: zod.z.literal(true)
190
+ isValid: zod.z.literal(true),
191
+ settleToken: zod.z.string().min(1)
188
192
  });
189
193
  var settleSuccessSchema = zod.z.object({
190
194
  txHash: txHashSchema,
191
195
  route: zod.z.string(),
192
196
  gasSponsored: zod.z.boolean(),
193
197
  // Metered routes: the amount actually charged (smallest unit).
194
- amountSettled: zod.z.string().optional()
198
+ amountSettled: zod.z.string().optional(),
199
+ // The facilitator answered from a recorded outcome, nothing was broadcast.
200
+ alreadySettled: zod.z.boolean().optional()
195
201
  });
196
202
  var settleErrorSchema = zod.z.object({
197
203
  error: zod.z.string(),
@@ -205,11 +211,25 @@ var settleResponseSchema = zod.z.union([
205
211
  // src/client.ts
206
212
  var FACILITATOR_TIMEOUT_MS = 1e4;
207
213
  var SETTLE_TIMEOUT_MS = 6e4;
214
+ 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)$/;
215
+ async function readErrorBody(response) {
216
+ const json = await response.json().catch(() => null);
217
+ if (typeof json !== "object" || json === null) return {};
218
+ const { error, message } = json;
219
+ return {
220
+ ...typeof error === "string" && { error },
221
+ ...typeof message === "string" && { message }
222
+ };
223
+ }
224
+ function retryAfterOf(response) {
225
+ const value = response.headers.get("retry-after");
226
+ return value !== null && RETRY_AFTER_SHAPE.test(value) ? value : void 0;
227
+ }
208
228
  function decodePaymentHeader(paymentHeader) {
209
229
  try {
210
230
  return decodeBase64(paymentHeader);
211
231
  } catch {
212
- throw new chunkJPMK5YBU_cjs.X402RuntimeError(chunkJPMK5YBU_cjs.X402_ERROR_CODES.INVALID_PAYMENT);
232
+ throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.INVALID_PAYMENT);
213
233
  }
214
234
  }
215
235
  var FacilitatorClient = class {
@@ -228,19 +248,26 @@ var FacilitatorClient = class {
228
248
  signal: AbortSignal.timeout(FACILITATOR_TIMEOUT_MS)
229
249
  });
230
250
  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);
251
+ const body = await readErrorBody(response);
252
+ throw new chunkKZZY5LS6_cjs.X402RuntimeError(
253
+ body.error ?? chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE,
254
+ {
255
+ detail: body.message,
256
+ status: response.status,
257
+ retryAfter: retryAfterOf(response)
258
+ }
259
+ );
234
260
  }
235
261
  json = await response.json();
236
262
  } catch (e) {
237
- if (e instanceof chunkJPMK5YBU_cjs.X402RuntimeError) throw e;
238
- throw new chunkJPMK5YBU_cjs.X402RuntimeError(chunkJPMK5YBU_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
263
+ if (e instanceof chunkKZZY5LS6_cjs.X402RuntimeError) throw e;
264
+ throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
239
265
  }
240
266
  try {
241
- verifyResponseSchema.parse(json);
267
+ const { settleToken } = verifyResponseSchema.parse(json);
268
+ return { settleToken };
242
269
  } catch {
243
- throw new chunkJPMK5YBU_cjs.X402RuntimeError(chunkJPMK5YBU_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
270
+ throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
244
271
  }
245
272
  }
246
273
  async settle(paymentHeader, paymentRequirements, options) {
@@ -250,26 +277,43 @@ var FacilitatorClient = class {
250
277
  const response = await fetch(`${this.config.url}/x402/settle`, {
251
278
  method: "POST",
252
279
  headers: this.buildHeaders(),
253
- // settleAmount only travels for metered routes — exact bodies are unchanged.
280
+ // settleAmount only travels for metered routes; the settle token
281
+ // travels whenever verify issued one.
254
282
  body: JSON.stringify({
255
283
  paymentPayload,
256
284
  paymentRequirements,
257
285
  ...options?.amount !== void 0 && {
258
286
  settleAmount: options.amount
287
+ },
288
+ ...options?.settleToken !== void 0 && {
289
+ settleToken: options.settleToken
259
290
  }
260
291
  }),
261
292
  signal: AbortSignal.timeout(SETTLE_TIMEOUT_MS)
262
293
  });
263
294
  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 } };
295
+ const body = await readErrorBody(response);
296
+ if (body.error === void 0 && response.status >= 500) {
297
+ throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
298
+ }
299
+ const retryAfter = retryAfterOf(response);
300
+ return {
301
+ success: false,
302
+ error: {
303
+ code: body.error ?? "SUBMISSION_FAILED",
304
+ message: body.message ?? "Settlement failed",
305
+ status: response.status,
306
+ ...retryAfter !== void 0 && { retryAfter }
307
+ }
308
+ };
268
309
  }
269
310
  json = await response.json();
270
311
  } catch (e) {
271
- if (e instanceof chunkJPMK5YBU_cjs.X402RuntimeError) throw e;
272
- throw new chunkJPMK5YBU_cjs.X402RuntimeError(chunkJPMK5YBU_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
312
+ if (e instanceof chunkKZZY5LS6_cjs.X402RuntimeError) throw e;
313
+ if (e instanceof Error && e.name === "TimeoutError") {
314
+ throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.SETTLE_TIMEOUT);
315
+ }
316
+ throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
273
317
  }
274
318
  try {
275
319
  const parsed = settleResponseSchema.parse(json);
@@ -283,7 +327,8 @@ var FacilitatorClient = class {
283
327
  networkId: toEip155Network(paymentRequirements.network),
284
328
  ...parsed.amountSettled !== void 0 && {
285
329
  amountSettled: parsed.amountSettled
286
- }
330
+ },
331
+ ...parsed.alreadySettled && { alreadySettled: true }
287
332
  };
288
333
  }
289
334
  return {
@@ -291,7 +336,7 @@ var FacilitatorClient = class {
291
336
  error: { code: parsed.error, message: parsed.message }
292
337
  };
293
338
  } catch {
294
- throw new chunkJPMK5YBU_cjs.X402RuntimeError(chunkJPMK5YBU_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
339
+ throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
295
340
  }
296
341
  }
297
342
  async pay(paymentHeader) {
@@ -305,17 +350,17 @@ var FacilitatorClient = class {
305
350
  signal: AbortSignal.timeout(FACILITATOR_TIMEOUT_MS)
306
351
  });
307
352
  if (!response.ok) {
308
- throw new chunkJPMK5YBU_cjs.X402RuntimeError(chunkJPMK5YBU_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
353
+ throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
309
354
  }
310
355
  json = await response.json();
311
356
  } catch (e) {
312
- if (e instanceof chunkJPMK5YBU_cjs.X402RuntimeError) throw e;
313
- throw new chunkJPMK5YBU_cjs.X402RuntimeError(chunkJPMK5YBU_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
357
+ if (e instanceof chunkKZZY5LS6_cjs.X402RuntimeError) throw e;
358
+ throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
314
359
  }
315
360
  try {
316
361
  return facilitatorResponseSchema.parse(json);
317
362
  } catch {
318
- throw new chunkJPMK5YBU_cjs.X402RuntimeError(chunkJPMK5YBU_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
363
+ throw new chunkKZZY5LS6_cjs.X402RuntimeError(chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
319
364
  }
320
365
  }
321
366
  buildHeaders() {
@@ -358,20 +403,20 @@ async function requestContext(adapter) {
358
403
  }
359
404
  async function resolvePrice(config, ctx) {
360
405
  if (typeof config.price !== "function") return config.price;
361
- if (!ctx) throw new chunkJPMK5YBU_cjs.X402ConfigError("A price function needs the request");
406
+ if (!ctx) throw new chunkKZZY5LS6_cjs.X402ConfigError("A price function needs the request");
362
407
  let price;
363
408
  try {
364
409
  price = await config.price(ctx);
365
410
  } catch (cause) {
366
- if (chunkJPMK5YBU_cjs.isPriceRejection(cause)) throw cause;
367
- throw new chunkJPMK5YBU_cjs.X402ConfigError(
411
+ if (chunkKZZY5LS6_cjs.isPriceRejection(cause)) throw cause;
412
+ throw new chunkKZZY5LS6_cjs.X402ConfigError(
368
413
  `The price function of route "${config.network}${ctx.path}" threw`,
369
414
  cause
370
415
  );
371
416
  }
372
417
  const valid = typeof price === "bigint" ? price > 0n : typeof price === "string" && PRICE_FORMAT.test(price) && Number(price) > 0;
373
418
  if (!valid) {
374
- throw new chunkJPMK5YBU_cjs.X402ConfigError(
419
+ throw new chunkKZZY5LS6_cjs.X402ConfigError(
375
420
  `The price function of route "${config.network}${ctx.path}" returned ${JSON.stringify(
376
421
  typeof price === "bigint" ? price.toString() : price
377
422
  )} \u2014 expected a positive amount like "0.01" or a bigint`
@@ -381,10 +426,21 @@ async function resolvePrice(config, ctx) {
381
426
  }
382
427
  var SETTLE_AMOUNT_HEADER = "payment-settle-amount";
383
428
  var DEFAULT_MAX_TIMEOUT_SECONDS = 300;
429
+ var SETTLEMENT_RESERVE_SECONDS = 120;
384
430
  function handlerTimeoutFor(config) {
385
431
  if ((config.scheme ?? "exact") !== "upto") return HANDLER_TIMEOUT_MS;
386
432
  const seconds = config.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS;
387
- return Math.max(HANDLER_TIMEOUT_MS, (seconds - 60) * 1e3);
433
+ return Math.max(
434
+ HANDLER_TIMEOUT_MS,
435
+ (seconds - SETTLEMENT_RESERVE_SECONDS) * 1e3
436
+ );
437
+ }
438
+ function jsonResult(status, body, headers = {}) {
439
+ return {
440
+ status,
441
+ headers: { "Content-Type": "application/json", ...headers },
442
+ body
443
+ };
388
444
  }
389
445
  function resolveSettleAmount(raw, maxAmount, decimals) {
390
446
  if (raw === void 0)
@@ -412,13 +468,16 @@ var INVALID_METER_RESPONSE = {
412
468
  message: "The route's meter function failed or returned something that is not an amount; the response was discarded and nothing was charged."
413
469
  };
414
470
  function invalidSettleAmountResponse(reason) {
415
- return reason === "meter" ? INVALID_METER_RESPONSE : INVALID_SETTLE_AMOUNT_RESPONSE;
471
+ return jsonResult(
472
+ 402,
473
+ reason === "meter" ? INVALID_METER_RESPONSE : INVALID_SETTLE_AMOUNT_RESPONSE
474
+ );
416
475
  }
417
476
  var AMOUNT_FORMAT = /^\d+(?:\.\d+)?$/;
418
477
  async function resolveCharge(verified, settleHeader, response) {
419
478
  if (verified.meter) {
420
479
  if (!verified.request) {
421
- throw new chunkJPMK5YBU_cjs.X402ConfigError(
480
+ throw new chunkKZZY5LS6_cjs.X402ConfigError(
422
481
  "A metered payment carries a meter but no request context"
423
482
  );
424
483
  }
@@ -469,14 +528,14 @@ function resolveAcceptedTokens(config) {
469
528
  }
470
529
  const defaults = config.scheme === "upto" ? getMeteredAcceptedTokens(config.network) : getDefaultAcceptedTokens(config.network);
471
530
  if (defaults) return defaults;
472
- throw new chunkJPMK5YBU_cjs.X402ConfigError(
531
+ throw new chunkKZZY5LS6_cjs.X402ConfigError(
473
532
  `acceptedTokens is required for network "${config.network}" \u2014 no defaults available for this chain`
474
533
  );
475
534
  }
476
535
  function resolveDecimals(config, asset) {
477
536
  const decimals = config.decimals ?? getTokenDecimals(config.network, asset);
478
537
  if (decimals === void 0) {
479
- throw new chunkJPMK5YBU_cjs.X402ConfigError(
538
+ throw new chunkKZZY5LS6_cjs.X402ConfigError(
480
539
  `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
540
  );
482
541
  }
@@ -485,7 +544,7 @@ function resolveDecimals(config, asset) {
485
544
  function priceInUnits(price, decimals, asset) {
486
545
  const units = parsePrice(price, decimals);
487
546
  if (units === 0n) {
488
- throw new chunkJPMK5YBU_cjs.X402ConfigError(
547
+ throw new chunkKZZY5LS6_cjs.X402ConfigError(
489
548
  `Price ${typeof price === "bigint" ? price.toString() : `"${price}"`} is below the precision of token ${asset} (${decimals} decimals)`
490
549
  );
491
550
  }
@@ -560,29 +619,43 @@ function buildFacilitatorRequirements(config, asset, price) {
560
619
  }
561
620
  };
562
621
  }
563
- function paymentRequiredResponse(url, config, resolvedTokens, price, error) {
622
+ var CODE_SHAPE = /^[A-Z][A-Z0-9_]{2,63}$/;
623
+ var MESSAGE_MAX_LENGTH = 300;
624
+ function shapedCode(code) {
625
+ return typeof code === "string" && CODE_SHAPE.test(code) ? code : void 0;
626
+ }
627
+ function boundedMessage(message) {
628
+ if (typeof message !== "string") return void 0;
629
+ const oneLine = message.replace(/[\p{Cc}\p{Cf}]+/gu, " ").replace(/\s+/g, " ").trim();
630
+ if (!oneLine) return void 0;
631
+ const chars = Array.from(oneLine);
632
+ return chars.length > MESSAGE_MAX_LENGTH ? `${chars.slice(0, MESSAGE_MAX_LENGTH).join("")}\u2026` : oneLine;
633
+ }
634
+ function paymentRequiredResponse(url, config, resolvedTokens, price, refusal) {
564
635
  const paymentRequired = buildPaymentRequired(
565
636
  url,
566
637
  config,
567
638
  resolvedTokens,
568
639
  price
569
640
  );
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
- };
641
+ if (refusal) {
642
+ paymentRequired.error = shapedCode(refusal.code) ?? chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE;
643
+ const message = boundedMessage(refusal.detail);
644
+ if (message) paymentRequired.message = message;
645
+ }
646
+ return jsonResult(402, paymentRequired, {
647
+ "payment-required": encodeBase64(paymentRequired),
648
+ "access-control-expose-headers": "payment-required",
649
+ ...refusal?.retryAfter !== void 0 && {
650
+ "Retry-After": refusal.retryAfter
651
+ }
652
+ });
580
653
  }
581
654
  function createPaymentGate(config) {
582
655
  try {
583
656
  paymentMiddlewareConfigSchema.parse(config);
584
657
  } catch (cause) {
585
- throw new chunkJPMK5YBU_cjs.X402ConfigError(
658
+ throw new chunkKZZY5LS6_cjs.X402ConfigError(
586
659
  "Invalid paymentMiddleware config \u2014 check your routes, facilitator URL and payTo addresses",
587
660
  cause
588
661
  );
@@ -599,7 +672,7 @@ function createPaymentGate(config) {
599
672
  try {
600
673
  price = await resolvePrice(routeConfig, request);
601
674
  } catch (err) {
602
- if (!chunkJPMK5YBU_cjs.isPriceRejection(err)) throw err;
675
+ if (!chunkKZZY5LS6_cjs.isPriceRejection(err)) throw err;
603
676
  const status = Number.isInteger(err.status) && err.status >= 400 && err.status <= 599 ? err.status : 400;
604
677
  return {
605
678
  type: "error",
@@ -632,10 +705,11 @@ function createPaymentGate(config) {
632
705
  asset,
633
706
  price
634
707
  );
708
+ let settleToken;
635
709
  try {
636
- await client.verify(paymentHeader, requirements);
710
+ ({ settleToken } = await client.verify(paymentHeader, requirements));
637
711
  } catch (e) {
638
- const code = e instanceof chunkJPMK5YBU_cjs.X402RuntimeError ? e.code : chunkJPMK5YBU_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE;
712
+ 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
713
  return {
640
714
  type: "error",
641
715
  result: paymentRequiredResponse(
@@ -643,7 +717,7 @@ function createPaymentGate(config) {
643
717
  routeConfig,
644
718
  resolvedTokens,
645
719
  price,
646
- code
720
+ refusal
647
721
  )
648
722
  };
649
723
  }
@@ -654,12 +728,71 @@ function createPaymentGate(config) {
654
728
  maxAmount: BigInt(requirements.maxAmountRequired),
655
729
  decimals: resolveDecimals(routeConfig, asset),
656
730
  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),
731
+ settle: (options) => client.settle(paymentHeader, requirements, { ...options, settleToken }),
659
732
  ...routeConfig.meter && { meter: routeConfig.meter, request }
660
733
  };
661
734
  };
662
735
  }
736
+ var UNKNOWN_SETTLEMENT_MESSAGE = "The payment could not be settled.";
737
+ 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.";
738
+ var AGENT_MESSAGES = {
739
+ [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.",
740
+ [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.",
741
+ [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.",
742
+ SETTLE_IN_PROGRESS: "The facilitator is still settling this payment. Do not sign another payment for this request before checking with the merchant.",
743
+ 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.",
744
+ SETTLE_TOKEN_INVALID: MERCHANT_SETTLEMENT_MESSAGE,
745
+ SETTLE_TOKEN_REQUIRED: MERCHANT_SETTLEMENT_MESSAGE,
746
+ 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.",
747
+ UNKNOWN: UNKNOWN_SETTLEMENT_MESSAGE
748
+ };
749
+ function settlementFailure(code, failure) {
750
+ const reason = shapedCode(code) ?? "UNKNOWN";
751
+ return {
752
+ settled: false,
753
+ reason,
754
+ message: AGENT_MESSAGES[reason] ?? boundedMessage(failure?.message) ?? UNKNOWN_SETTLEMENT_MESSAGE,
755
+ ...failure?.retryAfter !== void 0 && {
756
+ retryAfter: failure.retryAfter
757
+ }
758
+ };
759
+ }
760
+ var RETRY_ONCE_CODES = /* @__PURE__ */ new Set([
761
+ chunkKZZY5LS6_cjs.X402_ERROR_CODES.SETTLE_TIMEOUT,
762
+ chunkKZZY5LS6_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE
763
+ ]);
764
+ async function settleVerified(verified, options) {
765
+ let response;
766
+ for (let attempt = 0; response === void 0; attempt++) {
767
+ try {
768
+ response = await verified.settle(options);
769
+ } catch (e) {
770
+ const code = e instanceof chunkKZZY5LS6_cjs.X402RuntimeError ? e.code : "UNKNOWN";
771
+ if (attempt > 0 || !RETRY_ONCE_CODES.has(code)) {
772
+ return settlementFailure(code);
773
+ }
774
+ }
775
+ }
776
+ if (!response.success) {
777
+ return settlementFailure(response.error.code, response.error);
778
+ }
779
+ if (options?.amount !== void 0 && response.amountSettled !== options.amount) {
780
+ return settlementFailure("AMOUNT_MISMATCH");
781
+ }
782
+ return { settled: true, confirmation: response };
783
+ }
784
+ function settlementFailureResponse(failure) {
785
+ const body = {
786
+ error: "PAYMENT_SETTLEMENT_FAILED",
787
+ reason: failure.reason,
788
+ message: failure.message
789
+ };
790
+ return jsonResult(402, body, {
791
+ ...failure.retryAfter !== void 0 && {
792
+ "Retry-After": failure.retryAfter
793
+ }
794
+ });
795
+ }
663
796
  function createPaymentMiddleware(config) {
664
797
  const gate = createPaymentGate(config);
665
798
  return async (adapter) => {
@@ -689,11 +822,7 @@ function createPaymentMiddleware(config) {
689
822
  });
690
823
  if (resolved.kind === "invalid") {
691
824
  handlerResult.discardResponse();
692
- return {
693
- status: 402,
694
- headers: { "Content-Type": "application/json" },
695
- body: invalidSettleAmountResponse(resolved.reason)
696
- };
825
+ return invalidSettleAmountResponse(resolved.reason);
697
826
  }
698
827
  if (resolved.kind === "skip") {
699
828
  handlerResult.sendResponse();
@@ -701,33 +830,19 @@ function createPaymentMiddleware(config) {
701
830
  }
702
831
  settleOptions = { amount: resolved.amount };
703
832
  }
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) {
833
+ const outcome = await settleVerified(gateResult, settleOptions);
834
+ if (!outcome.settled) {
721
835
  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
- };
836
+ return settlementFailureResponse(outcome);
730
837
  }
838
+ handlerResult.setResponseHeader(
839
+ "payment-response",
840
+ encodeBase64(outcome.confirmation)
841
+ );
842
+ handlerResult.setResponseHeader(
843
+ "access-control-expose-headers",
844
+ "payment-response"
845
+ );
731
846
  }
732
847
  handlerResult.sendResponse();
733
848
  return { pass: true, handlerRan: true };
@@ -751,5 +866,7 @@ exports.parsePrice = parsePrice;
751
866
  exports.raceTimeout = raceTimeout;
752
867
  exports.resolveCharge = resolveCharge;
753
868
  exports.resolveSettleAmount = resolveSettleAmount;
869
+ exports.settleVerified = settleVerified;
870
+ exports.settlementFailureResponse = settlementFailureResponse;
754
871
  exports.toEip155Network = toEip155Network;
755
872
  exports.validateAddress = validateAddress;
@@ -2,7 +2,9 @@
2
2
  var X402_ERROR_CODES = {
3
3
  FACILITATOR_UNAVAILABLE: "FACILITATOR_UNAVAILABLE",
4
4
  FACILITATOR_INVALID_RESPONSE: "FACILITATOR_INVALID_RESPONSE",
5
- INVALID_PAYMENT: "INVALID_PAYMENT"
5
+ INVALID_PAYMENT: "INVALID_PAYMENT",
6
+ /** The facilitator did not answer a settle within the client's timeout: the payment may still settle. */
7
+ SETTLE_TIMEOUT: "SETTLE_TIMEOUT"
6
8
  };
7
9
  var X402ConfigError = class extends Error {
8
10
  constructor(message, cause) {
@@ -12,12 +14,21 @@ var X402ConfigError = class extends Error {
12
14
  }
13
15
  };
14
16
  var X402RuntimeError = class extends Error {
15
- constructor(code) {
17
+ constructor(code, extra) {
16
18
  super(code);
17
19
  this.code = code;
18
20
  this.name = "X402RuntimeError";
21
+ if (extra?.detail !== void 0) this.detail = extra.detail;
22
+ if (extra?.status !== void 0) this.status = extra.status;
23
+ if (extra?.retryAfter !== void 0) this.retryAfter = extra.retryAfter;
19
24
  }
20
25
  code;
26
+ /** The facilitator's message for this code, when it answered with one. */
27
+ detail;
28
+ /** The facilitator's HTTP status, when the error comes from its answer. */
29
+ status;
30
+ /** The facilitator's `Retry-After` header, when it sent one. */
31
+ retryAfter;
21
32
  };
22
33
  var X402PriceRejectedError = class extends Error {
23
34
  constructor(code, message, status = 400) {