@parallel-protocol/x402 0.5.3 → 0.7.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.
@@ -0,0 +1,755 @@
1
+ 'use strict';
2
+
3
+ var chunkJPMK5YBU_cjs = require('./chunk-JPMK5YBU.cjs');
4
+ var chains = require('@parallel-protocol/chains');
5
+ var zod = require('zod');
6
+
7
+ var PARALLEL_TOKENS = {
8
+ ethereum: {
9
+ usdp: "0x9B3a8f7CEC208e247d97dEE13313690977e24459",
10
+ susdp: "0xd3a452B305C8285c0Dd7b8537665c734d3D279eF",
11
+ usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
12
+ frxUSD: "0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29",
13
+ sfrxUSD: "0xcf62F905562626CfcDD2261162a51fd02Fc9c5b6",
14
+ USDe: "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3",
15
+ sUSDe: "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497"
16
+ },
17
+ base: {
18
+ usdp: "0x76A9A0062ec6712b99B4f63bD2b4270185759dd5",
19
+ susdp: "0x472eD57b376fE400259FB28e5C46eB53f0E3e7E7",
20
+ usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
21
+ USDS: "0x820C137fa70C8691f0e44Dc420a5e53c168921Dc",
22
+ sUSDS: "0x5875eEE11Cf8398102FdAd704C9E96607675467a"
23
+ },
24
+ avalanche: {
25
+ usdp: "0x9eE1963f05553eF838604Dd39403be21ceF26AA4",
26
+ susdp: "0x9d92c21205383651610f90722131655a5b8ed3e0",
27
+ usdc: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
28
+ ygamiUSDC: "0x9fD32FD5e32C6B95483d36C5E724C5C5250Ce010"
29
+ },
30
+ hyperevm: {
31
+ usdp: "0xBE65F0F410A72BeC163dC65d46c83699e957D588",
32
+ susdp: "0x9B3a8f7CEC208e247d97dEE13313690977e24459",
33
+ usdc: "0xb88339cb7199b77e23db6e890353e22632ba630f",
34
+ USDe: "0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34",
35
+ sUSDe: "0x211Cc4DD073734dA055fbF44a2b4667d5E5fE5d2"
36
+ }
37
+ };
38
+ function getDefaultAcceptedTokens(network) {
39
+ const entry = PARALLEL_TOKENS[network];
40
+ if (!entry) return void 0;
41
+ return [entry.usdp, entry.usdc, entry.susdp];
42
+ }
43
+ function getMeteredAcceptedTokens(network) {
44
+ const entry = PARALLEL_TOKENS[network];
45
+ if (!entry) return void 0;
46
+ return [entry.usdp];
47
+ }
48
+ function getTokenDecimals(network, address) {
49
+ return chains.getTokenDecimals(network, address);
50
+ }
51
+
52
+ // src/utils.ts
53
+ var EIP155_NETWORKS = {
54
+ ethereum: "eip155:1",
55
+ base: "eip155:8453",
56
+ avalanche: "eip155:43114",
57
+ hyperevm: "eip155:999"
58
+ };
59
+ function parseDecimalAmount(amount, decimals) {
60
+ const multiplier = 10n ** BigInt(decimals);
61
+ const parts = amount.split(".");
62
+ const whole = parts[0] ?? "0";
63
+ const fraction = (parts[1] ?? "").padEnd(decimals, "0").slice(0, decimals);
64
+ return BigInt(whole) * multiplier + BigInt(fraction || "0");
65
+ }
66
+ function parsePrice(price, decimals = 18) {
67
+ if (typeof price === "bigint") return price;
68
+ const match = price.match(/^(\d+(?:\.\d+)?)$/);
69
+ if (match?.[1]) return parseDecimalAmount(match[1], decimals);
70
+ throw new Error(
71
+ `Invalid price format: "${price}". Expected "0.01" or a bigint.`
72
+ );
73
+ }
74
+ function isJsonContentType(contentType) {
75
+ return /^application\/(?:[\w.-]+\+)?json(?:\s*;.*)?$/i.test(
76
+ contentType?.trim() ?? ""
77
+ );
78
+ }
79
+ function parseJsonBody(contentType, text) {
80
+ if (text === void 0 || !isJsonContentType(contentType)) return void 0;
81
+ try {
82
+ return JSON.parse(text);
83
+ } catch {
84
+ return void 0;
85
+ }
86
+ }
87
+ async function jsonFromResponse(res) {
88
+ const contentType = res.headers.get("content-type") ?? void 0;
89
+ if (!isJsonContentType(contentType)) return void 0;
90
+ try {
91
+ return parseJsonBody(contentType, await res.clone().text());
92
+ } catch {
93
+ return void 0;
94
+ }
95
+ }
96
+ function validateAddress(address) {
97
+ return /^0x[0-9a-fA-F]{40}$/.test(address);
98
+ }
99
+ function toEip155Network(network) {
100
+ if (network.startsWith("eip155:")) return network;
101
+ return EIP155_NETWORKS[network] ?? `eip155:${network}`;
102
+ }
103
+ function encodeBase64(obj) {
104
+ return Buffer.from(JSON.stringify(obj)).toString("base64");
105
+ }
106
+ function decodeBase64(str) {
107
+ return JSON.parse(Buffer.from(str, "base64").toString("utf8"));
108
+ }
109
+ function matchRoute(path, routes) {
110
+ if (routes[path]) return routes[path];
111
+ let bestMatch;
112
+ 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;
117
+ }
118
+ }
119
+ return bestMatch;
120
+ }
121
+ var ethereumAddressSchema = zod.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "Invalid Ethereum address");
122
+ var txHashSchema = zod.z.string().regex(/^0x[0-9a-fA-F]{64}$/);
123
+ var priceSchema = zod.z.union([
124
+ zod.z.bigint().positive("Price must be greater than 0"),
125
+ zod.z.string().regex(/^\d+(?:\.\d+)?$/, 'Price must be in format "0.01"').refine((price) => Number(price) > 0, "Price must be greater than 0"),
126
+ // A per-request price function; its return value is checked per request.
127
+ zod.z.custom(
128
+ (value) => typeof value === "function",
129
+ "Price must be a string, a bigint or a function of the request"
130
+ )
131
+ ]);
132
+ var facilitatorConfigSchema = zod.z.object({
133
+ url: zod.z.string().refine((v) => URL.canParse(v), "Invalid facilitator URL"),
134
+ apiKey: zod.z.string().optional()
135
+ });
136
+ var UPTO_MAX_TIMEOUT_SECONDS = 3600;
137
+ var routeConfigSchema = zod.z.object({
138
+ price: priceSchema,
139
+ network: zod.z.string().min(1, "Network is required"),
140
+ payTo: ethereumAddressSchema,
141
+ acceptedTokens: zod.z.array(ethereumAddressSchema).optional(),
142
+ description: zod.z.string().optional(),
143
+ scheme: zod.z.enum(["exact", "upto"]).optional(),
144
+ maxTimeoutSeconds: zod.z.number().int().positive().optional(),
145
+ meter: zod.z.custom(
146
+ (value) => typeof value === "function",
147
+ "meter must be a function of the response"
148
+ ).optional()
149
+ }).refine(
150
+ (route) => route.meter === void 0 || route.scheme === "upto",
151
+ "meter only applies to upto routes"
152
+ ).refine(
153
+ (route) => route.scheme !== "upto" || (route.maxTimeoutSeconds ?? 300) <= UPTO_MAX_TIMEOUT_SECONDS,
154
+ `maxTimeoutSeconds must be at most ${UPTO_MAX_TIMEOUT_SECONDS} on an upto route`
155
+ ).refine((route) => {
156
+ if (route.scheme !== "upto" || !route.acceptedTokens?.length) return true;
157
+ const usdp = PARALLEL_TOKENS[route.network]?.usdp;
158
+ if (!usdp) return true;
159
+ return route.acceptedTokens.every(
160
+ (token) => token.toLowerCase() === usdp.toLowerCase()
161
+ );
162
+ }, "acceptedTokens on an upto route may only list the network's USDp");
163
+ var paymentMiddlewareConfigSchema = zod.z.object({
164
+ facilitator: facilitatorConfigSchema,
165
+ routes: zod.z.record(zod.z.string(), routeConfigSchema).refine((r) => Object.keys(r).length > 0, "At least one route is required")
166
+ });
167
+ var facilitatorResponseSchema = zod.z.discriminatedUnion("success", [
168
+ zod.z.object({
169
+ success: zod.z.literal(true),
170
+ txHash: txHashSchema,
171
+ route: zod.z.string(),
172
+ chain: zod.z.string(),
173
+ gasSponsored: zod.z.boolean(),
174
+ networkId: zod.z.string(),
175
+ amountSettled: zod.z.string().optional()
176
+ }),
177
+ zod.z.object({
178
+ success: zod.z.literal(false),
179
+ error: zod.z.object({
180
+ code: zod.z.string(),
181
+ message: zod.z.string(),
182
+ hint: zod.z.string().optional()
183
+ })
184
+ })
185
+ ]);
186
+ var verifyResponseSchema = zod.z.object({
187
+ isValid: zod.z.literal(true)
188
+ });
189
+ var settleSuccessSchema = zod.z.object({
190
+ txHash: txHashSchema,
191
+ route: zod.z.string(),
192
+ gasSponsored: zod.z.boolean(),
193
+ // Metered routes: the amount actually charged (smallest unit).
194
+ amountSettled: zod.z.string().optional()
195
+ });
196
+ var settleErrorSchema = zod.z.object({
197
+ error: zod.z.string(),
198
+ message: zod.z.string()
199
+ });
200
+ var settleResponseSchema = zod.z.union([
201
+ settleSuccessSchema,
202
+ settleErrorSchema
203
+ ]);
204
+
205
+ // src/client.ts
206
+ var FACILITATOR_TIMEOUT_MS = 1e4;
207
+ var SETTLE_TIMEOUT_MS = 6e4;
208
+ function decodePaymentHeader(paymentHeader) {
209
+ try {
210
+ return decodeBase64(paymentHeader);
211
+ } catch {
212
+ throw new chunkJPMK5YBU_cjs.X402RuntimeError(chunkJPMK5YBU_cjs.X402_ERROR_CODES.INVALID_PAYMENT);
213
+ }
214
+ }
215
+ var FacilitatorClient = class {
216
+ constructor(config) {
217
+ this.config = config;
218
+ }
219
+ config;
220
+ async verify(paymentHeader, paymentRequirements) {
221
+ const paymentPayload = decodePaymentHeader(paymentHeader);
222
+ let json;
223
+ try {
224
+ const response = await fetch(`${this.config.url}/x402/verify`, {
225
+ method: "POST",
226
+ headers: this.buildHeaders(),
227
+ body: JSON.stringify({ paymentPayload, paymentRequirements }),
228
+ signal: AbortSignal.timeout(FACILITATOR_TIMEOUT_MS)
229
+ });
230
+ 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);
234
+ }
235
+ json = await response.json();
236
+ } catch (e) {
237
+ if (e instanceof chunkJPMK5YBU_cjs.X402RuntimeError) throw e;
238
+ throw new chunkJPMK5YBU_cjs.X402RuntimeError(chunkJPMK5YBU_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
239
+ }
240
+ try {
241
+ verifyResponseSchema.parse(json);
242
+ } catch {
243
+ throw new chunkJPMK5YBU_cjs.X402RuntimeError(chunkJPMK5YBU_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
244
+ }
245
+ }
246
+ async settle(paymentHeader, paymentRequirements, options) {
247
+ const paymentPayload = decodePaymentHeader(paymentHeader);
248
+ let json;
249
+ try {
250
+ const response = await fetch(`${this.config.url}/x402/settle`, {
251
+ method: "POST",
252
+ headers: this.buildHeaders(),
253
+ // settleAmount only travels for metered routes — exact bodies are unchanged.
254
+ body: JSON.stringify({
255
+ paymentPayload,
256
+ paymentRequirements,
257
+ ...options?.amount !== void 0 && {
258
+ settleAmount: options.amount
259
+ }
260
+ }),
261
+ signal: AbortSignal.timeout(SETTLE_TIMEOUT_MS)
262
+ });
263
+ 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 } };
268
+ }
269
+ json = await response.json();
270
+ } catch (e) {
271
+ if (e instanceof chunkJPMK5YBU_cjs.X402RuntimeError) throw e;
272
+ throw new chunkJPMK5YBU_cjs.X402RuntimeError(chunkJPMK5YBU_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
273
+ }
274
+ try {
275
+ const parsed = settleResponseSchema.parse(json);
276
+ if ("txHash" in parsed) {
277
+ return {
278
+ success: true,
279
+ txHash: parsed.txHash,
280
+ route: parsed.route,
281
+ chain: paymentRequirements.network,
282
+ gasSponsored: parsed.gasSponsored,
283
+ networkId: toEip155Network(paymentRequirements.network),
284
+ ...parsed.amountSettled !== void 0 && {
285
+ amountSettled: parsed.amountSettled
286
+ }
287
+ };
288
+ }
289
+ return {
290
+ success: false,
291
+ error: { code: parsed.error, message: parsed.message }
292
+ };
293
+ } catch {
294
+ throw new chunkJPMK5YBU_cjs.X402RuntimeError(chunkJPMK5YBU_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
295
+ }
296
+ }
297
+ async pay(paymentHeader) {
298
+ const headers = this.buildHeaders();
299
+ headers["X-PAYMENT"] = paymentHeader;
300
+ let json;
301
+ try {
302
+ const response = await fetch(`${this.config.url}/x402/pay`, {
303
+ method: "POST",
304
+ headers,
305
+ signal: AbortSignal.timeout(FACILITATOR_TIMEOUT_MS)
306
+ });
307
+ if (!response.ok) {
308
+ throw new chunkJPMK5YBU_cjs.X402RuntimeError(chunkJPMK5YBU_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
309
+ }
310
+ json = await response.json();
311
+ } catch (e) {
312
+ if (e instanceof chunkJPMK5YBU_cjs.X402RuntimeError) throw e;
313
+ throw new chunkJPMK5YBU_cjs.X402RuntimeError(chunkJPMK5YBU_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE);
314
+ }
315
+ try {
316
+ return facilitatorResponseSchema.parse(json);
317
+ } catch {
318
+ throw new chunkJPMK5YBU_cjs.X402RuntimeError(chunkJPMK5YBU_cjs.X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
319
+ }
320
+ }
321
+ buildHeaders() {
322
+ const headers = {
323
+ "Content-Type": "application/json"
324
+ };
325
+ if (this.config.apiKey) {
326
+ headers["X-API-Key"] = this.config.apiKey;
327
+ }
328
+ return headers;
329
+ }
330
+ };
331
+
332
+ // src/timeout.ts
333
+ var HANDLER_TIMEOUT_MS = 3e4;
334
+ var HANDLER_TIMEOUT = /* @__PURE__ */ Symbol("x402:handler-timeout");
335
+ async function raceTimeout(promise, ms = HANDLER_TIMEOUT_MS) {
336
+ let timer;
337
+ const timeout = new Promise((resolve) => {
338
+ timer = setTimeout(() => resolve(HANDLER_TIMEOUT), ms);
339
+ timer.unref?.();
340
+ });
341
+ try {
342
+ return await Promise.race([promise, timeout]);
343
+ } finally {
344
+ clearTimeout(timer);
345
+ }
346
+ }
347
+
348
+ // src/middleware.ts
349
+ var PRICE_FORMAT = /^\d+(?:\.\d+)?$/;
350
+ async function requestContext(adapter) {
351
+ return {
352
+ method: adapter.getMethod(),
353
+ path: adapter.getPath(),
354
+ url: adapter.getUrl(),
355
+ header: (name) => adapter.getHeader(name),
356
+ body: adapter.getBody ? await adapter.getBody() : void 0
357
+ };
358
+ }
359
+ async function resolvePrice(config, ctx) {
360
+ if (typeof config.price !== "function") return config.price;
361
+ if (!ctx) throw new chunkJPMK5YBU_cjs.X402ConfigError("A price function needs the request");
362
+ let price;
363
+ try {
364
+ price = await config.price(ctx);
365
+ } catch (cause) {
366
+ if (chunkJPMK5YBU_cjs.isPriceRejection(cause)) throw cause;
367
+ throw new chunkJPMK5YBU_cjs.X402ConfigError(
368
+ `The price function of route "${config.network}${ctx.path}" threw`,
369
+ cause
370
+ );
371
+ }
372
+ const valid = typeof price === "bigint" ? price > 0n : typeof price === "string" && PRICE_FORMAT.test(price) && Number(price) > 0;
373
+ if (!valid) {
374
+ throw new chunkJPMK5YBU_cjs.X402ConfigError(
375
+ `The price function of route "${config.network}${ctx.path}" returned ${JSON.stringify(
376
+ typeof price === "bigint" ? price.toString() : price
377
+ )} \u2014 expected a positive amount like "0.01" or a bigint`
378
+ );
379
+ }
380
+ return price;
381
+ }
382
+ var SETTLE_AMOUNT_HEADER = "payment-settle-amount";
383
+ var DEFAULT_MAX_TIMEOUT_SECONDS = 300;
384
+ function handlerTimeoutFor(config) {
385
+ if ((config.scheme ?? "exact") !== "upto") return HANDLER_TIMEOUT_MS;
386
+ const seconds = config.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS;
387
+ return Math.max(HANDLER_TIMEOUT_MS, (seconds - 60) * 1e3);
388
+ }
389
+ function resolveSettleAmount(raw, maxAmount, decimals) {
390
+ if (raw === void 0)
391
+ return { kind: "settle", amount: maxAmount.toString() };
392
+ const value = raw.trim();
393
+ let amount;
394
+ try {
395
+ amount = parsePrice(value, decimals);
396
+ } catch {
397
+ return { kind: "invalid", reason: "malformed" };
398
+ }
399
+ const fraction = value.split(".")[1] ?? "";
400
+ if (fraction.length > decimals)
401
+ return { kind: "invalid", reason: "precision" };
402
+ if (amount === 0n) return { kind: "skip" };
403
+ if (amount > maxAmount) return { kind: "invalid", reason: "above-cap" };
404
+ return { kind: "settle", amount: amount.toString() };
405
+ }
406
+ var INVALID_SETTLE_AMOUNT_RESPONSE = {
407
+ error: "INVALID_SETTLE_AMOUNT",
408
+ message: `The handler set an invalid ${SETTLE_AMOUNT_HEADER} header \u2014 expected a decimal amount in token units, formatted like the route price and within the token's precision, at most the route price.`
409
+ };
410
+ var INVALID_METER_RESPONSE = {
411
+ error: "INVALID_SETTLE_AMOUNT",
412
+ message: "The route's meter function failed or returned something that is not an amount; the response was discarded and nothing was charged."
413
+ };
414
+ function invalidSettleAmountResponse(reason) {
415
+ return reason === "meter" ? INVALID_METER_RESPONSE : INVALID_SETTLE_AMOUNT_RESPONSE;
416
+ }
417
+ var AMOUNT_FORMAT = /^\d+(?:\.\d+)?$/;
418
+ async function resolveCharge(verified, settleHeader, response) {
419
+ if (verified.meter) {
420
+ if (!verified.request) {
421
+ throw new chunkJPMK5YBU_cjs.X402ConfigError(
422
+ "A metered payment carries a meter but no request context"
423
+ );
424
+ }
425
+ let result;
426
+ try {
427
+ const body = await raceTimeout(
428
+ response.body(),
429
+ verified.handlerTimeoutMs
430
+ );
431
+ if (body === HANDLER_TIMEOUT) return { kind: "invalid", reason: "meter" };
432
+ result = await verified.meter({
433
+ status: response.status,
434
+ header: response.header,
435
+ body,
436
+ request: verified.request
437
+ });
438
+ } catch {
439
+ return { kind: "invalid", reason: "meter" };
440
+ }
441
+ if (result != null) {
442
+ let amount;
443
+ if (typeof result === "bigint") {
444
+ if (result < 0n) return { kind: "invalid", reason: "meter" };
445
+ amount = result;
446
+ } else if (typeof result === "string" && AMOUNT_FORMAT.test(result)) {
447
+ const fraction = result.split(".")[1] ?? "";
448
+ if (fraction.length > verified.decimals) {
449
+ return { kind: "invalid", reason: "meter" };
450
+ }
451
+ amount = parsePrice(result, verified.decimals);
452
+ } else {
453
+ return { kind: "invalid", reason: "meter" };
454
+ }
455
+ if (amount === 0n) return { kind: "skip" };
456
+ if (amount > verified.maxAmount) amount = verified.maxAmount;
457
+ return { kind: "settle", amount: amount.toString() };
458
+ }
459
+ }
460
+ return resolveSettleAmount(
461
+ settleHeader,
462
+ verified.maxAmount,
463
+ verified.decimals
464
+ );
465
+ }
466
+ function resolveAcceptedTokens(config) {
467
+ if (config.acceptedTokens?.length) {
468
+ return config.acceptedTokens;
469
+ }
470
+ const defaults = config.scheme === "upto" ? getMeteredAcceptedTokens(config.network) : getDefaultAcceptedTokens(config.network);
471
+ if (defaults) return defaults;
472
+ throw new chunkJPMK5YBU_cjs.X402ConfigError(
473
+ `acceptedTokens is required for network "${config.network}" \u2014 no defaults available for this chain`
474
+ );
475
+ }
476
+ function resolveDecimals(config, asset) {
477
+ const decimals = config.decimals ?? getTokenDecimals(config.network, asset);
478
+ if (decimals === void 0) {
479
+ throw new chunkJPMK5YBU_cjs.X402ConfigError(
480
+ `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
+ );
482
+ }
483
+ return decimals;
484
+ }
485
+ function priceInUnits(price, decimals, asset) {
486
+ const units = parsePrice(price, decimals);
487
+ if (units === 0n) {
488
+ throw new chunkJPMK5YBU_cjs.X402ConfigError(
489
+ `Price ${typeof price === "bigint" ? price.toString() : `"${price}"`} is below the precision of token ${asset} (${decimals} decimals)`
490
+ );
491
+ }
492
+ return units.toString();
493
+ }
494
+ function buildPaymentRequired(url, config, resolvedTokens, price) {
495
+ const network = toEip155Network(config.network);
496
+ const accepts = resolvedTokens.map((asset) => {
497
+ const decimals = resolveDecimals(config, asset);
498
+ return {
499
+ scheme: config.scheme ?? "exact",
500
+ network,
501
+ asset,
502
+ amount: priceInUnits(price, decimals, asset),
503
+ payTo: config.payTo,
504
+ maxTimeoutSeconds: config.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS,
505
+ extra: { decimals }
506
+ };
507
+ });
508
+ return {
509
+ x402Version: 2,
510
+ resource: {
511
+ url,
512
+ description: config.description,
513
+ mimeType: "application/json"
514
+ },
515
+ accepts
516
+ };
517
+ }
518
+ function decodePaymentPayload(header) {
519
+ try {
520
+ const parsed = JSON.parse(
521
+ Buffer.from(header, "base64").toString("utf8")
522
+ );
523
+ return typeof parsed === "object" && parsed !== null ? parsed : void 0;
524
+ } catch {
525
+ return void 0;
526
+ }
527
+ }
528
+ function deliveredToken(payload, chainTokens) {
529
+ if (payload.method === "depositWithAuthorization" || payload.payload?.depositAuthorization) {
530
+ return chainTokens?.susdp;
531
+ }
532
+ if (payload.method === "redeemWithAuthorization") return chainTokens?.usdp;
533
+ if (payload.swapParams?.tokenOut) return payload.swapParams.tokenOut;
534
+ if (payload.method === "partialRedeemWithAuthorization") {
535
+ return chainTokens?.usdp;
536
+ }
537
+ return payload.tokenIn ?? chainTokens?.usdp;
538
+ }
539
+ function resolveDeliveredAsset(header, config, resolvedTokens) {
540
+ const payload = decodePaymentPayload(header);
541
+ const target = payload ? deliveredToken(
542
+ payload,
543
+ PARALLEL_TOKENS[config.network]
544
+ ) : void 0;
545
+ const lower = target?.toLowerCase();
546
+ return resolvedTokens.find((token) => token.toLowerCase() === lower) ?? resolvedTokens[0];
547
+ }
548
+ function buildFacilitatorRequirements(config, asset, price) {
549
+ const decimals = resolveDecimals(config, asset);
550
+ const scheme = config.scheme ?? "exact";
551
+ return {
552
+ maxAmountRequired: priceInUnits(price, decimals, asset),
553
+ asset,
554
+ payTo: config.payTo,
555
+ network: config.network,
556
+ // Only metered routes name their scheme — exact requirements are unchanged.
557
+ ...scheme === "upto" && {
558
+ scheme,
559
+ maxTimeoutSeconds: config.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS
560
+ }
561
+ };
562
+ }
563
+ function paymentRequiredResponse(url, config, resolvedTokens, price, error) {
564
+ const paymentRequired = buildPaymentRequired(
565
+ url,
566
+ config,
567
+ resolvedTokens,
568
+ price
569
+ );
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
+ };
580
+ }
581
+ 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
588
+ );
589
+ }
590
+ const client = new FacilitatorClient(config.facilitator);
591
+ return async (adapter) => {
592
+ if (adapter.getMethod().toUpperCase() === "OPTIONS")
593
+ return { type: "pass" };
594
+ const routeConfig = matchRoute(adapter.getPath(), config.routes);
595
+ if (!routeConfig) return { type: "pass" };
596
+ const resolvedTokens = resolveAcceptedTokens(routeConfig);
597
+ let request = typeof routeConfig.price === "function" ? await requestContext(adapter) : void 0;
598
+ let price;
599
+ try {
600
+ price = await resolvePrice(routeConfig, request);
601
+ } catch (err) {
602
+ if (!chunkJPMK5YBU_cjs.isPriceRejection(err)) throw err;
603
+ const status = Number.isInteger(err.status) && err.status >= 400 && err.status <= 599 ? err.status : 400;
604
+ return {
605
+ type: "error",
606
+ result: {
607
+ status,
608
+ headers: { "Content-Type": "application/json" },
609
+ body: { error: err.code, message: err.message }
610
+ }
611
+ };
612
+ }
613
+ const paymentHeader = adapter.getHeader("payment-signature") ?? adapter.getHeader("x-payment");
614
+ if (!paymentHeader) {
615
+ return {
616
+ type: "error",
617
+ result: paymentRequiredResponse(
618
+ adapter.getUrl(),
619
+ routeConfig,
620
+ resolvedTokens,
621
+ price
622
+ )
623
+ };
624
+ }
625
+ const asset = resolveDeliveredAsset(
626
+ paymentHeader,
627
+ routeConfig,
628
+ resolvedTokens
629
+ );
630
+ const requirements = buildFacilitatorRequirements(
631
+ routeConfig,
632
+ asset,
633
+ price
634
+ );
635
+ try {
636
+ await client.verify(paymentHeader, requirements);
637
+ } catch (e) {
638
+ const code = e instanceof chunkJPMK5YBU_cjs.X402RuntimeError ? e.code : chunkJPMK5YBU_cjs.X402_ERROR_CODES.FACILITATOR_UNAVAILABLE;
639
+ return {
640
+ type: "error",
641
+ result: paymentRequiredResponse(
642
+ adapter.getUrl(),
643
+ routeConfig,
644
+ resolvedTokens,
645
+ price,
646
+ code
647
+ )
648
+ };
649
+ }
650
+ if (routeConfig.meter && !request) request = await requestContext(adapter);
651
+ return {
652
+ type: "verified",
653
+ scheme: routeConfig.scheme ?? "exact",
654
+ maxAmount: BigInt(requirements.maxAmountRequired),
655
+ decimals: resolveDecimals(routeConfig, asset),
656
+ 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),
659
+ ...routeConfig.meter && { meter: routeConfig.meter, request }
660
+ };
661
+ };
662
+ }
663
+ function createPaymentMiddleware(config) {
664
+ const gate = createPaymentGate(config);
665
+ return async (adapter) => {
666
+ const gateResult = await gate(adapter);
667
+ if (gateResult.type === "pass") return { pass: true };
668
+ if (gateResult.type === "error") return gateResult.result;
669
+ let handlerResult;
670
+ try {
671
+ handlerResult = await adapter.runHandler({
672
+ timeoutMs: gateResult.handlerTimeoutMs
673
+ });
674
+ } catch {
675
+ return { pass: true, handlerRan: true };
676
+ }
677
+ let raw;
678
+ if (gateResult.scheme === "upto") {
679
+ raw = handlerResult.getResponseHeader?.(SETTLE_AMOUNT_HEADER);
680
+ handlerResult.removeResponseHeader?.(SETTLE_AMOUNT_HEADER);
681
+ }
682
+ if (handlerResult.status >= 200 && handlerResult.status < 300) {
683
+ let settleOptions;
684
+ if (gateResult.scheme === "upto") {
685
+ const resolved = await resolveCharge(gateResult, raw, {
686
+ status: handlerResult.status,
687
+ header: (name) => handlerResult.getResponseHeader?.(name),
688
+ body: async () => handlerResult.getResponseBody?.()
689
+ });
690
+ if (resolved.kind === "invalid") {
691
+ handlerResult.discardResponse();
692
+ return {
693
+ status: 402,
694
+ headers: { "Content-Type": "application/json" },
695
+ body: invalidSettleAmountResponse(resolved.reason)
696
+ };
697
+ }
698
+ if (resolved.kind === "skip") {
699
+ handlerResult.sendResponse();
700
+ return { pass: true, handlerRan: true };
701
+ }
702
+ settleOptions = { amount: resolved.amount };
703
+ }
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) {
721
+ 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
+ };
730
+ }
731
+ }
732
+ handlerResult.sendResponse();
733
+ return { pass: true, handlerRan: true };
734
+ };
735
+ }
736
+
737
+ exports.FacilitatorClient = FacilitatorClient;
738
+ exports.HANDLER_TIMEOUT = HANDLER_TIMEOUT;
739
+ exports.PARALLEL_TOKENS = PARALLEL_TOKENS;
740
+ exports.SETTLE_AMOUNT_HEADER = SETTLE_AMOUNT_HEADER;
741
+ exports.createPaymentGate = createPaymentGate;
742
+ exports.createPaymentMiddleware = createPaymentMiddleware;
743
+ exports.encodeBase64 = encodeBase64;
744
+ exports.getDefaultAcceptedTokens = getDefaultAcceptedTokens;
745
+ exports.invalidSettleAmountResponse = invalidSettleAmountResponse;
746
+ exports.isJsonContentType = isJsonContentType;
747
+ exports.jsonFromResponse = jsonFromResponse;
748
+ exports.matchRoute = matchRoute;
749
+ exports.parseJsonBody = parseJsonBody;
750
+ exports.parsePrice = parsePrice;
751
+ exports.raceTimeout = raceTimeout;
752
+ exports.resolveCharge = resolveCharge;
753
+ exports.resolveSettleAmount = resolveSettleAmount;
754
+ exports.toEip155Network = toEip155Network;
755
+ exports.validateAddress = validateAddress;