@agentcash/router 1.0.1 → 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 +458 -82
- package/dist/index.d.cts +45 -7
- package/dist/index.d.ts +45 -7
- package/dist/index.js +458 -82
- 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
|
|
|
@@ -387,51 +612,41 @@ var import_mppx = require("mppx");
|
|
|
387
612
|
var import_viem = require("viem");
|
|
388
613
|
|
|
389
614
|
// src/protocols/x402.ts
|
|
390
|
-
|
|
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;
|
|
391
620
|
const { encodePaymentRequiredHeader } = await import("@x402/core/http");
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
621
|
+
const resource = buildChallengeResource(request, routeEntry);
|
|
622
|
+
const requirements = await buildChallengeRequirements(
|
|
623
|
+
server,
|
|
624
|
+
request,
|
|
395
625
|
price,
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
method: routeEntry.method,
|
|
401
|
-
description: routeEntry.description,
|
|
402
|
-
mimeType: "application/json"
|
|
403
|
-
};
|
|
404
|
-
const requirements = await server.buildPaymentRequirementsFromOptions([options], {
|
|
405
|
-
request
|
|
406
|
-
});
|
|
626
|
+
accepts,
|
|
627
|
+
resource,
|
|
628
|
+
facilitatorsByNetwork
|
|
629
|
+
);
|
|
407
630
|
const paymentRequired = await server.createPaymentRequiredResponse(
|
|
408
631
|
requirements,
|
|
409
632
|
resource,
|
|
410
|
-
|
|
633
|
+
void 0,
|
|
411
634
|
extensions
|
|
412
635
|
);
|
|
413
636
|
const encoded = encodePaymentRequiredHeader(paymentRequired);
|
|
414
637
|
return { encoded, requirements };
|
|
415
638
|
}
|
|
416
|
-
async function verifyX402Payment(
|
|
417
|
-
const {
|
|
418
|
-
const
|
|
419
|
-
if (!
|
|
420
|
-
const
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
price,
|
|
425
|
-
payTo: payeeAddress
|
|
426
|
-
};
|
|
427
|
-
const requirements = await server.buildPaymentRequirementsFromOptions([options], {
|
|
428
|
-
request
|
|
429
|
-
});
|
|
430
|
-
const matching = server.findMatchingRequirements(requirements, payload);
|
|
431
|
-
const verify = await server.verifyPayment(payload, matching);
|
|
432
|
-
if (!verify.isValid) {
|
|
433
|
-
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();
|
|
434
647
|
}
|
|
648
|
+
const verify = await server.verifyPayment(payload, matching);
|
|
649
|
+
if (!verify.isValid) return invalidPaymentVerification();
|
|
435
650
|
return {
|
|
436
651
|
valid: true,
|
|
437
652
|
payer: verify.payer,
|
|
@@ -439,14 +654,150 @@ async function verifyX402Payment(server, request, routeEntry, price, payeeAddres
|
|
|
439
654
|
requirements: matching
|
|
440
655
|
};
|
|
441
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
|
+
}
|
|
442
799
|
async function settleX402Payment(server, payload, requirements) {
|
|
443
800
|
const { encodePaymentResponseHeader } = await import("@x402/core/http");
|
|
444
|
-
const payloadKeys = typeof payload === "object" && payload !== null ? Object.keys(payload).sort().join(",") : "n/a";
|
|
445
|
-
const reqKeys = typeof requirements === "object" && requirements !== null ? Object.keys(requirements).sort().join(",") : "n/a";
|
|
446
|
-
console.info("x402 settle input", {
|
|
447
|
-
payloadKeys,
|
|
448
|
-
requirementsKeys: reqKeys
|
|
449
|
-
});
|
|
450
801
|
const result = await server.settlePayment(payload, requirements);
|
|
451
802
|
const encoded = encodePaymentResponseHeader(result);
|
|
452
803
|
return { encoded, result };
|
|
@@ -521,10 +872,10 @@ function extractBearerToken(header) {
|
|
|
521
872
|
}
|
|
522
873
|
|
|
523
874
|
// src/orchestrate.ts
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
return
|
|
875
|
+
init_x402_config();
|
|
876
|
+
function getRequirementNetwork(requirements, fallback) {
|
|
877
|
+
const network = requirements?.network;
|
|
878
|
+
return typeof network === "string" ? network : fallback;
|
|
528
879
|
}
|
|
529
880
|
function createRequestHandler(routeEntry, handler, deps) {
|
|
530
881
|
async function invoke(request, meta, pluginCtx, wallet, account, parsedBody) {
|
|
@@ -805,24 +1156,28 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
805
1156
|
console.error(`[router] ${routeEntry.key}: ${reason}`);
|
|
806
1157
|
return fail(500, reason, meta, pluginCtx, body.data);
|
|
807
1158
|
}
|
|
808
|
-
const
|
|
809
|
-
const verify = await verifyX402Payment(
|
|
810
|
-
deps.x402Server,
|
|
1159
|
+
const accepts = await resolveX402Accepts(
|
|
811
1160
|
request,
|
|
812
1161
|
routeEntry,
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
deps.network
|
|
1162
|
+
deps.x402Accepts,
|
|
1163
|
+
deps.payeeAddress
|
|
816
1164
|
);
|
|
1165
|
+
const verify = await verifyX402Payment({
|
|
1166
|
+
server: deps.x402Server,
|
|
1167
|
+
request,
|
|
1168
|
+
price,
|
|
1169
|
+
accepts
|
|
1170
|
+
});
|
|
817
1171
|
if (!verify?.valid) return await build402(request, routeEntry, deps, meta, pluginCtx);
|
|
818
1172
|
const { payload: verifyPayload, requirements: verifyRequirements } = verify;
|
|
1173
|
+
const matchedNetwork = getRequirementNetwork(verifyRequirements, deps.network);
|
|
819
1174
|
const wallet = verify.payer.toLowerCase();
|
|
820
1175
|
pluginCtx.setVerifiedWallet(wallet);
|
|
821
1176
|
firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
|
|
822
1177
|
protocol: "x402",
|
|
823
1178
|
payer: wallet,
|
|
824
1179
|
amount: price,
|
|
825
|
-
network:
|
|
1180
|
+
network: matchedNetwork
|
|
826
1181
|
});
|
|
827
1182
|
const { response, rawResult } = await invoke(
|
|
828
1183
|
request,
|
|
@@ -834,15 +1189,6 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
834
1189
|
);
|
|
835
1190
|
if (response.status < 400) {
|
|
836
1191
|
try {
|
|
837
|
-
const payloadFingerprint = typeof verifyPayload === "object" && verifyPayload !== null ? {
|
|
838
|
-
keys: Object.keys(verifyPayload).sort().join(","),
|
|
839
|
-
payloadType: typeof verifyPayload
|
|
840
|
-
} : { payloadType: typeof verifyPayload };
|
|
841
|
-
console.info("Settlement attempt", {
|
|
842
|
-
route: routeEntry.key,
|
|
843
|
-
network: deps.network,
|
|
844
|
-
...payloadFingerprint
|
|
845
|
-
});
|
|
846
1192
|
const settle = await settleX402Payment(
|
|
847
1193
|
deps.x402Server,
|
|
848
1194
|
verifyPayload,
|
|
@@ -864,14 +1210,14 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
864
1210
|
protocol: "x402",
|
|
865
1211
|
payer: verify.payer,
|
|
866
1212
|
transaction: String(settle.result?.transaction ?? ""),
|
|
867
|
-
network:
|
|
1213
|
+
network: matchedNetwork
|
|
868
1214
|
});
|
|
869
1215
|
} catch (err) {
|
|
870
1216
|
const errObj = err;
|
|
871
1217
|
console.error("Settlement failed", {
|
|
872
1218
|
message: err instanceof Error ? err.message : String(err),
|
|
873
1219
|
route: routeEntry.key,
|
|
874
|
-
network:
|
|
1220
|
+
network: matchedNetwork,
|
|
875
1221
|
facilitatorStatus: errObj.response?.status,
|
|
876
1222
|
facilitatorBody: errObj.response?.data ?? errObj.response?.body
|
|
877
1223
|
});
|
|
@@ -1086,16 +1432,21 @@ async function build402(request, routeEntry, deps, meta, pluginCtx, bodyData) {
|
|
|
1086
1432
|
}
|
|
1087
1433
|
if (routeEntry.protocols.includes("x402") && deps.x402Server) {
|
|
1088
1434
|
try {
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
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,
|
|
1092
1443
|
routeEntry,
|
|
1093
1444
|
request,
|
|
1094
|
-
challengePrice,
|
|
1095
|
-
|
|
1096
|
-
deps.
|
|
1445
|
+
price: challengePrice,
|
|
1446
|
+
accepts,
|
|
1447
|
+
facilitatorsByNetwork: deps.x402FacilitatorsByNetwork,
|
|
1097
1448
|
extensions
|
|
1098
|
-
);
|
|
1449
|
+
});
|
|
1099
1450
|
response.headers.set("PAYMENT-REQUIRED", encoded);
|
|
1100
1451
|
} catch (err) {
|
|
1101
1452
|
const message = `x402 challenge build failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
@@ -1719,11 +2070,15 @@ function createLlmsTxtHandler(discovery) {
|
|
|
1719
2070
|
}
|
|
1720
2071
|
|
|
1721
2072
|
// src/index.ts
|
|
2073
|
+
init_x402_config();
|
|
2074
|
+
init_evm();
|
|
2075
|
+
init_solana();
|
|
1722
2076
|
function createRouter(config) {
|
|
1723
2077
|
const registry = new RouteRegistry();
|
|
1724
2078
|
const nonceStore = config.siwx?.nonceStore ?? new MemoryNonceStore();
|
|
1725
2079
|
const entitlementStore = config.siwx?.entitlementStore ?? new MemoryEntitlementStore();
|
|
1726
2080
|
const network = config.network ?? "eip155:8453";
|
|
2081
|
+
const x402Accepts = getConfiguredX402Accepts(config);
|
|
1727
2082
|
if (!config.baseUrl) {
|
|
1728
2083
|
throw new Error(
|
|
1729
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.'
|
|
@@ -1737,8 +2092,23 @@ function createRouter(config) {
|
|
|
1737
2092
|
const resolvedBaseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
1738
2093
|
let x402ConfigError;
|
|
1739
2094
|
let mppConfigError;
|
|
1740
|
-
if (
|
|
1741
|
-
|
|
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
|
+
}
|
|
1742
2112
|
}
|
|
1743
2113
|
if (config.protocols?.includes("mpp")) {
|
|
1744
2114
|
if (!config.mpp) {
|
|
@@ -1772,8 +2142,10 @@ function createRouter(config) {
|
|
|
1772
2142
|
plugin: config.plugin,
|
|
1773
2143
|
nonceStore,
|
|
1774
2144
|
entitlementStore,
|
|
1775
|
-
payeeAddress: config.payeeAddress,
|
|
2145
|
+
payeeAddress: config.payeeAddress ?? "",
|
|
1776
2146
|
network,
|
|
2147
|
+
x402FacilitatorsByNetwork: void 0,
|
|
2148
|
+
x402Accepts,
|
|
1777
2149
|
mppx: null
|
|
1778
2150
|
};
|
|
1779
2151
|
deps.initPromise = (async () => {
|
|
@@ -1784,6 +2156,7 @@ function createRouter(config) {
|
|
|
1784
2156
|
const { createX402Server: createX402Server2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
1785
2157
|
const result = await createX402Server2(config);
|
|
1786
2158
|
deps.x402Server = result.server;
|
|
2159
|
+
deps.x402FacilitatorsByNetwork = result.facilitatorsByNetwork;
|
|
1787
2160
|
await result.initPromise;
|
|
1788
2161
|
} catch (err) {
|
|
1789
2162
|
deps.x402Server = null;
|
|
@@ -1874,6 +2247,9 @@ function createRouter(config) {
|
|
|
1874
2247
|
registry
|
|
1875
2248
|
};
|
|
1876
2249
|
}
|
|
2250
|
+
function isSupportedX402Network(network) {
|
|
2251
|
+
return isEvmNetwork(network) || isSolanaNetwork(network);
|
|
2252
|
+
}
|
|
1877
2253
|
function normalizePath(path) {
|
|
1878
2254
|
let normalized = path.trim();
|
|
1879
2255
|
normalized = normalized.replace(/^\/+/, "");
|