@agentcash/router 1.0.0 → 1.1.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.
- package/dist/index.cjs +486 -97
- package/dist/index.d.cts +46 -7
- package/dist/index.d.ts +46 -7
- package/dist/index.js +486 -97
- package/package.json +14 -1
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,204 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
30
|
));
|
|
31
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
32
|
|
|
33
|
+
// src/protocols/evm.ts
|
|
34
|
+
function isEvmNetwork(network) {
|
|
35
|
+
return network.startsWith("eip155:");
|
|
36
|
+
}
|
|
37
|
+
function filterEvmNetworks(networks) {
|
|
38
|
+
return networks.filter(isEvmNetwork);
|
|
39
|
+
}
|
|
40
|
+
function buildEvmExactOptions(accepts, price) {
|
|
41
|
+
return accepts.filter(
|
|
42
|
+
(accept) => accept.scheme === "exact" && isEvmNetwork(accept.network)
|
|
43
|
+
).map(({ network, payTo }) => ({
|
|
44
|
+
scheme: "exact",
|
|
45
|
+
network,
|
|
46
|
+
price,
|
|
47
|
+
payTo
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
var init_evm = __esm({
|
|
51
|
+
"src/protocols/evm.ts"() {
|
|
52
|
+
"use strict";
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// src/protocols/solana.ts
|
|
57
|
+
function isSolanaNetwork(network) {
|
|
58
|
+
return network.startsWith("solana:");
|
|
59
|
+
}
|
|
60
|
+
function filterSolanaNetworks(networks) {
|
|
61
|
+
return networks.filter(isSolanaNetwork);
|
|
62
|
+
}
|
|
63
|
+
function buildSolanaExactOptions(accepts, price) {
|
|
64
|
+
return accepts.filter(
|
|
65
|
+
(accept) => accept.scheme === "exact" && isSolanaNetwork(accept.network)
|
|
66
|
+
).map(({ network, payTo }) => ({
|
|
67
|
+
scheme: "exact",
|
|
68
|
+
network,
|
|
69
|
+
price,
|
|
70
|
+
payTo
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
function hasSolanaAccepts(accepts) {
|
|
74
|
+
return accepts.some((accept) => isSolanaNetwork(accept.network));
|
|
75
|
+
}
|
|
76
|
+
async function enrichRequirementsWithFacilitatorAccepts(facilitator, resource, requirements) {
|
|
77
|
+
if (!facilitator.url) {
|
|
78
|
+
throw new Error(`Facilitator for ${facilitator.network} is missing a URL for /accepts`);
|
|
79
|
+
}
|
|
80
|
+
const authHeaders = await getAcceptsHeadersForFacilitator(facilitator);
|
|
81
|
+
const response = await fetch(`${facilitator.url.replace(/\/+$/, "")}/accepts`, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: {
|
|
84
|
+
...authHeaders,
|
|
85
|
+
"content-type": "application/json"
|
|
86
|
+
},
|
|
87
|
+
body: JSON.stringify({
|
|
88
|
+
x402Version: 2,
|
|
89
|
+
resource,
|
|
90
|
+
accepts: requirements
|
|
91
|
+
})
|
|
92
|
+
});
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
throw new Error(`Facilitator /accepts failed with status ${response.status}`);
|
|
95
|
+
}
|
|
96
|
+
const body = await response.json();
|
|
97
|
+
if (!Array.isArray(body.accepts)) {
|
|
98
|
+
throw new Error("Facilitator /accepts response did not include accepts");
|
|
99
|
+
}
|
|
100
|
+
return body.accepts;
|
|
101
|
+
}
|
|
102
|
+
function isSolanaRequirement(requirement) {
|
|
103
|
+
return isSolanaNetwork(requirement.network);
|
|
104
|
+
}
|
|
105
|
+
var init_solana = __esm({
|
|
106
|
+
"src/protocols/solana.ts"() {
|
|
107
|
+
"use strict";
|
|
108
|
+
init_x402_facilitators();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// src/x402-facilitators.ts
|
|
113
|
+
function getResolvedX402Facilitator(config, network, defaultEvmFacilitator) {
|
|
114
|
+
const family = getNetworkFamily(network);
|
|
115
|
+
if (!family) return null;
|
|
116
|
+
const target = resolveX402FacilitatorTarget(config, network, defaultEvmFacilitator);
|
|
117
|
+
const resolvedConfig = normalizeFacilitatorTarget(target);
|
|
118
|
+
return {
|
|
119
|
+
family,
|
|
120
|
+
network,
|
|
121
|
+
url: resolvedConfig.url,
|
|
122
|
+
config: resolvedConfig
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function getResolvedX402Facilitators(config, networks, defaultEvmFacilitator) {
|
|
126
|
+
return Object.fromEntries(
|
|
127
|
+
networks.flatMap((network) => {
|
|
128
|
+
const facilitator = getResolvedX402Facilitator(config, network, defaultEvmFacilitator);
|
|
129
|
+
return facilitator ? [[network, facilitator]] : [];
|
|
130
|
+
})
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
function getResolvedX402FacilitatorGroups(facilitatorsByNetwork) {
|
|
134
|
+
const groups = [];
|
|
135
|
+
for (const facilitator of Object.values(facilitatorsByNetwork)) {
|
|
136
|
+
const existing = groups.find(
|
|
137
|
+
(group) => group.family === facilitator.family && sameFacilitatorConfig(group.config, facilitator.config)
|
|
138
|
+
);
|
|
139
|
+
if (existing) {
|
|
140
|
+
existing.networks.push(facilitator.network);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
groups.push({
|
|
144
|
+
family: facilitator.family,
|
|
145
|
+
config: facilitator.config,
|
|
146
|
+
networks: [facilitator.network]
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return groups;
|
|
150
|
+
}
|
|
151
|
+
function getFacilitatorForRequirement(facilitatorsByNetwork, requirement) {
|
|
152
|
+
return facilitatorsByNetwork?.[requirement.network];
|
|
153
|
+
}
|
|
154
|
+
function sameResolvedX402Facilitator(a, b) {
|
|
155
|
+
return sameFacilitatorConfig(a.config, b.config);
|
|
156
|
+
}
|
|
157
|
+
async function getAcceptsHeadersForFacilitator(facilitator) {
|
|
158
|
+
if (facilitator.config.createAcceptsHeaders) {
|
|
159
|
+
return facilitator.config.createAcceptsHeaders();
|
|
160
|
+
}
|
|
161
|
+
if (facilitator.config.createAuthHeaders) {
|
|
162
|
+
const headers = await facilitator.config.createAuthHeaders();
|
|
163
|
+
return headers.supported;
|
|
164
|
+
}
|
|
165
|
+
return {};
|
|
166
|
+
}
|
|
167
|
+
function resolveX402FacilitatorTarget(config, network, defaultEvmFacilitator) {
|
|
168
|
+
return (isSolanaNetwork(network) ? config.x402?.facilitators?.solana : void 0) ?? (isEvmNetwork(network) ? config.x402?.facilitators?.evm : void 0) ?? (isSolanaNetwork(network) ? DEFAULT_SOLANA_FACILITATOR_URL : defaultEvmFacilitator);
|
|
169
|
+
}
|
|
170
|
+
function normalizeFacilitatorTarget(target) {
|
|
171
|
+
return typeof target === "string" ? { url: target } : target;
|
|
172
|
+
}
|
|
173
|
+
function getNetworkFamily(network) {
|
|
174
|
+
if (isEvmNetwork(network)) return "evm";
|
|
175
|
+
if (isSolanaNetwork(network)) return "solana";
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
function sameFacilitatorConfig(a, b) {
|
|
179
|
+
return a.url === b.url && a.createAuthHeaders === b.createAuthHeaders && a.createAcceptsHeaders === b.createAcceptsHeaders;
|
|
180
|
+
}
|
|
181
|
+
var DEFAULT_SOLANA_FACILITATOR_URL;
|
|
182
|
+
var init_x402_facilitators = __esm({
|
|
183
|
+
"src/x402-facilitators.ts"() {
|
|
184
|
+
"use strict";
|
|
185
|
+
init_evm();
|
|
186
|
+
init_solana();
|
|
187
|
+
DEFAULT_SOLANA_FACILITATOR_URL = "https://facilitator.corbits.dev";
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// src/x402-config.ts
|
|
192
|
+
async function resolvePayToValue(payTo, request, fallback) {
|
|
193
|
+
if (!payTo) return fallback;
|
|
194
|
+
if (typeof payTo === "string") return payTo;
|
|
195
|
+
return payTo(request);
|
|
196
|
+
}
|
|
197
|
+
function getConfiguredX402Accepts(config) {
|
|
198
|
+
if (config.x402?.accepts?.length) {
|
|
199
|
+
return [...config.x402.accepts];
|
|
200
|
+
}
|
|
201
|
+
return [
|
|
202
|
+
{
|
|
203
|
+
scheme: "exact",
|
|
204
|
+
network: config.network ?? "eip155:8453",
|
|
205
|
+
payTo: config.payeeAddress
|
|
206
|
+
}
|
|
207
|
+
];
|
|
208
|
+
}
|
|
209
|
+
function getConfiguredX402Networks(config) {
|
|
210
|
+
return [...new Set(getConfiguredX402Accepts(config).map((accept) => accept.network))];
|
|
211
|
+
}
|
|
212
|
+
async function resolveX402Accepts(request, routeEntry, accepts, fallbackPayTo) {
|
|
213
|
+
return Promise.all(
|
|
214
|
+
accepts.map(async (accept) => ({
|
|
215
|
+
network: accept.network,
|
|
216
|
+
scheme: accept.scheme ?? "exact",
|
|
217
|
+
payTo: await resolvePayToValue(accept.payTo ?? routeEntry.payTo, request, fallbackPayTo),
|
|
218
|
+
...accept.asset ? { asset: accept.asset } : {},
|
|
219
|
+
...accept.decimals !== void 0 ? { decimals: accept.decimals } : {},
|
|
220
|
+
...accept.maxTimeoutSeconds !== void 0 ? { maxTimeoutSeconds: accept.maxTimeoutSeconds } : {},
|
|
221
|
+
...accept.extra ? { extra: accept.extra } : {}
|
|
222
|
+
}))
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
var init_x402_config = __esm({
|
|
226
|
+
"src/x402-config.ts"() {
|
|
227
|
+
"use strict";
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
33
231
|
// src/server.ts
|
|
34
232
|
var server_exports = {};
|
|
35
233
|
__export(server_exports, {
|
|
@@ -41,32 +239,59 @@ async function createX402Server(config) {
|
|
|
41
239
|
const { bazaarResourceServerExtension } = await import("@x402/extensions/bazaar");
|
|
42
240
|
const { siwxResourceServerExtension } = await import("@x402/extensions/sign-in-with-x");
|
|
43
241
|
const { facilitator: defaultFacilitator } = await import("@coinbase/x402");
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
242
|
+
const configuredNetworks = getConfiguredX402Networks(config);
|
|
243
|
+
const facilitatorsByNetwork = getResolvedX402Facilitators(
|
|
244
|
+
config,
|
|
245
|
+
configuredNetworks,
|
|
246
|
+
defaultFacilitator
|
|
247
|
+
);
|
|
248
|
+
const evmNetworks = filterEvmNetworks(configuredNetworks);
|
|
249
|
+
const svmNetworks = filterSolanaNetworks(configuredNetworks);
|
|
250
|
+
const facilitatorClients = createFacilitatorClients(facilitatorsByNetwork, HTTPFacilitatorClient);
|
|
251
|
+
const server = new x402ResourceServer(
|
|
252
|
+
facilitatorClients.length === 1 ? facilitatorClients[0] : facilitatorClients
|
|
253
|
+
);
|
|
254
|
+
if (evmNetworks.length > 0) {
|
|
255
|
+
registerExactEvmScheme(server, { networks: evmNetworks });
|
|
256
|
+
}
|
|
257
|
+
if (svmNetworks.length > 0) {
|
|
258
|
+
const { registerExactSvmScheme } = await import("@x402/svm/exact/server");
|
|
259
|
+
registerExactSvmScheme(server, { networks: svmNetworks });
|
|
260
|
+
}
|
|
51
261
|
server.registerExtension(bazaarResourceServerExtension);
|
|
52
262
|
server.registerExtension(siwxResourceServerExtension);
|
|
53
263
|
const initPromise = server.initialize();
|
|
54
|
-
return {
|
|
264
|
+
return {
|
|
265
|
+
server,
|
|
266
|
+
initPromise,
|
|
267
|
+
facilitatorsByNetwork
|
|
268
|
+
};
|
|
55
269
|
}
|
|
56
|
-
function cachedClient(inner,
|
|
270
|
+
function cachedClient(inner, networks) {
|
|
57
271
|
return {
|
|
58
272
|
verify: inner.verify.bind(inner),
|
|
59
273
|
settle: inner.settle.bind(inner),
|
|
60
274
|
getSupported: async () => ({
|
|
61
|
-
kinds:
|
|
275
|
+
kinds: networks.map((network) => ({ x402Version: 2, scheme: "exact", network })),
|
|
62
276
|
extensions: [],
|
|
63
277
|
signers: {}
|
|
64
278
|
})
|
|
65
279
|
};
|
|
66
280
|
}
|
|
281
|
+
function createFacilitatorClients(facilitatorsByNetwork, HTTPFacilitatorClient) {
|
|
282
|
+
const groups = getResolvedX402FacilitatorGroups(facilitatorsByNetwork);
|
|
283
|
+
return groups.map((group) => {
|
|
284
|
+
const inner = new HTTPFacilitatorClient(group.config);
|
|
285
|
+
return group.family === "evm" ? cachedClient(inner, group.networks) : inner;
|
|
286
|
+
});
|
|
287
|
+
}
|
|
67
288
|
var init_server = __esm({
|
|
68
289
|
"src/server.ts"() {
|
|
69
290
|
"use strict";
|
|
291
|
+
init_evm();
|
|
292
|
+
init_solana();
|
|
293
|
+
init_x402_facilitators();
|
|
294
|
+
init_x402_config();
|
|
70
295
|
}
|
|
71
296
|
});
|
|
72
297
|
|
|
@@ -90,33 +315,46 @@ module.exports = __toCommonJS(index_exports);
|
|
|
90
315
|
// src/registry.ts
|
|
91
316
|
var RouteRegistry = class {
|
|
92
317
|
routes = /* @__PURE__ */ new Map();
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
318
|
+
// Internal map key includes the HTTP method so that POST and DELETE on the
|
|
319
|
+
// same path coexist. Within the same path+method, last-write-wins is still
|
|
320
|
+
// intentional — Next.js module loading order is non-deterministic during
|
|
321
|
+
// build and discovery stubs may register the same route in either order.
|
|
96
322
|
// Prior art: ElysiaJS uses the same pattern (silent overwrite in router.history).
|
|
323
|
+
mapKey(entry) {
|
|
324
|
+
return `${entry.key}:${entry.method}`;
|
|
325
|
+
}
|
|
97
326
|
register(entry) {
|
|
98
|
-
|
|
327
|
+
const k = this.mapKey(entry);
|
|
328
|
+
if (this.routes.has(k) && typeof process !== "undefined" && process.env?.["NODE_ENV"] !== "production") {
|
|
99
329
|
console.warn(
|
|
100
|
-
`[agentcash/router] route '${entry.key}' registered twice \u2014 overwriting (this is expected for discovery stubs during next build)`
|
|
330
|
+
`[agentcash/router] route '${entry.key}' (${entry.method}) registered twice \u2014 overwriting (this is expected for discovery stubs during next build)`
|
|
101
331
|
);
|
|
102
332
|
}
|
|
103
|
-
this.routes.set(
|
|
333
|
+
this.routes.set(k, entry);
|
|
104
334
|
}
|
|
335
|
+
// Accepts either a compound key ("site/domain:DELETE") or a path-only key
|
|
336
|
+
// ("site/domain") — path-only returns the first registered method for that path.
|
|
105
337
|
get(key) {
|
|
106
|
-
|
|
338
|
+
const direct = this.routes.get(key);
|
|
339
|
+
if (direct) return direct;
|
|
340
|
+
for (const entry of this.routes.values()) {
|
|
341
|
+
if (entry.key === key) return entry;
|
|
342
|
+
}
|
|
343
|
+
return void 0;
|
|
107
344
|
}
|
|
108
345
|
entries() {
|
|
109
346
|
return this.routes.entries();
|
|
110
347
|
}
|
|
111
348
|
has(key) {
|
|
112
|
-
return this.
|
|
349
|
+
return this.get(key) !== void 0;
|
|
113
350
|
}
|
|
114
351
|
get size() {
|
|
115
352
|
return this.routes.size;
|
|
116
353
|
}
|
|
117
354
|
validate(expectedKeys) {
|
|
118
355
|
if (!expectedKeys) return;
|
|
119
|
-
const
|
|
356
|
+
const registeredPathKeys = new Set([...this.routes.values()].map((e) => e.key));
|
|
357
|
+
const missing = expectedKeys.filter((k) => !registeredPathKeys.has(k));
|
|
120
358
|
if (missing.length > 0) {
|
|
121
359
|
throw new Error(
|
|
122
360
|
`route${missing.length > 1 ? "s" : ""} ${missing.map((k) => `'${k}'`).join(", ")} in prices map but not registered \u2014 add to barrel imports`
|
|
@@ -374,51 +612,41 @@ var import_mppx = require("mppx");
|
|
|
374
612
|
var import_viem = require("viem");
|
|
375
613
|
|
|
376
614
|
// src/protocols/x402.ts
|
|
377
|
-
|
|
615
|
+
init_x402_facilitators();
|
|
616
|
+
init_evm();
|
|
617
|
+
init_solana();
|
|
618
|
+
async function buildX402Challenge(opts) {
|
|
619
|
+
const { server, routeEntry, request, price, accepts, facilitatorsByNetwork, extensions } = opts;
|
|
378
620
|
const { encodePaymentRequiredHeader } = await import("@x402/core/http");
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
621
|
+
const resource = buildChallengeResource(request, routeEntry);
|
|
622
|
+
const requirements = await buildChallengeRequirements(
|
|
623
|
+
server,
|
|
624
|
+
request,
|
|
382
625
|
price,
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
method: routeEntry.method,
|
|
388
|
-
description: routeEntry.description,
|
|
389
|
-
mimeType: "application/json"
|
|
390
|
-
};
|
|
391
|
-
const requirements = await server.buildPaymentRequirementsFromOptions([options], {
|
|
392
|
-
request
|
|
393
|
-
});
|
|
626
|
+
accepts,
|
|
627
|
+
resource,
|
|
628
|
+
facilitatorsByNetwork
|
|
629
|
+
);
|
|
394
630
|
const paymentRequired = await server.createPaymentRequiredResponse(
|
|
395
631
|
requirements,
|
|
396
632
|
resource,
|
|
397
|
-
|
|
633
|
+
void 0,
|
|
398
634
|
extensions
|
|
399
635
|
);
|
|
400
636
|
const encoded = encodePaymentRequiredHeader(paymentRequired);
|
|
401
637
|
return { encoded, requirements };
|
|
402
638
|
}
|
|
403
|
-
async function verifyX402Payment(
|
|
404
|
-
const {
|
|
405
|
-
const
|
|
406
|
-
if (!
|
|
407
|
-
const
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
price,
|
|
412
|
-
payTo: payeeAddress
|
|
413
|
-
};
|
|
414
|
-
const requirements = await server.buildPaymentRequirementsFromOptions([options], {
|
|
415
|
-
request
|
|
416
|
-
});
|
|
417
|
-
const matching = server.findMatchingRequirements(requirements, payload);
|
|
418
|
-
const verify = await server.verifyPayment(payload, matching);
|
|
419
|
-
if (!verify.isValid) {
|
|
420
|
-
return { valid: false, payload: null, requirements: null, payer: null };
|
|
639
|
+
async function verifyX402Payment(opts) {
|
|
640
|
+
const { server, request, price, accepts } = opts;
|
|
641
|
+
const payload = await readPaymentPayload(request);
|
|
642
|
+
if (!payload) return null;
|
|
643
|
+
const requirements = await buildExpectedRequirements(server, request, price, accepts);
|
|
644
|
+
const matching = findVerifiableRequirements(server, requirements, payload);
|
|
645
|
+
if (!matching) {
|
|
646
|
+
return invalidPaymentVerification();
|
|
421
647
|
}
|
|
648
|
+
const verify = await server.verifyPayment(payload, matching);
|
|
649
|
+
if (!verify.isValid) return invalidPaymentVerification();
|
|
422
650
|
return {
|
|
423
651
|
valid: true,
|
|
424
652
|
payer: verify.payer,
|
|
@@ -426,14 +654,150 @@ async function verifyX402Payment(server, request, routeEntry, price, payeeAddres
|
|
|
426
654
|
requirements: matching
|
|
427
655
|
};
|
|
428
656
|
}
|
|
657
|
+
function findVerifiableRequirements(server, requirements, payload) {
|
|
658
|
+
const strictMatch = server.findMatchingRequirements(requirements, payload);
|
|
659
|
+
if (strictMatch) {
|
|
660
|
+
return payload.x402Version === 2 ? payload.accepted : strictMatch;
|
|
661
|
+
}
|
|
662
|
+
if (payload.x402Version !== 2) {
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
const stableMatch = requirements.find(
|
|
666
|
+
(requirement) => matchesStableFields(requirement, payload.accepted)
|
|
667
|
+
);
|
|
668
|
+
return stableMatch ? payload.accepted : null;
|
|
669
|
+
}
|
|
670
|
+
function matchesStableFields(requirement, accepted) {
|
|
671
|
+
return requirement.scheme === accepted.scheme && requirement.network === accepted.network && requirement.payTo === accepted.payTo && requirement.asset === accepted.asset && requirement.amount === accepted.amount && requirement.maxTimeoutSeconds === accepted.maxTimeoutSeconds;
|
|
672
|
+
}
|
|
673
|
+
async function buildExpectedRequirements(server, request, price, accepts) {
|
|
674
|
+
const exactRequirements = await buildExactRequirements(server, request, price, accepts);
|
|
675
|
+
const customRequirements = buildCustomRequirements(price, accepts);
|
|
676
|
+
return [...exactRequirements, ...customRequirements];
|
|
677
|
+
}
|
|
678
|
+
async function buildExactRequirements(server, request, price, accepts) {
|
|
679
|
+
const exactOptions = [
|
|
680
|
+
...buildEvmExactOptions(accepts, price),
|
|
681
|
+
...buildSolanaExactOptions(accepts, price)
|
|
682
|
+
];
|
|
683
|
+
if (exactOptions.length === 0) return [];
|
|
684
|
+
return server.buildPaymentRequirementsFromOptions(exactOptions, { request });
|
|
685
|
+
}
|
|
686
|
+
function buildCustomRequirements(price, accepts) {
|
|
687
|
+
return accepts.filter((accept) => accept.scheme !== "exact").map((accept) => buildCustomRequirement(price, accept));
|
|
688
|
+
}
|
|
689
|
+
async function buildChallengeRequirements(server, request, price, accepts, resource, facilitatorsByNetwork) {
|
|
690
|
+
const requirements = await buildExpectedRequirements(server, request, price, accepts);
|
|
691
|
+
if (!needsFacilitatorEnrichment(accepts)) return requirements;
|
|
692
|
+
return enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork);
|
|
693
|
+
}
|
|
694
|
+
function needsFacilitatorEnrichment(accepts) {
|
|
695
|
+
return accepts.some((accept) => accept.scheme !== "exact") || hasSolanaAccepts(accepts);
|
|
696
|
+
}
|
|
697
|
+
async function enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork) {
|
|
698
|
+
const groups = collectEnrichmentGroups(requirements, facilitatorsByNetwork);
|
|
699
|
+
if (groups.length === 0) return requirements;
|
|
700
|
+
const enrichedRequirements = [...requirements];
|
|
701
|
+
await Promise.all(
|
|
702
|
+
groups.map(async (group) => {
|
|
703
|
+
const enriched = await enrichRequirementsWithFacilitatorAccepts(
|
|
704
|
+
group.facilitator,
|
|
705
|
+
resource,
|
|
706
|
+
group.items.map(({ requirement }) => requirement)
|
|
707
|
+
);
|
|
708
|
+
if (enriched.length !== group.items.length) {
|
|
709
|
+
throw new Error(
|
|
710
|
+
`Facilitator /accepts returned ${enriched.length} requirements for ${group.items.length} inputs on ${group.facilitator.url ?? group.facilitator.network}`
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
enriched.forEach((requirement, offset) => {
|
|
714
|
+
const index = group.items[offset]?.index;
|
|
715
|
+
if (index === void 0) return;
|
|
716
|
+
enrichedRequirements[index] = requirement;
|
|
717
|
+
});
|
|
718
|
+
})
|
|
719
|
+
);
|
|
720
|
+
return enrichedRequirements;
|
|
721
|
+
}
|
|
722
|
+
function collectEnrichmentGroups(requirements, facilitatorsByNetwork) {
|
|
723
|
+
const groups = [];
|
|
724
|
+
requirements.forEach((requirement, index) => {
|
|
725
|
+
if (!requiresFacilitatorEnrichment(requirement)) return;
|
|
726
|
+
const facilitator = getRequiredFacilitator(requirement, facilitatorsByNetwork);
|
|
727
|
+
const existing = groups.find(
|
|
728
|
+
(group) => sameResolvedX402Facilitator(group.facilitator, facilitator)
|
|
729
|
+
);
|
|
730
|
+
if (existing) {
|
|
731
|
+
existing.items.push({ index, requirement });
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
groups.push({
|
|
735
|
+
facilitator,
|
|
736
|
+
items: [{ index, requirement }]
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
return groups;
|
|
740
|
+
}
|
|
741
|
+
function getRequiredFacilitator(requirement, facilitatorsByNetwork) {
|
|
742
|
+
const facilitator = getFacilitatorForRequirement(facilitatorsByNetwork, requirement);
|
|
743
|
+
if (!facilitator) {
|
|
744
|
+
throw new Error(
|
|
745
|
+
`Missing x402 facilitator for ${requirement.scheme} requirement on ${requirement.network}`
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
return facilitator;
|
|
749
|
+
}
|
|
750
|
+
function requiresFacilitatorEnrichment(requirement) {
|
|
751
|
+
return requirement.scheme !== "exact" || isSolanaRequirement(requirement);
|
|
752
|
+
}
|
|
753
|
+
function buildCustomRequirement(price, accept) {
|
|
754
|
+
if (!accept.asset) {
|
|
755
|
+
throw new Error(
|
|
756
|
+
`Custom x402 accept '${accept.scheme}' on '${accept.network}' is missing asset`
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
return {
|
|
760
|
+
scheme: accept.scheme,
|
|
761
|
+
network: accept.network,
|
|
762
|
+
amount: decimalToAtomicUnits(price, accept.decimals ?? 6),
|
|
763
|
+
asset: accept.asset,
|
|
764
|
+
payTo: accept.payTo,
|
|
765
|
+
maxTimeoutSeconds: accept.maxTimeoutSeconds ?? 300,
|
|
766
|
+
extra: accept.extra ?? {}
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
function buildChallengeResource(request, routeEntry) {
|
|
770
|
+
return {
|
|
771
|
+
url: request.url,
|
|
772
|
+
method: routeEntry.method,
|
|
773
|
+
description: routeEntry.description,
|
|
774
|
+
mimeType: "application/json"
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
async function readPaymentPayload(request) {
|
|
778
|
+
const paymentHeader = request.headers.get("PAYMENT-SIGNATURE") ?? request.headers.get("X-PAYMENT");
|
|
779
|
+
if (!paymentHeader) return null;
|
|
780
|
+
const { decodePaymentSignatureHeader } = await import("@x402/core/http");
|
|
781
|
+
return decodePaymentSignatureHeader(paymentHeader);
|
|
782
|
+
}
|
|
783
|
+
function invalidPaymentVerification() {
|
|
784
|
+
return { valid: false, payload: null, requirements: null, payer: null };
|
|
785
|
+
}
|
|
786
|
+
function decimalToAtomicUnits(amount, decimals) {
|
|
787
|
+
const match = /^(?<whole>\d+)(?:\.(?<fraction>\d+))?$/.exec(amount);
|
|
788
|
+
if (!match?.groups) {
|
|
789
|
+
throw new Error(`Invalid decimal amount '${amount}'`);
|
|
790
|
+
}
|
|
791
|
+
const whole = match.groups.whole;
|
|
792
|
+
const fraction = match.groups.fraction ?? "";
|
|
793
|
+
if (fraction.length > decimals) {
|
|
794
|
+
throw new Error(`Amount '${amount}' exceeds ${decimals} decimal places`);
|
|
795
|
+
}
|
|
796
|
+
const normalized = `${whole}${fraction.padEnd(decimals, "0")}`.replace(/^0+(?=\d)/, "");
|
|
797
|
+
return normalized === "" ? "0" : normalized;
|
|
798
|
+
}
|
|
429
799
|
async function settleX402Payment(server, payload, requirements) {
|
|
430
800
|
const { encodePaymentResponseHeader } = await import("@x402/core/http");
|
|
431
|
-
const payloadKeys = typeof payload === "object" && payload !== null ? Object.keys(payload).sort().join(",") : "n/a";
|
|
432
|
-
const reqKeys = typeof requirements === "object" && requirements !== null ? Object.keys(requirements).sort().join(",") : "n/a";
|
|
433
|
-
console.info("x402 settle input", {
|
|
434
|
-
payloadKeys,
|
|
435
|
-
requirementsKeys: reqKeys
|
|
436
|
-
});
|
|
437
801
|
const result = await server.settlePayment(payload, requirements);
|
|
438
802
|
const encoded = encodePaymentResponseHeader(result);
|
|
439
803
|
return { encoded, result };
|
|
@@ -508,10 +872,10 @@ function extractBearerToken(header) {
|
|
|
508
872
|
}
|
|
509
873
|
|
|
510
874
|
// src/orchestrate.ts
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
return
|
|
875
|
+
init_x402_config();
|
|
876
|
+
function getRequirementNetwork(requirements, fallback) {
|
|
877
|
+
const network = requirements?.network;
|
|
878
|
+
return typeof network === "string" ? network : fallback;
|
|
515
879
|
}
|
|
516
880
|
function createRequestHandler(routeEntry, handler, deps) {
|
|
517
881
|
async function invoke(request, meta, pluginCtx, wallet, account, parsedBody) {
|
|
@@ -792,24 +1156,28 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
792
1156
|
console.error(`[router] ${routeEntry.key}: ${reason}`);
|
|
793
1157
|
return fail(500, reason, meta, pluginCtx, body.data);
|
|
794
1158
|
}
|
|
795
|
-
const
|
|
796
|
-
const verify = await verifyX402Payment(
|
|
797
|
-
deps.x402Server,
|
|
1159
|
+
const accepts = await resolveX402Accepts(
|
|
798
1160
|
request,
|
|
799
1161
|
routeEntry,
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
deps.network
|
|
1162
|
+
deps.x402Accepts,
|
|
1163
|
+
deps.payeeAddress
|
|
803
1164
|
);
|
|
1165
|
+
const verify = await verifyX402Payment({
|
|
1166
|
+
server: deps.x402Server,
|
|
1167
|
+
request,
|
|
1168
|
+
price,
|
|
1169
|
+
accepts
|
|
1170
|
+
});
|
|
804
1171
|
if (!verify?.valid) return await build402(request, routeEntry, deps, meta, pluginCtx);
|
|
805
1172
|
const { payload: verifyPayload, requirements: verifyRequirements } = verify;
|
|
1173
|
+
const matchedNetwork = getRequirementNetwork(verifyRequirements, deps.network);
|
|
806
1174
|
const wallet = verify.payer.toLowerCase();
|
|
807
1175
|
pluginCtx.setVerifiedWallet(wallet);
|
|
808
1176
|
firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
|
|
809
1177
|
protocol: "x402",
|
|
810
1178
|
payer: wallet,
|
|
811
1179
|
amount: price,
|
|
812
|
-
network:
|
|
1180
|
+
network: matchedNetwork
|
|
813
1181
|
});
|
|
814
1182
|
const { response, rawResult } = await invoke(
|
|
815
1183
|
request,
|
|
@@ -821,15 +1189,6 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
821
1189
|
);
|
|
822
1190
|
if (response.status < 400) {
|
|
823
1191
|
try {
|
|
824
|
-
const payloadFingerprint = typeof verifyPayload === "object" && verifyPayload !== null ? {
|
|
825
|
-
keys: Object.keys(verifyPayload).sort().join(","),
|
|
826
|
-
payloadType: typeof verifyPayload
|
|
827
|
-
} : { payloadType: typeof verifyPayload };
|
|
828
|
-
console.info("Settlement attempt", {
|
|
829
|
-
route: routeEntry.key,
|
|
830
|
-
network: deps.network,
|
|
831
|
-
...payloadFingerprint
|
|
832
|
-
});
|
|
833
1192
|
const settle = await settleX402Payment(
|
|
834
1193
|
deps.x402Server,
|
|
835
1194
|
verifyPayload,
|
|
@@ -851,14 +1210,14 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
851
1210
|
protocol: "x402",
|
|
852
1211
|
payer: verify.payer,
|
|
853
1212
|
transaction: String(settle.result?.transaction ?? ""),
|
|
854
|
-
network:
|
|
1213
|
+
network: matchedNetwork
|
|
855
1214
|
});
|
|
856
1215
|
} catch (err) {
|
|
857
1216
|
const errObj = err;
|
|
858
1217
|
console.error("Settlement failed", {
|
|
859
1218
|
message: err instanceof Error ? err.message : String(err),
|
|
860
1219
|
route: routeEntry.key,
|
|
861
|
-
network:
|
|
1220
|
+
network: matchedNetwork,
|
|
862
1221
|
facilitatorStatus: errObj.response?.status,
|
|
863
1222
|
facilitatorBody: errObj.response?.data ?? errObj.response?.body
|
|
864
1223
|
});
|
|
@@ -1073,16 +1432,21 @@ async function build402(request, routeEntry, deps, meta, pluginCtx, bodyData) {
|
|
|
1073
1432
|
}
|
|
1074
1433
|
if (routeEntry.protocols.includes("x402") && deps.x402Server) {
|
|
1075
1434
|
try {
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1078
|
-
|
|
1435
|
+
const accepts = await resolveX402Accepts(
|
|
1436
|
+
request,
|
|
1437
|
+
routeEntry,
|
|
1438
|
+
deps.x402Accepts,
|
|
1439
|
+
deps.payeeAddress
|
|
1440
|
+
);
|
|
1441
|
+
const { encoded } = await buildX402Challenge({
|
|
1442
|
+
server: deps.x402Server,
|
|
1079
1443
|
routeEntry,
|
|
1080
1444
|
request,
|
|
1081
|
-
challengePrice,
|
|
1082
|
-
|
|
1083
|
-
deps.
|
|
1445
|
+
price: challengePrice,
|
|
1446
|
+
accepts,
|
|
1447
|
+
facilitatorsByNetwork: deps.x402FacilitatorsByNetwork,
|
|
1084
1448
|
extensions
|
|
1085
|
-
);
|
|
1449
|
+
});
|
|
1086
1450
|
response.headers.set("PAYMENT-REQUIRED", encoded);
|
|
1087
1451
|
} catch (err) {
|
|
1088
1452
|
const message = `x402 challenge build failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
@@ -1475,8 +1839,8 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
|
1475
1839
|
const x402Set = /* @__PURE__ */ new Set();
|
|
1476
1840
|
const mppSet = /* @__PURE__ */ new Set();
|
|
1477
1841
|
const methodHints = discovery.methodHints ?? "non-default";
|
|
1478
|
-
for (const [
|
|
1479
|
-
const url = `${normalizedBase}/api/${entry.path ?? key}`;
|
|
1842
|
+
for (const [, entry] of registry.entries()) {
|
|
1843
|
+
const url = `${normalizedBase}/api/${entry.path ?? entry.key}`;
|
|
1480
1844
|
const resource = toDiscoveryResource(entry.method, url, methodHints);
|
|
1481
1845
|
if (entry.authMode !== "unprotected") x402Set.add(resource);
|
|
1482
1846
|
if (entry.protocols.includes("mpp")) mppSet.add(resource);
|
|
@@ -1532,12 +1896,12 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
|
1532
1896
|
const tagSet = /* @__PURE__ */ new Set();
|
|
1533
1897
|
let requiresSiwxScheme = false;
|
|
1534
1898
|
let requiresApiKeyScheme = false;
|
|
1535
|
-
for (const [
|
|
1536
|
-
const apiPath = `/api/${entry.path ?? key}`;
|
|
1899
|
+
for (const [, entry] of registry.entries()) {
|
|
1900
|
+
const apiPath = `/api/${entry.path ?? entry.key}`;
|
|
1537
1901
|
const method = entry.method.toLowerCase();
|
|
1538
|
-
const tag = deriveTag(key);
|
|
1902
|
+
const tag = deriveTag(entry.key);
|
|
1539
1903
|
tagSet.add(tag);
|
|
1540
|
-
const built = buildOperation(key, entry, tag);
|
|
1904
|
+
const built = buildOperation(entry.key, entry, tag);
|
|
1541
1905
|
if (built.requiresSiwxScheme) requiresSiwxScheme = true;
|
|
1542
1906
|
if (built.requiresApiKeyScheme) requiresApiKeyScheme = true;
|
|
1543
1907
|
paths[apiPath] = { ...paths[apiPath], [method]: built.operation };
|
|
@@ -1706,11 +2070,15 @@ function createLlmsTxtHandler(discovery) {
|
|
|
1706
2070
|
}
|
|
1707
2071
|
|
|
1708
2072
|
// src/index.ts
|
|
2073
|
+
init_x402_config();
|
|
2074
|
+
init_evm();
|
|
2075
|
+
init_solana();
|
|
1709
2076
|
function createRouter(config) {
|
|
1710
2077
|
const registry = new RouteRegistry();
|
|
1711
2078
|
const nonceStore = config.siwx?.nonceStore ?? new MemoryNonceStore();
|
|
1712
2079
|
const entitlementStore = config.siwx?.entitlementStore ?? new MemoryEntitlementStore();
|
|
1713
2080
|
const network = config.network ?? "eip155:8453";
|
|
2081
|
+
const x402Accepts = getConfiguredX402Accepts(config);
|
|
1714
2082
|
if (!config.baseUrl) {
|
|
1715
2083
|
throw new Error(
|
|
1716
2084
|
'[router] baseUrl is required in RouterConfig. Set it to your production domain (e.g., "https://api.example.com"). The realm is used for payment matching and must be correct.'
|
|
@@ -1724,8 +2092,23 @@ function createRouter(config) {
|
|
|
1724
2092
|
const resolvedBaseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
1725
2093
|
let x402ConfigError;
|
|
1726
2094
|
let mppConfigError;
|
|
1727
|
-
if (
|
|
1728
|
-
|
|
2095
|
+
if (!config.protocols || config.protocols.includes("x402")) {
|
|
2096
|
+
if (x402Accepts.length === 0) {
|
|
2097
|
+
x402ConfigError = "x402 requires at least one accept configuration.";
|
|
2098
|
+
} else if (x402Accepts.some((accept) => !accept.network)) {
|
|
2099
|
+
x402ConfigError = "x402 accepts require a network.";
|
|
2100
|
+
} else if (x402Accepts.some((accept) => !isSupportedX402Network(accept.network))) {
|
|
2101
|
+
const unsupported = x402Accepts.find((accept) => !isSupportedX402Network(accept.network));
|
|
2102
|
+
x402ConfigError = `unsupported x402 network '${unsupported?.network}'. Use eip155:* or solana:*.`;
|
|
2103
|
+
} else if (x402Accepts.some((accept) => (accept.scheme ?? "exact") !== "exact" && !accept.asset)) {
|
|
2104
|
+
x402ConfigError = "non-exact x402 accepts require an asset.";
|
|
2105
|
+
} else if (x402Accepts.some(
|
|
2106
|
+
(accept) => accept.decimals !== void 0 && (!Number.isInteger(accept.decimals) || accept.decimals < 0)
|
|
2107
|
+
)) {
|
|
2108
|
+
x402ConfigError = "x402 accept decimals must be a non-negative integer.";
|
|
2109
|
+
} else if (x402Accepts.some((accept) => !accept.payTo) && !config.payeeAddress) {
|
|
2110
|
+
x402ConfigError = "x402 requires payeeAddress in router config or payTo on every x402 accept.";
|
|
2111
|
+
}
|
|
1729
2112
|
}
|
|
1730
2113
|
if (config.protocols?.includes("mpp")) {
|
|
1731
2114
|
if (!config.mpp) {
|
|
@@ -1759,8 +2142,10 @@ function createRouter(config) {
|
|
|
1759
2142
|
plugin: config.plugin,
|
|
1760
2143
|
nonceStore,
|
|
1761
2144
|
entitlementStore,
|
|
1762
|
-
payeeAddress: config.payeeAddress,
|
|
2145
|
+
payeeAddress: config.payeeAddress ?? "",
|
|
1763
2146
|
network,
|
|
2147
|
+
x402FacilitatorsByNetwork: void 0,
|
|
2148
|
+
x402Accepts,
|
|
1764
2149
|
mppx: null
|
|
1765
2150
|
};
|
|
1766
2151
|
deps.initPromise = (async () => {
|
|
@@ -1771,6 +2156,7 @@ function createRouter(config) {
|
|
|
1771
2156
|
const { createX402Server: createX402Server2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
1772
2157
|
const result = await createX402Server2(config);
|
|
1773
2158
|
deps.x402Server = result.server;
|
|
2159
|
+
deps.x402FacilitatorsByNetwork = result.facilitatorsByNetwork;
|
|
1774
2160
|
await result.initPromise;
|
|
1775
2161
|
} catch (err) {
|
|
1776
2162
|
deps.x402Server = null;
|
|
@@ -1861,6 +2247,9 @@ function createRouter(config) {
|
|
|
1861
2247
|
registry
|
|
1862
2248
|
};
|
|
1863
2249
|
}
|
|
2250
|
+
function isSupportedX402Network(network) {
|
|
2251
|
+
return isEvmNetwork(network) || isSolanaNetwork(network);
|
|
2252
|
+
}
|
|
1864
2253
|
function normalizePath(path) {
|
|
1865
2254
|
let normalized = path.trim();
|
|
1866
2255
|
normalized = normalized.replace(/^\/+/, "");
|