@faremeter/facilitator 0.16.0 → 0.17.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/src/adapters.d.ts +22 -0
- package/dist/src/adapters.d.ts.map +1 -0
- package/dist/src/adapters.js +68 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -0
- package/dist/src/routes.d.ts +23 -1
- package/dist/src/routes.d.ts.map +1 -1
- package/dist/src/routes.js +337 -116
- package/dist/src/routes.test.d.ts +3 -0
- package/dist/src/routes.test.d.ts.map +1 -0
- package/dist/src/routes.test.js +65 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -3
package/dist/src/routes.js
CHANGED
|
@@ -1,10 +1,41 @@
|
|
|
1
1
|
import { getLogger } from "@faremeter/logs";
|
|
2
2
|
import { Hono } from "hono";
|
|
3
3
|
import * as x from "@faremeter/types/x402";
|
|
4
|
+
import * as x2 from "@faremeter/types/x402v2";
|
|
4
5
|
import { isValidationError } from "@faremeter/types";
|
|
5
|
-
import {} from "@faremeter/
|
|
6
|
+
import { caip2ToLegacyName, legacyNameToCAIP2 } from "@faremeter/info/evm";
|
|
7
|
+
import { caip2ToLegacyNetworkIds, legacyNetworkIdToCAIP2, } from "@faremeter/info/solana";
|
|
6
8
|
import { allSettledWithTimeout } from "./promise.js";
|
|
9
|
+
import { adaptRequirementsV1ToV2, adaptPayloadV1ToV2, adaptVerifyResponseV2ToV1, adaptSettleResponseV2ToV1, adaptRequirementsV2ToV1, extractResourceInfoV1, adaptSupportedKindV2ToV1, } from "./adapters.js";
|
|
7
10
|
const logger = await getLogger(["faremeter", "facilitator"]);
|
|
11
|
+
/**
|
|
12
|
+
* Translate a CAIP-2 network identifier to a v1 legacy network name.
|
|
13
|
+
* Falls through to the original identifier if no mapping exists.
|
|
14
|
+
*/
|
|
15
|
+
function translateNetwork(network) {
|
|
16
|
+
const evmLegacy = caip2ToLegacyName(network);
|
|
17
|
+
if (evmLegacy)
|
|
18
|
+
return evmLegacy;
|
|
19
|
+
const solanaLegacy = caip2ToLegacyNetworkIds(network);
|
|
20
|
+
const firstSolanaId = solanaLegacy?.[0];
|
|
21
|
+
if (firstSolanaId)
|
|
22
|
+
return firstSolanaId;
|
|
23
|
+
return network;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Normalize a v1 legacy network name to a CAIP-2 identifier.
|
|
27
|
+
* Falls through to the original identifier if no mapping exists (it may
|
|
28
|
+
* already be CAIP-2 or an unknown network).
|
|
29
|
+
*/
|
|
30
|
+
function normalizeNetwork(network) {
|
|
31
|
+
const evmCaip2 = legacyNameToCAIP2(network);
|
|
32
|
+
if (evmCaip2)
|
|
33
|
+
return evmCaip2;
|
|
34
|
+
const solanaNetwork = legacyNetworkIdToCAIP2(network);
|
|
35
|
+
if (solanaNetwork)
|
|
36
|
+
return solanaNetwork.caip2;
|
|
37
|
+
return network;
|
|
38
|
+
}
|
|
8
39
|
function summarizeRequirements({ scheme, network, asset, payTo, }) {
|
|
9
40
|
return {
|
|
10
41
|
scheme,
|
|
@@ -13,181 +44,371 @@ function summarizeRequirements({ scheme, network, asset, payTo, }) {
|
|
|
13
44
|
payTo,
|
|
14
45
|
};
|
|
15
46
|
}
|
|
47
|
+
export function getClientIP(c) {
|
|
48
|
+
const xff = c.req.header("X-Forwarded-For");
|
|
49
|
+
if (xff) {
|
|
50
|
+
const firstIP = xff.split(",")[0]?.trim();
|
|
51
|
+
if (firstIP)
|
|
52
|
+
return firstIP;
|
|
53
|
+
}
|
|
54
|
+
return c.req.header("X-Real-IP");
|
|
55
|
+
}
|
|
16
56
|
function processException(step, e, cb) {
|
|
17
|
-
let msg = undefined;
|
|
18
57
|
// XXX - We can do a better job of determining if it's a chain
|
|
19
58
|
// error, or some other issue.
|
|
20
|
-
|
|
21
|
-
msg = e.message;
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
msg = `unknown error handling ${step}`;
|
|
25
|
-
}
|
|
59
|
+
const msg = e instanceof Error ? e.message : `unknown error handling ${step}`;
|
|
26
60
|
logger.error(`Caught exception during ${step}`, {
|
|
27
61
|
exception: e,
|
|
28
62
|
});
|
|
29
63
|
return cb(msg);
|
|
30
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Creates a Hono router with x402 facilitator endpoints.
|
|
67
|
+
*
|
|
68
|
+
* The router provides the following endpoints:
|
|
69
|
+
* - POST /verify - Verify a payment without settling
|
|
70
|
+
* - POST /settle - Verify and settle a payment
|
|
71
|
+
* - POST /accepts - Get payment requirements for a resource
|
|
72
|
+
* - GET /supported - List supported payment schemes and networks
|
|
73
|
+
*
|
|
74
|
+
* Both v1 and v2 protocol formats are supported on all endpoints.
|
|
75
|
+
*
|
|
76
|
+
* @param args - Configuration including payment handlers and timeouts
|
|
77
|
+
* @returns A Hono router instance with facilitator endpoints
|
|
78
|
+
*/
|
|
31
79
|
export function createFacilitatorRoutes(args) {
|
|
32
80
|
const router = new Hono();
|
|
33
|
-
function
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
81
|
+
function logRejected(results, label) {
|
|
82
|
+
for (const r of results) {
|
|
83
|
+
if (r.status === "rejected") {
|
|
84
|
+
const message = r.reason instanceof Error ? r.reason.message : "unknown reason";
|
|
85
|
+
logger.error(`failed to retrieve ${label} from facilitator handler: ${message}`, r.reason);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function logError(msg, stepLabel) {
|
|
37
90
|
if (msg !== undefined) {
|
|
38
|
-
response.invalidReason = msg;
|
|
39
91
|
logger.error(msg);
|
|
40
92
|
}
|
|
41
93
|
else {
|
|
42
|
-
logger.error(
|
|
94
|
+
logger.error(`unknown error during ${stepLabel}`);
|
|
43
95
|
}
|
|
96
|
+
}
|
|
97
|
+
function sendVerifyErrorV1(c, status, msg) {
|
|
98
|
+
logError(msg, "verification");
|
|
44
99
|
c.status(status);
|
|
100
|
+
const response = { isValid: false, payer: "" };
|
|
101
|
+
if (msg !== undefined) {
|
|
102
|
+
response.invalidReason = msg;
|
|
103
|
+
}
|
|
45
104
|
return c.json(response);
|
|
46
105
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
106
|
+
function sendSettleErrorV1(c, status, msg) {
|
|
107
|
+
logError(msg, "settlement");
|
|
108
|
+
c.status(status);
|
|
109
|
+
const response = {
|
|
110
|
+
success: false,
|
|
111
|
+
payer: "",
|
|
112
|
+
transaction: "",
|
|
113
|
+
network: "",
|
|
114
|
+
};
|
|
115
|
+
if (msg !== undefined) {
|
|
116
|
+
response.errorReason = msg;
|
|
51
117
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
118
|
+
return c.json(response);
|
|
119
|
+
}
|
|
120
|
+
// Accepts errors are always v1 format. A v2 request that fails to parse
|
|
121
|
+
// falls through to v1 parsing, so this path is only reached for v1 errors.
|
|
122
|
+
function sendAcceptsError(c, status, msg) {
|
|
123
|
+
logError(msg, "accepts");
|
|
124
|
+
c.status(status);
|
|
125
|
+
return c.json({ x402Version: 1, accepts: [], error: msg ?? "" });
|
|
126
|
+
}
|
|
127
|
+
function sendVerifyErrorV2(c, status, msg) {
|
|
128
|
+
logError(msg, "verification");
|
|
129
|
+
c.status(status);
|
|
130
|
+
const response = { isValid: false };
|
|
131
|
+
if (msg !== undefined) {
|
|
132
|
+
response.invalidReason = msg;
|
|
59
133
|
}
|
|
60
|
-
|
|
134
|
+
return c.json(response);
|
|
135
|
+
}
|
|
136
|
+
function sendSettleErrorV2(c, status, msg) {
|
|
137
|
+
logError(msg, "settlement");
|
|
138
|
+
c.status(status);
|
|
139
|
+
const response = {
|
|
140
|
+
success: false,
|
|
141
|
+
transaction: "",
|
|
142
|
+
network: "",
|
|
143
|
+
};
|
|
144
|
+
if (msg !== undefined) {
|
|
145
|
+
response.errorReason = msg;
|
|
146
|
+
}
|
|
147
|
+
return c.json(response);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Iterate handlers, invoking each until one returns a non-null result.
|
|
151
|
+
* Returns the handler result, or null if no handler matched.
|
|
152
|
+
* Throws on handler exceptions (caller decides how to surface the error).
|
|
153
|
+
*/
|
|
154
|
+
async function tryHandlers(invoke) {
|
|
61
155
|
for (const handler of args.handlers) {
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
|
|
156
|
+
const result = await invoke(handler);
|
|
157
|
+
if (result !== null) {
|
|
158
|
+
return result;
|
|
65
159
|
}
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
router.post("/verify", async (c) => {
|
|
164
|
+
const body = await c.req.json();
|
|
165
|
+
// Try v2 format first
|
|
166
|
+
const v2Req = x2.x402VerifyRequest(body);
|
|
167
|
+
if (!isValidationError(v2Req)) {
|
|
168
|
+
const clientIP = getClientIP(c);
|
|
169
|
+
logger.debug("starting verification attempt for v2 request", {
|
|
170
|
+
...v2Req,
|
|
171
|
+
clientIP,
|
|
172
|
+
});
|
|
173
|
+
let result;
|
|
66
174
|
try {
|
|
67
|
-
|
|
175
|
+
result = await tryHandlers((handler) => handler.handleVerify
|
|
176
|
+
? handler.handleVerify(v2Req.paymentRequirements, v2Req.paymentPayload)
|
|
177
|
+
: Promise.resolve(null));
|
|
68
178
|
}
|
|
69
179
|
catch (e) {
|
|
70
|
-
return processException("verify", e, (msg) =>
|
|
180
|
+
return processException("verify", e, (msg) => sendVerifyErrorV2(c, 500, msg));
|
|
71
181
|
}
|
|
72
|
-
if (
|
|
73
|
-
|
|
182
|
+
if (result === null) {
|
|
183
|
+
logger.warning("attempt to verify was made with no handler found, requirements summary was", summarizeRequirements(v2Req.paymentRequirements));
|
|
184
|
+
return sendVerifyErrorV2(c, 400, "no matching payment handler found");
|
|
74
185
|
}
|
|
75
|
-
logger.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
186
|
+
logger.info(`${result.isValid ? "succeeded" : "failed"} verifying v2 request`, {
|
|
187
|
+
...result,
|
|
188
|
+
requirements: summarizeRequirements(v2Req.paymentRequirements),
|
|
189
|
+
clientIP,
|
|
79
190
|
});
|
|
80
|
-
return c.json(
|
|
81
|
-
}
|
|
82
|
-
logger.warning("attempt to verify was made with no handler found, requirements summary was", summarizeRequirements(x402Req.paymentRequirements));
|
|
83
|
-
return sendVerifyError(c, 400, "no matching payment handler found");
|
|
84
|
-
});
|
|
85
|
-
function sendSettleError(c, status, msg) {
|
|
86
|
-
const response = {
|
|
87
|
-
success: false,
|
|
88
|
-
txHash: null,
|
|
89
|
-
networkId: null,
|
|
90
|
-
};
|
|
91
|
-
if (msg !== undefined) {
|
|
92
|
-
response.error = msg;
|
|
93
|
-
logger.error(msg);
|
|
191
|
+
return c.json(result);
|
|
94
192
|
}
|
|
95
|
-
|
|
96
|
-
|
|
193
|
+
// Try v1 format
|
|
194
|
+
const v1Req = x.x402VerifyRequest(body);
|
|
195
|
+
if (isValidationError(v1Req)) {
|
|
196
|
+
return sendVerifyErrorV1(c, 400, `couldn't validate request: ${v1Req.summary}`);
|
|
97
197
|
}
|
|
98
|
-
|
|
99
|
-
return c.json(response);
|
|
100
|
-
}
|
|
101
|
-
router.post("/settle", async (c) => {
|
|
102
|
-
const x402Req = x.x402SettleRequest(await c.req.json());
|
|
103
|
-
if (isValidationError(x402Req)) {
|
|
104
|
-
return sendSettleError(c, 400, `couldn't validate request: ${x402Req.summary}`);
|
|
105
|
-
}
|
|
106
|
-
let paymentPayload = x402Req.paymentPayload;
|
|
198
|
+
let paymentPayload = v1Req.paymentPayload;
|
|
107
199
|
if (paymentPayload === undefined) {
|
|
108
|
-
const decodedHeader = x.x402PaymentHeaderToPayload(
|
|
200
|
+
const decodedHeader = x.x402PaymentHeaderToPayload(v1Req.paymentHeader);
|
|
109
201
|
if (isValidationError(decodedHeader)) {
|
|
110
|
-
return
|
|
202
|
+
return sendVerifyErrorV1(c, 400, `couldn't validate x402 payload: ${decodedHeader.summary}`);
|
|
111
203
|
}
|
|
112
204
|
paymentPayload = decodedHeader;
|
|
113
205
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
206
|
+
const clientIP = getClientIP(c);
|
|
207
|
+
logger.debug("starting verification attempt for v1 request", {
|
|
208
|
+
...v1Req,
|
|
209
|
+
clientIP,
|
|
210
|
+
});
|
|
211
|
+
const v2Requirements = adaptRequirementsV1ToV2(v1Req.paymentRequirements, normalizeNetwork);
|
|
212
|
+
const v2Payload = adaptPayloadV1ToV2(paymentPayload, v1Req.paymentRequirements, normalizeNetwork);
|
|
213
|
+
let result;
|
|
214
|
+
try {
|
|
215
|
+
result = await tryHandlers((handler) => handler.handleVerify
|
|
216
|
+
? handler.handleVerify(v2Requirements, v2Payload)
|
|
217
|
+
: Promise.resolve(null));
|
|
218
|
+
}
|
|
219
|
+
catch (e) {
|
|
220
|
+
return processException("verify", e, (msg) => sendVerifyErrorV1(c, 500, msg));
|
|
221
|
+
}
|
|
222
|
+
if (result === null) {
|
|
223
|
+
logger.warning("attempt to verify was made with no handler found, requirements summary was", summarizeRequirements(v2Requirements));
|
|
224
|
+
return sendVerifyErrorV1(c, 400, "no matching payment handler found");
|
|
225
|
+
}
|
|
226
|
+
logger.info(`${result.isValid ? "succeeded" : "failed"} verifying v1 request`, {
|
|
227
|
+
...result,
|
|
228
|
+
requirements: summarizeRequirements(v2Requirements),
|
|
229
|
+
clientIP,
|
|
230
|
+
});
|
|
231
|
+
return c.json(adaptVerifyResponseV2ToV1(result));
|
|
232
|
+
});
|
|
233
|
+
router.post("/settle", async (c) => {
|
|
234
|
+
const body = await c.req.json();
|
|
235
|
+
// Try v2 format first
|
|
236
|
+
const v2Req = x2.x402SettleRequest(body);
|
|
237
|
+
if (!isValidationError(v2Req)) {
|
|
238
|
+
const clientIP = getClientIP(c);
|
|
239
|
+
logger.debug("starting settlement attempt for v2 request", {
|
|
240
|
+
...v2Req,
|
|
241
|
+
clientIP,
|
|
242
|
+
});
|
|
243
|
+
let result;
|
|
117
244
|
try {
|
|
118
|
-
|
|
245
|
+
result = await tryHandlers((handler) => handler.handleSettle(v2Req.paymentRequirements, v2Req.paymentPayload));
|
|
119
246
|
}
|
|
120
247
|
catch (e) {
|
|
121
|
-
return processException("settle", e, (msg) =>
|
|
248
|
+
return processException("settle", e, (msg) => sendSettleErrorV2(c, 500, msg));
|
|
122
249
|
}
|
|
123
|
-
if (
|
|
124
|
-
|
|
250
|
+
if (result === null) {
|
|
251
|
+
logger.warning("attempt to settle was made with no handler found, requirements summary was", summarizeRequirements(v2Req.paymentRequirements));
|
|
252
|
+
return sendSettleErrorV2(c, 400, "no matching payment handler found");
|
|
125
253
|
}
|
|
126
|
-
logger.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
254
|
+
logger.info(`${result.success ? "succeeded" : "failed"} settlement v2 request`, {
|
|
255
|
+
requirements: summarizeRequirements(v2Req.paymentRequirements),
|
|
256
|
+
transaction: result.transaction,
|
|
257
|
+
clientIP,
|
|
130
258
|
});
|
|
131
|
-
return c.json(
|
|
259
|
+
return c.json(result);
|
|
132
260
|
}
|
|
133
|
-
|
|
134
|
-
|
|
261
|
+
// Try v1 format
|
|
262
|
+
const v1Req = x.x402SettleRequest(body);
|
|
263
|
+
if (isValidationError(v1Req)) {
|
|
264
|
+
return sendSettleErrorV1(c, 400, `couldn't validate request: ${v1Req.summary}`);
|
|
265
|
+
}
|
|
266
|
+
let paymentPayload = v1Req.paymentPayload;
|
|
267
|
+
if (paymentPayload === undefined) {
|
|
268
|
+
const decodedHeader = x.x402PaymentHeaderToPayload(v1Req.paymentHeader);
|
|
269
|
+
if (isValidationError(decodedHeader)) {
|
|
270
|
+
return sendSettleErrorV1(c, 400, `couldn't validate x402 payload: ${decodedHeader.summary}`);
|
|
271
|
+
}
|
|
272
|
+
paymentPayload = decodedHeader;
|
|
273
|
+
}
|
|
274
|
+
const clientIP = getClientIP(c);
|
|
275
|
+
logger.debug("starting settlement attempt for v1 request", {
|
|
276
|
+
...v1Req,
|
|
277
|
+
clientIP,
|
|
278
|
+
});
|
|
279
|
+
const v2Requirements = adaptRequirementsV1ToV2(v1Req.paymentRequirements, normalizeNetwork);
|
|
280
|
+
const v2Payload = adaptPayloadV1ToV2(paymentPayload, v1Req.paymentRequirements, normalizeNetwork);
|
|
281
|
+
let result;
|
|
282
|
+
try {
|
|
283
|
+
result = await tryHandlers((handler) => handler.handleSettle(v2Requirements, v2Payload));
|
|
284
|
+
}
|
|
285
|
+
catch (e) {
|
|
286
|
+
return processException("settle", e, (msg) => sendSettleErrorV1(c, 500, msg));
|
|
287
|
+
}
|
|
288
|
+
if (result === null) {
|
|
289
|
+
logger.warning("attempt to settle was made with no handler found, requirements summary was", summarizeRequirements(v2Requirements));
|
|
290
|
+
return sendSettleErrorV1(c, 400, "no matching payment handler found");
|
|
291
|
+
}
|
|
292
|
+
logger.info(`${result.success ? "succeeded" : "failed"} settlement v1 request`, {
|
|
293
|
+
requirements: summarizeRequirements(v2Requirements),
|
|
294
|
+
transaction: result.transaction,
|
|
295
|
+
clientIP,
|
|
296
|
+
});
|
|
297
|
+
return c.json(adaptSettleResponseV2ToV1(result, translateNetwork));
|
|
135
298
|
});
|
|
136
299
|
router.post("/accepts", async (c) => {
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
.
|
|
300
|
+
const body = await c.req.json();
|
|
301
|
+
const clientIP = getClientIP(c);
|
|
302
|
+
// The /accepts request body shares the same shape as the payment required
|
|
303
|
+
// response (resource + accepts array), so we reuse the response validator.
|
|
304
|
+
const v2Req = x2.x402PaymentRequiredResponse(body);
|
|
305
|
+
if (!isValidationError(v2Req)) {
|
|
306
|
+
// Native v2 request - call handlers directly with v2 requirements
|
|
307
|
+
const results = await allSettledWithTimeout(args.handlers.flatMap((handler) => handler.getRequirements({
|
|
308
|
+
accepts: v2Req.accepts,
|
|
309
|
+
resource: v2Req.resource,
|
|
310
|
+
})), args.timeout?.getRequirements ?? 500);
|
|
311
|
+
const accepts = results
|
|
312
|
+
.filter((r) => r.status === "fulfilled")
|
|
313
|
+
.map((r) => r.value)
|
|
314
|
+
.flat();
|
|
315
|
+
logRejected(results, "requirements");
|
|
316
|
+
logger.debug(`returning ${accepts.length} accepts for v2 request`, {
|
|
317
|
+
accepts: accepts.map(summarizeRequirements),
|
|
318
|
+
clientIP,
|
|
319
|
+
});
|
|
320
|
+
c.status(200);
|
|
321
|
+
return c.json({
|
|
322
|
+
x402Version: 2,
|
|
323
|
+
resource: v2Req.resource,
|
|
324
|
+
accepts,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
// Try v1 format (lenient: Coinbase's implementation omits the error field)
|
|
328
|
+
const v1Req = x.x402PaymentRequiredResponseLenient(body);
|
|
329
|
+
if (isValidationError(v1Req)) {
|
|
330
|
+
return sendAcceptsError(c, 400, `couldn't parse required response: ${v1Req.summary}`);
|
|
331
|
+
}
|
|
332
|
+
// Adapt v1 accepts to v2, normalizing legacy network names to CAIP-2
|
|
333
|
+
const v2Accepts = v1Req.accepts.map((req) => adaptRequirementsV1ToV2(req, normalizeNetwork));
|
|
334
|
+
const resourceInfo = v1Req.accepts[0]
|
|
335
|
+
? extractResourceInfoV1(v1Req.accepts[0])
|
|
336
|
+
: { url: "" };
|
|
337
|
+
const results = await allSettledWithTimeout(args.handlers.flatMap((handler) => handler.getRequirements({
|
|
338
|
+
accepts: v2Accepts,
|
|
339
|
+
resource: resourceInfo,
|
|
340
|
+
})), args.timeout?.getRequirements ?? 500);
|
|
341
|
+
const v2Results = results
|
|
342
|
+
.filter((r) => r.status === "fulfilled")
|
|
343
|
+
.map((r) => r.value)
|
|
145
344
|
.flat();
|
|
146
|
-
results
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
});
|
|
158
|
-
logger.debug(`returning ${accepts.length} accepts`, {
|
|
159
|
-
accepts: accepts.map(summarizeRequirements),
|
|
345
|
+
logRejected(results, "requirements");
|
|
346
|
+
// Adapt v2 results back to v1 format
|
|
347
|
+
const accepts = v2Results.map((r) => adaptRequirementsV2ToV1(r, resourceInfo, translateNetwork));
|
|
348
|
+
logger.debug(`returning ${accepts.length} accepts for v1 request`, {
|
|
349
|
+
accepts: accepts.map((a) => ({
|
|
350
|
+
scheme: a.scheme,
|
|
351
|
+
network: a.network,
|
|
352
|
+
asset: a.asset,
|
|
353
|
+
payTo: a.payTo,
|
|
354
|
+
})),
|
|
355
|
+
clientIP,
|
|
160
356
|
});
|
|
161
357
|
c.status(200);
|
|
162
358
|
return c.json({
|
|
163
359
|
x402Version: 1,
|
|
164
360
|
accepts,
|
|
361
|
+
error: "",
|
|
165
362
|
});
|
|
166
363
|
});
|
|
167
364
|
router.get("/supported", async (c) => {
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
.
|
|
365
|
+
const clientIP = getClientIP(c);
|
|
366
|
+
const results = await allSettledWithTimeout(args.handlers.flatMap((handler) => handler.getSupported ? handler.getSupported() : []), args.timeout?.getSupported ?? 500);
|
|
367
|
+
const v2Kinds = results
|
|
368
|
+
.filter((r) => r.status === "fulfilled")
|
|
369
|
+
.map((r) => r.value)
|
|
172
370
|
.flat();
|
|
173
|
-
results
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
371
|
+
logRejected(results, "supported");
|
|
372
|
+
// Aggregate signers from handlers
|
|
373
|
+
const signers = {};
|
|
374
|
+
for (const handler of args.handlers) {
|
|
375
|
+
if (handler.getSigners) {
|
|
376
|
+
try {
|
|
377
|
+
const handlerSigners = await handler.getSigners();
|
|
378
|
+
for (const [network, addresses] of Object.entries(handlerSigners)) {
|
|
379
|
+
if (signers[network]) {
|
|
380
|
+
// Merge addresses, avoiding duplicates
|
|
381
|
+
const existing = new Set(signers[network]);
|
|
382
|
+
for (const addr of addresses) {
|
|
383
|
+
if (!existing.has(addr)) {
|
|
384
|
+
signers[network].push(addr);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
signers[network] = [...addresses];
|
|
390
|
+
}
|
|
391
|
+
}
|
|
178
392
|
}
|
|
179
|
-
|
|
180
|
-
|
|
393
|
+
catch (e) {
|
|
394
|
+
logger.error("failed to retrieve signers from facilitator handler", {
|
|
395
|
+
error: e,
|
|
396
|
+
});
|
|
181
397
|
}
|
|
182
|
-
logger.error(`failed to retrieve supported from facilitator handler: ${message}`, x.reason);
|
|
183
398
|
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
|
|
399
|
+
}
|
|
400
|
+
// Advertise both v1 and v2 support for all kinds
|
|
401
|
+
const v1Kinds = v2Kinds.map((k) => adaptSupportedKindV2ToV1(k, translateNetwork));
|
|
402
|
+
const allKinds = [...v1Kinds, ...v2Kinds];
|
|
403
|
+
logger.debug(`returning ${allKinds.length} kinds supported`, {
|
|
404
|
+
kinds: allKinds,
|
|
405
|
+
clientIP,
|
|
187
406
|
});
|
|
188
407
|
c.status(200);
|
|
189
408
|
return c.json({
|
|
190
|
-
kinds,
|
|
409
|
+
kinds: allKinds,
|
|
410
|
+
extensions: [],
|
|
411
|
+
signers,
|
|
191
412
|
});
|
|
192
413
|
});
|
|
193
414
|
return router;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.test.d.ts","sourceRoot":"","sources":["../../src/routes.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env pnpm tsx
|
|
2
|
+
import t from "tap";
|
|
3
|
+
import { getClientIP } from "./routes.js";
|
|
4
|
+
function mockContext(headers) {
|
|
5
|
+
return {
|
|
6
|
+
req: {
|
|
7
|
+
header: (key) => headers[key],
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
await t.test("getClientIP returns first IP from X-Forwarded-For", async (t) => {
|
|
12
|
+
const c = mockContext({
|
|
13
|
+
"X-Forwarded-For": "192.168.1.1, 10.0.0.1, 172.16.0.1",
|
|
14
|
+
});
|
|
15
|
+
t.equal(getClientIP(c), "192.168.1.1");
|
|
16
|
+
t.end();
|
|
17
|
+
});
|
|
18
|
+
await t.test("getClientIP trims whitespace from X-Forwarded-For", async (t) => {
|
|
19
|
+
const c = mockContext({
|
|
20
|
+
"X-Forwarded-For": " 192.168.1.1 , 10.0.0.1",
|
|
21
|
+
});
|
|
22
|
+
t.equal(getClientIP(c), "192.168.1.1");
|
|
23
|
+
t.end();
|
|
24
|
+
});
|
|
25
|
+
await t.test("getClientIP returns single IP from X-Forwarded-For", async (t) => {
|
|
26
|
+
const c = mockContext({
|
|
27
|
+
"X-Forwarded-For": "192.168.1.1",
|
|
28
|
+
});
|
|
29
|
+
t.equal(getClientIP(c), "192.168.1.1");
|
|
30
|
+
t.end();
|
|
31
|
+
});
|
|
32
|
+
await t.test("getClientIP falls back to X-Real-IP when X-Forwarded-For is missing", async (t) => {
|
|
33
|
+
const c = mockContext({
|
|
34
|
+
"X-Real-IP": "10.0.0.1",
|
|
35
|
+
});
|
|
36
|
+
t.equal(getClientIP(c), "10.0.0.1");
|
|
37
|
+
t.end();
|
|
38
|
+
});
|
|
39
|
+
await t.test("getClientIP prefers X-Forwarded-For over X-Real-IP", async (t) => {
|
|
40
|
+
const c = mockContext({
|
|
41
|
+
"X-Forwarded-For": "192.168.1.1",
|
|
42
|
+
"X-Real-IP": "10.0.0.1",
|
|
43
|
+
});
|
|
44
|
+
t.equal(getClientIP(c), "192.168.1.1");
|
|
45
|
+
t.end();
|
|
46
|
+
});
|
|
47
|
+
await t.test("getClientIP returns undefined when no proxy headers present", async (t) => {
|
|
48
|
+
const c = mockContext({});
|
|
49
|
+
t.equal(getClientIP(c), undefined);
|
|
50
|
+
t.end();
|
|
51
|
+
});
|
|
52
|
+
await t.test("getClientIP returns undefined for empty X-Forwarded-For", async (t) => {
|
|
53
|
+
const c = mockContext({
|
|
54
|
+
"X-Forwarded-For": "",
|
|
55
|
+
});
|
|
56
|
+
t.equal(getClientIP(c), undefined);
|
|
57
|
+
t.end();
|
|
58
|
+
});
|
|
59
|
+
await t.test("getClientIP returns undefined for whitespace-only X-Forwarded-For", async (t) => {
|
|
60
|
+
const c = mockContext({
|
|
61
|
+
"X-Forwarded-For": " ",
|
|
62
|
+
});
|
|
63
|
+
t.equal(getClientIP(c), undefined);
|
|
64
|
+
t.end();
|
|
65
|
+
});
|