@agentcash/router 1.0.1 → 1.1.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/dist/index.cjs +465 -82
- package/dist/index.d.cts +45 -7
- package/dist/index.d.ts +45 -7
- package/dist/index.js +465 -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,20 +1189,17 @@ 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,
|
|
849
1195
|
verifyRequirements
|
|
850
1196
|
);
|
|
1197
|
+
if (!settle.result?.success) {
|
|
1198
|
+
const reason = settle.result?.errorReason || "x402 settlement returned success=false";
|
|
1199
|
+
const error = new Error(reason);
|
|
1200
|
+
error.errorReason = reason;
|
|
1201
|
+
throw error;
|
|
1202
|
+
}
|
|
851
1203
|
if (routeEntry.siwxEnabled) {
|
|
852
1204
|
try {
|
|
853
1205
|
await deps.entitlementStore.grant(routeEntry.key, wallet);
|
|
@@ -864,14 +1216,15 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
864
1216
|
protocol: "x402",
|
|
865
1217
|
payer: verify.payer,
|
|
866
1218
|
transaction: String(settle.result?.transaction ?? ""),
|
|
867
|
-
network:
|
|
1219
|
+
network: matchedNetwork
|
|
868
1220
|
});
|
|
869
1221
|
} catch (err) {
|
|
870
1222
|
const errObj = err;
|
|
871
1223
|
console.error("Settlement failed", {
|
|
872
1224
|
message: err instanceof Error ? err.message : String(err),
|
|
873
1225
|
route: routeEntry.key,
|
|
874
|
-
network:
|
|
1226
|
+
network: matchedNetwork,
|
|
1227
|
+
errorReason: errObj.errorReason,
|
|
875
1228
|
facilitatorStatus: errObj.response?.status,
|
|
876
1229
|
facilitatorBody: errObj.response?.data ?? errObj.response?.body
|
|
877
1230
|
});
|
|
@@ -1086,16 +1439,21 @@ async function build402(request, routeEntry, deps, meta, pluginCtx, bodyData) {
|
|
|
1086
1439
|
}
|
|
1087
1440
|
if (routeEntry.protocols.includes("x402") && deps.x402Server) {
|
|
1088
1441
|
try {
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
1442
|
+
const accepts = await resolveX402Accepts(
|
|
1443
|
+
request,
|
|
1444
|
+
routeEntry,
|
|
1445
|
+
deps.x402Accepts,
|
|
1446
|
+
deps.payeeAddress
|
|
1447
|
+
);
|
|
1448
|
+
const { encoded } = await buildX402Challenge({
|
|
1449
|
+
server: deps.x402Server,
|
|
1092
1450
|
routeEntry,
|
|
1093
1451
|
request,
|
|
1094
|
-
challengePrice,
|
|
1095
|
-
|
|
1096
|
-
deps.
|
|
1452
|
+
price: challengePrice,
|
|
1453
|
+
accepts,
|
|
1454
|
+
facilitatorsByNetwork: deps.x402FacilitatorsByNetwork,
|
|
1097
1455
|
extensions
|
|
1098
|
-
);
|
|
1456
|
+
});
|
|
1099
1457
|
response.headers.set("PAYMENT-REQUIRED", encoded);
|
|
1100
1458
|
} catch (err) {
|
|
1101
1459
|
const message = `x402 challenge build failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
@@ -1719,11 +2077,15 @@ function createLlmsTxtHandler(discovery) {
|
|
|
1719
2077
|
}
|
|
1720
2078
|
|
|
1721
2079
|
// src/index.ts
|
|
2080
|
+
init_x402_config();
|
|
2081
|
+
init_evm();
|
|
2082
|
+
init_solana();
|
|
1722
2083
|
function createRouter(config) {
|
|
1723
2084
|
const registry = new RouteRegistry();
|
|
1724
2085
|
const nonceStore = config.siwx?.nonceStore ?? new MemoryNonceStore();
|
|
1725
2086
|
const entitlementStore = config.siwx?.entitlementStore ?? new MemoryEntitlementStore();
|
|
1726
2087
|
const network = config.network ?? "eip155:8453";
|
|
2088
|
+
const x402Accepts = getConfiguredX402Accepts(config);
|
|
1727
2089
|
if (!config.baseUrl) {
|
|
1728
2090
|
throw new Error(
|
|
1729
2091
|
'[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 +2099,23 @@ function createRouter(config) {
|
|
|
1737
2099
|
const resolvedBaseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
1738
2100
|
let x402ConfigError;
|
|
1739
2101
|
let mppConfigError;
|
|
1740
|
-
if (
|
|
1741
|
-
|
|
2102
|
+
if (!config.protocols || config.protocols.includes("x402")) {
|
|
2103
|
+
if (x402Accepts.length === 0) {
|
|
2104
|
+
x402ConfigError = "x402 requires at least one accept configuration.";
|
|
2105
|
+
} else if (x402Accepts.some((accept) => !accept.network)) {
|
|
2106
|
+
x402ConfigError = "x402 accepts require a network.";
|
|
2107
|
+
} else if (x402Accepts.some((accept) => !isSupportedX402Network(accept.network))) {
|
|
2108
|
+
const unsupported = x402Accepts.find((accept) => !isSupportedX402Network(accept.network));
|
|
2109
|
+
x402ConfigError = `unsupported x402 network '${unsupported?.network}'. Use eip155:* or solana:*.`;
|
|
2110
|
+
} else if (x402Accepts.some((accept) => (accept.scheme ?? "exact") !== "exact" && !accept.asset)) {
|
|
2111
|
+
x402ConfigError = "non-exact x402 accepts require an asset.";
|
|
2112
|
+
} else if (x402Accepts.some(
|
|
2113
|
+
(accept) => accept.decimals !== void 0 && (!Number.isInteger(accept.decimals) || accept.decimals < 0)
|
|
2114
|
+
)) {
|
|
2115
|
+
x402ConfigError = "x402 accept decimals must be a non-negative integer.";
|
|
2116
|
+
} else if (x402Accepts.some((accept) => !accept.payTo) && !config.payeeAddress) {
|
|
2117
|
+
x402ConfigError = "x402 requires payeeAddress in router config or payTo on every x402 accept.";
|
|
2118
|
+
}
|
|
1742
2119
|
}
|
|
1743
2120
|
if (config.protocols?.includes("mpp")) {
|
|
1744
2121
|
if (!config.mpp) {
|
|
@@ -1772,8 +2149,10 @@ function createRouter(config) {
|
|
|
1772
2149
|
plugin: config.plugin,
|
|
1773
2150
|
nonceStore,
|
|
1774
2151
|
entitlementStore,
|
|
1775
|
-
payeeAddress: config.payeeAddress,
|
|
2152
|
+
payeeAddress: config.payeeAddress ?? "",
|
|
1776
2153
|
network,
|
|
2154
|
+
x402FacilitatorsByNetwork: void 0,
|
|
2155
|
+
x402Accepts,
|
|
1777
2156
|
mppx: null
|
|
1778
2157
|
};
|
|
1779
2158
|
deps.initPromise = (async () => {
|
|
@@ -1784,6 +2163,7 @@ function createRouter(config) {
|
|
|
1784
2163
|
const { createX402Server: createX402Server2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
1785
2164
|
const result = await createX402Server2(config);
|
|
1786
2165
|
deps.x402Server = result.server;
|
|
2166
|
+
deps.x402FacilitatorsByNetwork = result.facilitatorsByNetwork;
|
|
1787
2167
|
await result.initPromise;
|
|
1788
2168
|
} catch (err) {
|
|
1789
2169
|
deps.x402Server = null;
|
|
@@ -1874,6 +2254,9 @@ function createRouter(config) {
|
|
|
1874
2254
|
registry
|
|
1875
2255
|
};
|
|
1876
2256
|
}
|
|
2257
|
+
function isSupportedX402Network(network) {
|
|
2258
|
+
return isEvmNetwork(network) || isSolanaNetwork(network);
|
|
2259
|
+
}
|
|
1877
2260
|
function normalizePath(path) {
|
|
1878
2261
|
let normalized = path.trim();
|
|
1879
2262
|
normalized = normalized.replace(/^\/+/, "");
|