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