@faremeter/middleware 0.16.0 → 0.17.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/src/cache.d.ts +12 -0
- package/dist/src/cache.d.ts.map +1 -1
- package/dist/src/cache.js +6 -0
- package/dist/src/common.d.ts +174 -13
- package/dist/src/common.d.ts.map +1 -1
- package/dist/src/common.js +345 -37
- package/dist/src/common.test.d.ts +3 -0
- package/dist/src/common.test.d.ts.map +1 -0
- package/dist/src/common.test.js +50 -0
- package/dist/src/express.d.ts +10 -0
- package/dist/src/express.d.ts.map +1 -1
- package/dist/src/express.js +29 -3
- package/dist/src/hono.d.ts +14 -0
- package/dist/src/hono.d.ts.map +1 -1
- package/dist/src/hono.js +27 -4
- package/dist/src/index.d.ts +26 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +26 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -3
package/dist/src/common.js
CHANGED
|
@@ -1,26 +1,69 @@
|
|
|
1
1
|
import { isValidationError } from "@faremeter/types";
|
|
2
|
-
import {
|
|
2
|
+
import { x402PaymentRequiredResponseLenient, normalizePaymentRequiredResponse, x402PaymentHeaderToPayload as x402PaymentHeaderToPayloadV1, x402VerifyRequest as x402VerifyRequestV1, x402VerifyResponseLenient, normalizeVerifyResponse, x402SettleRequest as x402SettleRequestV1, x402SettleResponse as x402SettleResponseV1, x402SettleResponseLenient, normalizeSettleResponse, X_PAYMENT_HEADER, X_PAYMENT_RESPONSE_HEADER, } from "@faremeter/types/x402";
|
|
3
|
+
import { x402PaymentRequiredResponse, x402PaymentHeaderToPayload, x402VerifyRequest, x402VerifyResponse, x402SettleRequest, x402SettleResponse, V2_PAYMENT_HEADER, V2_PAYMENT_REQUIRED_HEADER, V2_PAYMENT_RESPONSE_HEADER, } from "@faremeter/types/x402v2";
|
|
4
|
+
import { adaptPaymentRequiredResponseV1ToV2, adaptPaymentRequiredResponseV2ToV1, } from "@faremeter/types/x402-adapters";
|
|
5
|
+
import { normalizeNetworkId } from "@faremeter/info";
|
|
3
6
|
import { AgedLRUCache } from "./cache.js";
|
|
4
7
|
import { logger } from "./logger.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Build the X-PAYMENT-RESPONSE header value from a settle response.
|
|
10
|
+
*
|
|
11
|
+
* The header uses spec-compliant field names: `transaction`, `network`,
|
|
12
|
+
* and `errorReason`.
|
|
13
|
+
*/
|
|
14
|
+
function buildPaymentResponseHeader(settlementResponse) {
|
|
15
|
+
const headerPayload = {
|
|
16
|
+
success: settlementResponse.success,
|
|
17
|
+
transaction: settlementResponse.transaction ?? "",
|
|
18
|
+
network: settlementResponse.network ?? "",
|
|
19
|
+
};
|
|
20
|
+
if (settlementResponse.payer !== undefined) {
|
|
21
|
+
headerPayload.payer = settlementResponse.payer;
|
|
12
22
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
23
|
+
if (!settlementResponse.success &&
|
|
24
|
+
settlementResponse.errorReason !== undefined) {
|
|
25
|
+
headerPayload.errorReason = settlementResponse.errorReason;
|
|
16
26
|
}
|
|
27
|
+
return btoa(JSON.stringify(headerPayload));
|
|
28
|
+
}
|
|
29
|
+
function findMatching(accepts, criteria, label, payload) {
|
|
30
|
+
const possible = criteria.asset !== undefined
|
|
31
|
+
? accepts.filter((x) => x.network === criteria.network &&
|
|
32
|
+
x.scheme === criteria.scheme &&
|
|
33
|
+
x.asset === criteria.asset)
|
|
34
|
+
: accepts.filter((x) => x.network === criteria.network && x.scheme === criteria.scheme);
|
|
17
35
|
if (possible.length > 1) {
|
|
18
|
-
logger.warning(`found ${possible.length} ambiguous matching requirements for client payment`, payload);
|
|
36
|
+
logger.warning(`found ${possible.length} ambiguous matching requirements for ${label} client payment`, payload);
|
|
19
37
|
}
|
|
20
38
|
// XXX - If there are more than one, this really should be an error.
|
|
21
39
|
// For now, err on the side of potential compatibility.
|
|
22
40
|
return possible[0];
|
|
23
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Finds the payment requirement that matches the client's v1 payment payload.
|
|
44
|
+
*
|
|
45
|
+
* @param accepts - Array of accepted payment requirements from the facilitator
|
|
46
|
+
* @param payload - The client's payment payload
|
|
47
|
+
* @returns The matching requirement, or undefined if no match found
|
|
48
|
+
*/
|
|
49
|
+
export function findMatchingPaymentRequirements(accepts, payload) {
|
|
50
|
+
return findMatching(accepts, payload, "v1", payload);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Finds the payment requirement that matches the client's v2 payment payload.
|
|
54
|
+
*
|
|
55
|
+
* @param accepts - Array of accepted payment requirements from the facilitator
|
|
56
|
+
* @param payload - The client's v2 payment payload
|
|
57
|
+
* @returns The matching requirement, or undefined if no match found
|
|
58
|
+
*/
|
|
59
|
+
export function findMatchingPaymentRequirementsV2(accepts, payload) {
|
|
60
|
+
return findMatching(accepts, payload.accepted, "v2", payload);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Validates that a facilitator response is successful, throwing if not.
|
|
64
|
+
*
|
|
65
|
+
* @param res - The Response from the facilitator
|
|
66
|
+
*/
|
|
24
67
|
export function gateGetPaymentRequiredResponse(res) {
|
|
25
68
|
if (res.status === 200) {
|
|
26
69
|
return;
|
|
@@ -29,6 +72,54 @@ export function gateGetPaymentRequiredResponse(res) {
|
|
|
29
72
|
logger.error(msg);
|
|
30
73
|
throw new Error(msg);
|
|
31
74
|
}
|
|
75
|
+
function relaxedRequirementsToV2(req) {
|
|
76
|
+
const result = {};
|
|
77
|
+
if (req.scheme !== undefined)
|
|
78
|
+
result.scheme = req.scheme;
|
|
79
|
+
if (req.network !== undefined)
|
|
80
|
+
result.network = req.network;
|
|
81
|
+
if (req.maxAmountRequired !== undefined)
|
|
82
|
+
result.amount = req.maxAmountRequired;
|
|
83
|
+
if (req.asset !== undefined)
|
|
84
|
+
result.asset = req.asset;
|
|
85
|
+
if (req.payTo !== undefined)
|
|
86
|
+
result.payTo = req.payTo;
|
|
87
|
+
if (req.maxTimeoutSeconds !== undefined)
|
|
88
|
+
result.maxTimeoutSeconds = req.maxTimeoutSeconds;
|
|
89
|
+
if (req.extra !== undefined)
|
|
90
|
+
result.extra = req.extra;
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Parse a payment header from either v1 (X-PAYMENT) or v2 (PAYMENT-SIGNATURE).
|
|
95
|
+
* Returns the parsed payload with version discriminant, or undefined if no valid header found.
|
|
96
|
+
*/
|
|
97
|
+
function parsePaymentHeader(getHeader) {
|
|
98
|
+
// Try v2 header first
|
|
99
|
+
const v2Header = getHeader(V2_PAYMENT_HEADER);
|
|
100
|
+
if (v2Header) {
|
|
101
|
+
const v2Payload = x402PaymentHeaderToPayload(v2Header);
|
|
102
|
+
if (!isValidationError(v2Payload)) {
|
|
103
|
+
return { version: 2, payload: v2Payload, rawHeader: v2Header };
|
|
104
|
+
}
|
|
105
|
+
logger.debug(`couldn't validate v2 client payload: ${v2Payload.summary}`);
|
|
106
|
+
}
|
|
107
|
+
const v1Header = getHeader(X_PAYMENT_HEADER);
|
|
108
|
+
if (v1Header) {
|
|
109
|
+
const v1Payload = x402PaymentHeaderToPayloadV1(v1Header);
|
|
110
|
+
if (!isValidationError(v1Payload)) {
|
|
111
|
+
return { version: 1, payload: v1Payload, rawHeader: v1Header };
|
|
112
|
+
}
|
|
113
|
+
logger.debug(`couldn't validate v1 client payload: ${v1Payload.summary}`);
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Fetches v1 payment requirements from the facilitator's /accepts endpoint.
|
|
119
|
+
*
|
|
120
|
+
* @param args - Arguments including facilitator URL and accepted payment types
|
|
121
|
+
* @returns The validated payment required response from the facilitator
|
|
122
|
+
*/
|
|
32
123
|
export async function getPaymentRequiredResponse(args) {
|
|
33
124
|
const fetchFn = args.fetch ?? fetch;
|
|
34
125
|
const accepts = args.accepts.map((x) => ({
|
|
@@ -44,46 +135,242 @@ export async function getPaymentRequiredResponse(args) {
|
|
|
44
135
|
body: JSON.stringify({
|
|
45
136
|
x402Version: 1,
|
|
46
137
|
accepts,
|
|
138
|
+
error: "",
|
|
139
|
+
}),
|
|
140
|
+
});
|
|
141
|
+
gateGetPaymentRequiredResponse(t);
|
|
142
|
+
const rawResponse = x402PaymentRequiredResponseLenient(await t.json());
|
|
143
|
+
if (isValidationError(rawResponse)) {
|
|
144
|
+
throw new Error(`invalid payment requirements from facilitator: ${rawResponse.summary}`);
|
|
145
|
+
}
|
|
146
|
+
return normalizePaymentRequiredResponse(rawResponse);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Fetches v2 payment requirements from the facilitator's /accepts endpoint.
|
|
150
|
+
*
|
|
151
|
+
* @param args - Arguments including facilitator URL, resource info, and accepted payment types
|
|
152
|
+
* @returns The validated v2 payment required response from the facilitator
|
|
153
|
+
*/
|
|
154
|
+
export async function getPaymentRequiredResponseV2(args) {
|
|
155
|
+
const fetchFn = args.fetch ?? fetch;
|
|
156
|
+
const t = await fetchFn(`${args.facilitatorURL}/accepts`, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: {
|
|
159
|
+
Accept: "application/json",
|
|
160
|
+
"Content-Type": "application/json",
|
|
161
|
+
},
|
|
162
|
+
body: JSON.stringify({
|
|
163
|
+
x402Version: 2,
|
|
164
|
+
resource: args.resource,
|
|
165
|
+
accepts: args.accepts,
|
|
47
166
|
}),
|
|
48
167
|
});
|
|
49
168
|
gateGetPaymentRequiredResponse(t);
|
|
50
169
|
const response = x402PaymentRequiredResponse(await t.json());
|
|
51
170
|
if (isValidationError(response)) {
|
|
52
|
-
throw new Error(`invalid payment requirements from facilitator: ${response.summary}`);
|
|
171
|
+
throw new Error(`invalid v2 payment requirements from facilitator: ${response.summary}`);
|
|
53
172
|
}
|
|
54
173
|
return response;
|
|
55
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Resolve and validate supported versions config.
|
|
177
|
+
* Returns resolved config with defaults applied.
|
|
178
|
+
* Throws if configuration is invalid.
|
|
179
|
+
*/
|
|
180
|
+
export function resolveSupportedVersions(config) {
|
|
181
|
+
const resolved = {
|
|
182
|
+
x402v1: config?.x402v1 ?? true,
|
|
183
|
+
x402v2: config?.x402v2 ?? false,
|
|
184
|
+
};
|
|
185
|
+
if (!resolved.x402v1 && !resolved.x402v2) {
|
|
186
|
+
throw new Error("Invalid supportedVersions configuration: at least one protocol version must be enabled");
|
|
187
|
+
}
|
|
188
|
+
return resolved;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Core middleware request handler that processes x402 payment flows.
|
|
192
|
+
*
|
|
193
|
+
* This function handles both v1 and v2 protocol versions, validates payment
|
|
194
|
+
* headers, communicates with the facilitator, and delegates to the body
|
|
195
|
+
* handler when payment is valid.
|
|
196
|
+
*
|
|
197
|
+
* @param args - Handler arguments including framework-specific adapters
|
|
198
|
+
* @returns The middleware response, or undefined if the body handler should continue
|
|
199
|
+
*/
|
|
56
200
|
export async function handleMiddlewareRequest(args) {
|
|
57
201
|
const accepts = args.accepts.flat();
|
|
58
202
|
const fetchFn = args.fetch ?? fetch;
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
203
|
+
const { supportedVersions } = args;
|
|
204
|
+
// Fetch requirements in the highest supported version, adapt downward as needed.
|
|
205
|
+
let v1Response;
|
|
206
|
+
let v2Response;
|
|
207
|
+
if (supportedVersions.x402v2 && args.getPaymentRequiredResponseV2) {
|
|
208
|
+
const firstAccept = accepts.find((a) => a.resource !== undefined);
|
|
209
|
+
const resourceInfo = {
|
|
210
|
+
url: firstAccept?.resource ?? args.resource,
|
|
211
|
+
};
|
|
212
|
+
if (firstAccept?.description) {
|
|
213
|
+
resourceInfo.description = firstAccept.description;
|
|
214
|
+
}
|
|
215
|
+
if (firstAccept?.mimeType) {
|
|
216
|
+
resourceInfo.mimeType = firstAccept.mimeType;
|
|
217
|
+
}
|
|
218
|
+
v2Response = await args.getPaymentRequiredResponseV2({
|
|
219
|
+
accepts: accepts.map(relaxedRequirementsToV2),
|
|
220
|
+
facilitatorURL: args.facilitatorURL,
|
|
221
|
+
resource: resourceInfo,
|
|
222
|
+
fetch: fetchFn,
|
|
223
|
+
});
|
|
224
|
+
if (supportedVersions.x402v1) {
|
|
225
|
+
v1Response = adaptPaymentRequiredResponseV2ToV1(v2Response);
|
|
226
|
+
}
|
|
69
227
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
228
|
+
else {
|
|
229
|
+
v1Response = await args.getPaymentRequiredResponse({
|
|
230
|
+
accepts,
|
|
231
|
+
facilitatorURL: args.facilitatorURL,
|
|
232
|
+
resource: args.resource,
|
|
233
|
+
fetch: fetchFn,
|
|
234
|
+
});
|
|
235
|
+
if (supportedVersions.x402v2) {
|
|
236
|
+
v2Response = adaptPaymentRequiredResponseV1ToV2(v1Response, args.resource, normalizeNetworkId);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const parsedHeader = parsePaymentHeader(args.getHeader);
|
|
240
|
+
const sendPaymentRequired = () => {
|
|
241
|
+
if (supportedVersions.x402v2 && v2Response) {
|
|
242
|
+
const v2Headers = {
|
|
243
|
+
[V2_PAYMENT_REQUIRED_HEADER]: btoa(JSON.stringify(v2Response)),
|
|
244
|
+
};
|
|
245
|
+
if (supportedVersions.x402v1 && v1Response) {
|
|
246
|
+
return args.sendJSONResponse(402, v1Response, v2Headers);
|
|
247
|
+
}
|
|
248
|
+
return args.sendJSONResponse(402, v2Response.error ? { error: v2Response.error } : undefined, v2Headers);
|
|
249
|
+
}
|
|
250
|
+
if (v1Response) {
|
|
251
|
+
return args.sendJSONResponse(402, v1Response);
|
|
252
|
+
}
|
|
253
|
+
throw new Error("no payment required response available");
|
|
254
|
+
};
|
|
255
|
+
if (!parsedHeader) {
|
|
73
256
|
return sendPaymentRequired();
|
|
74
257
|
}
|
|
258
|
+
if (parsedHeader.version === 2 && !supportedVersions.x402v2) {
|
|
259
|
+
return args.sendJSONResponse(400, {
|
|
260
|
+
error: "This server does not support x402 protocol version 2",
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
if (parsedHeader.version === 1 && !supportedVersions.x402v1) {
|
|
264
|
+
return args.sendJSONResponse(400, {
|
|
265
|
+
error: "This server does not support x402 protocol version 1",
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
if (parsedHeader.version === 2) {
|
|
269
|
+
if (!v2Response) {
|
|
270
|
+
throw new Error("v2 response unavailable for v2 request");
|
|
271
|
+
}
|
|
272
|
+
return handleV2Request(args, parsedHeader.payload, v2Response, sendPaymentRequired, fetchFn);
|
|
273
|
+
}
|
|
274
|
+
if (!v1Response) {
|
|
275
|
+
throw new Error("v1 response unavailable for v1 request");
|
|
276
|
+
}
|
|
277
|
+
return handleV1Request(args, parsedHeader.payload, parsedHeader.rawHeader, v1Response, sendPaymentRequired, fetchFn);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Handle v1 protocol request.
|
|
281
|
+
*/
|
|
282
|
+
async function handleV1Request(args, paymentPayload, paymentHeader, paymentRequiredResponse, sendPaymentRequired, fetchFn) {
|
|
75
283
|
const paymentRequirements = findMatchingPaymentRequirements(paymentRequiredResponse.accepts, paymentPayload);
|
|
76
284
|
if (!paymentRequirements) {
|
|
77
|
-
logger.warning(`couldn't find matching payment requirements for payload`, paymentPayload);
|
|
285
|
+
logger.warning(`couldn't find matching payment requirements for v1 payload`, paymentPayload);
|
|
78
286
|
return sendPaymentRequired();
|
|
79
287
|
}
|
|
80
288
|
const settle = async () => {
|
|
81
289
|
const settleRequest = {
|
|
82
|
-
x402Version: 1,
|
|
83
290
|
paymentHeader,
|
|
84
291
|
paymentPayload,
|
|
85
292
|
paymentRequirements,
|
|
86
293
|
};
|
|
294
|
+
const t = await fetchFn(`${args.facilitatorURL}/settle`, {
|
|
295
|
+
method: "POST",
|
|
296
|
+
headers: {
|
|
297
|
+
Accept: "application/json",
|
|
298
|
+
"Content-Type": "application/json",
|
|
299
|
+
},
|
|
300
|
+
// x402Version is not part of the v1 spec but older Faremeter
|
|
301
|
+
// facilitators require it, so include it for backwards compatibility.
|
|
302
|
+
body: JSON.stringify({ x402Version: 1, ...settleRequest }),
|
|
303
|
+
});
|
|
304
|
+
// Parse with lenient type to accept both legacy and spec-compliant field names
|
|
305
|
+
const rawSettlementResponse = x402SettleResponseLenient(await t.json());
|
|
306
|
+
if (isValidationError(rawSettlementResponse)) {
|
|
307
|
+
const msg = `error getting response from facilitator for settlement: ${rawSettlementResponse.summary}`;
|
|
308
|
+
logger.error(msg);
|
|
309
|
+
throw new Error(msg);
|
|
310
|
+
}
|
|
311
|
+
// Normalize to spec-compliant field names
|
|
312
|
+
const settlementResponse = normalizeSettleResponse(rawSettlementResponse);
|
|
313
|
+
// Set the X-PAYMENT-RESPONSE header for both success and failure
|
|
314
|
+
if (args.setResponseHeader) {
|
|
315
|
+
args.setResponseHeader(X_PAYMENT_RESPONSE_HEADER, buildPaymentResponseHeader(settlementResponse));
|
|
316
|
+
}
|
|
317
|
+
if (!settlementResponse.success) {
|
|
318
|
+
logger.warning("failed to settle payment: {errorReason}", settlementResponse);
|
|
319
|
+
return { success: false, errorResponse: sendPaymentRequired() };
|
|
320
|
+
}
|
|
321
|
+
return { success: true, facilitatorResponse: settlementResponse };
|
|
322
|
+
};
|
|
323
|
+
const verify = async () => {
|
|
324
|
+
const verifyRequest = {
|
|
325
|
+
paymentHeader,
|
|
326
|
+
paymentPayload,
|
|
327
|
+
paymentRequirements,
|
|
328
|
+
};
|
|
329
|
+
const t = await fetchFn(`${args.facilitatorURL}/verify`, {
|
|
330
|
+
method: "POST",
|
|
331
|
+
headers: {
|
|
332
|
+
Accept: "application/json",
|
|
333
|
+
"Content-Type": "application/json",
|
|
334
|
+
},
|
|
335
|
+
// x402Version is not part of the v1 spec but older Faremeter
|
|
336
|
+
// facilitators require it, so include it for backwards compatibility.
|
|
337
|
+
body: JSON.stringify({ x402Version: 1, ...verifyRequest }),
|
|
338
|
+
});
|
|
339
|
+
const rawVerifyResponse = x402VerifyResponseLenient(await t.json());
|
|
340
|
+
if (isValidationError(rawVerifyResponse)) {
|
|
341
|
+
const msg = `error getting response from facilitator for verification: ${rawVerifyResponse.summary}`;
|
|
342
|
+
logger.error(msg);
|
|
343
|
+
throw new Error(msg);
|
|
344
|
+
}
|
|
345
|
+
const verifyResponse = normalizeVerifyResponse(rawVerifyResponse);
|
|
346
|
+
if (!verifyResponse.isValid) {
|
|
347
|
+
logger.warning("failed to verify payment: {invalidReason}", verifyResponse);
|
|
348
|
+
return { success: false, errorResponse: sendPaymentRequired() };
|
|
349
|
+
}
|
|
350
|
+
return { success: true, facilitatorResponse: verifyResponse };
|
|
351
|
+
};
|
|
352
|
+
return await args.body({
|
|
353
|
+
protocolVersion: 1,
|
|
354
|
+
paymentRequirements,
|
|
355
|
+
paymentPayload,
|
|
356
|
+
settle,
|
|
357
|
+
verify,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Handle v2 protocol request.
|
|
362
|
+
*/
|
|
363
|
+
async function handleV2Request(args, paymentPayload, paymentRequiredResponse, sendPaymentRequired, fetchFn) {
|
|
364
|
+
const paymentRequirements = findMatchingPaymentRequirementsV2(paymentRequiredResponse.accepts, paymentPayload);
|
|
365
|
+
if (!paymentRequirements) {
|
|
366
|
+
logger.warning(`couldn't find matching payment requirements for v2 payload`, paymentPayload);
|
|
367
|
+
return sendPaymentRequired();
|
|
368
|
+
}
|
|
369
|
+
const settle = async () => {
|
|
370
|
+
const settleRequest = {
|
|
371
|
+
paymentPayload,
|
|
372
|
+
paymentRequirements,
|
|
373
|
+
};
|
|
87
374
|
const t = await fetchFn(`${args.facilitatorURL}/settle`, {
|
|
88
375
|
method: "POST",
|
|
89
376
|
headers: {
|
|
@@ -94,20 +381,21 @@ export async function handleMiddlewareRequest(args) {
|
|
|
94
381
|
});
|
|
95
382
|
const settlementResponse = x402SettleResponse(await t.json());
|
|
96
383
|
if (isValidationError(settlementResponse)) {
|
|
97
|
-
const msg = `error getting response from facilitator for settlement: ${settlementResponse.summary}`;
|
|
384
|
+
const msg = `error getting response from facilitator for v2 settlement: ${settlementResponse.summary}`;
|
|
98
385
|
logger.error(msg);
|
|
99
386
|
throw new Error(msg);
|
|
100
387
|
}
|
|
388
|
+
if (args.setResponseHeader) {
|
|
389
|
+
args.setResponseHeader(V2_PAYMENT_RESPONSE_HEADER, btoa(JSON.stringify(settlementResponse)));
|
|
390
|
+
}
|
|
101
391
|
if (!settlementResponse.success) {
|
|
102
|
-
logger.warning("failed to settle payment: {
|
|
392
|
+
logger.warning("failed to settle v2 payment: {errorReason}", settlementResponse);
|
|
103
393
|
return { success: false, errorResponse: sendPaymentRequired() };
|
|
104
394
|
}
|
|
105
395
|
return { success: true, facilitatorResponse: settlementResponse };
|
|
106
396
|
};
|
|
107
397
|
const verify = async () => {
|
|
108
398
|
const verifyRequest = {
|
|
109
|
-
x402Version: 1,
|
|
110
|
-
paymentHeader,
|
|
111
399
|
paymentPayload,
|
|
112
400
|
paymentRequirements,
|
|
113
401
|
};
|
|
@@ -121,37 +409,57 @@ export async function handleMiddlewareRequest(args) {
|
|
|
121
409
|
});
|
|
122
410
|
const verifyResponse = x402VerifyResponse(await t.json());
|
|
123
411
|
if (isValidationError(verifyResponse)) {
|
|
124
|
-
const msg = `error getting response from facilitator for verification: ${verifyResponse.summary}`;
|
|
412
|
+
const msg = `error getting response from facilitator for v2 verification: ${verifyResponse.summary}`;
|
|
125
413
|
logger.error(msg);
|
|
126
414
|
throw new Error(msg);
|
|
127
415
|
}
|
|
128
416
|
if (!verifyResponse.isValid) {
|
|
129
|
-
logger.warning("failed to verify payment: {invalidReason}", verifyResponse);
|
|
417
|
+
logger.warning("failed to verify v2 payment: {invalidReason}", verifyResponse);
|
|
130
418
|
return { success: false, errorResponse: sendPaymentRequired() };
|
|
131
419
|
}
|
|
132
420
|
return { success: true, facilitatorResponse: verifyResponse };
|
|
133
421
|
};
|
|
134
422
|
return await args.body({
|
|
423
|
+
protocolVersion: 2,
|
|
135
424
|
paymentRequirements,
|
|
136
425
|
paymentPayload,
|
|
137
426
|
settle,
|
|
138
427
|
verify,
|
|
139
428
|
});
|
|
140
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* Creates a cached wrapper around payment requirements fetching functions.
|
|
432
|
+
*
|
|
433
|
+
* The cache reduces load on the facilitator by reusing recent responses
|
|
434
|
+
* for identical requirements.
|
|
435
|
+
*
|
|
436
|
+
* @param opts - Cache configuration options
|
|
437
|
+
* @returns Object containing cached getPaymentRequiredResponse functions
|
|
438
|
+
*/
|
|
141
439
|
export function createPaymentRequiredResponseCache(opts = {}) {
|
|
142
440
|
if (opts.disable) {
|
|
143
441
|
logger.warning("payment required response cache disabled");
|
|
144
442
|
return {
|
|
145
443
|
getPaymentRequiredResponse,
|
|
444
|
+
getPaymentRequiredResponseV2,
|
|
146
445
|
};
|
|
147
446
|
}
|
|
148
|
-
const
|
|
447
|
+
const v1Cache = new AgedLRUCache(opts);
|
|
448
|
+
const v2Cache = new AgedLRUCache(opts);
|
|
149
449
|
return {
|
|
150
450
|
getPaymentRequiredResponse: async (args) => {
|
|
151
|
-
let response =
|
|
451
|
+
let response = v1Cache.get(args.accepts);
|
|
152
452
|
if (response === undefined) {
|
|
153
453
|
response = await getPaymentRequiredResponse(args);
|
|
154
|
-
|
|
454
|
+
v1Cache.put(args.accepts, response);
|
|
455
|
+
}
|
|
456
|
+
return response;
|
|
457
|
+
},
|
|
458
|
+
getPaymentRequiredResponseV2: async (args) => {
|
|
459
|
+
let response = v2Cache.get(args.accepts);
|
|
460
|
+
if (response === undefined) {
|
|
461
|
+
response = await getPaymentRequiredResponseV2(args);
|
|
462
|
+
v2Cache.put(args.accepts, response);
|
|
155
463
|
}
|
|
156
464
|
return response;
|
|
157
465
|
},
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"common.test.d.ts","sourceRoot":"","sources":["../../src/common.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env pnpm tsx
|
|
2
|
+
import t from "tap";
|
|
3
|
+
import { getPaymentRequiredResponse } from "./common.js";
|
|
4
|
+
const validAccepts = [
|
|
5
|
+
{
|
|
6
|
+
scheme: "exact",
|
|
7
|
+
network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
|
|
8
|
+
maxAmountRequired: "1000",
|
|
9
|
+
resource: "https://example.com/resource",
|
|
10
|
+
description: "Test resource",
|
|
11
|
+
payTo: "test-receiver",
|
|
12
|
+
maxTimeoutSeconds: 60,
|
|
13
|
+
asset: "test-token",
|
|
14
|
+
},
|
|
15
|
+
];
|
|
16
|
+
await t.test("getPaymentRequiredResponse accepts response without error field", async (t) => {
|
|
17
|
+
const mockFetch = async () => new Response(JSON.stringify({
|
|
18
|
+
x402Version: 1,
|
|
19
|
+
accepts: validAccepts,
|
|
20
|
+
}), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
21
|
+
const response = await getPaymentRequiredResponse({
|
|
22
|
+
facilitatorURL: "https://facilitator.example.com",
|
|
23
|
+
accepts: [
|
|
24
|
+
{ scheme: "exact", network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1" },
|
|
25
|
+
],
|
|
26
|
+
resource: "https://example.com/resource",
|
|
27
|
+
fetch: mockFetch,
|
|
28
|
+
});
|
|
29
|
+
t.equal(response.x402Version, 1);
|
|
30
|
+
t.equal(response.accepts.length, 1);
|
|
31
|
+
t.equal(response.error, "");
|
|
32
|
+
t.end();
|
|
33
|
+
});
|
|
34
|
+
await t.test("getPaymentRequiredResponse preserves error field when present", async (t) => {
|
|
35
|
+
const mockFetch = async () => new Response(JSON.stringify({
|
|
36
|
+
x402Version: 1,
|
|
37
|
+
accepts: validAccepts,
|
|
38
|
+
error: "some error",
|
|
39
|
+
}), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
40
|
+
const response = await getPaymentRequiredResponse({
|
|
41
|
+
facilitatorURL: "https://facilitator.example.com",
|
|
42
|
+
accepts: [
|
|
43
|
+
{ scheme: "exact", network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1" },
|
|
44
|
+
],
|
|
45
|
+
resource: "https://example.com/resource",
|
|
46
|
+
fetch: mockFetch,
|
|
47
|
+
});
|
|
48
|
+
t.equal(response.error, "some error");
|
|
49
|
+
t.end();
|
|
50
|
+
});
|
package/dist/src/express.d.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { type CommonMiddlewareArgs } from "./common.js";
|
|
2
2
|
import type { NextFunction, Request, Response } from "express";
|
|
3
3
|
type createMiddlewareArgs = CommonMiddlewareArgs;
|
|
4
|
+
/**
|
|
5
|
+
* Creates Express middleware that gates routes behind x402 payment.
|
|
6
|
+
*
|
|
7
|
+
* The middleware intercepts requests, checks for payment headers, communicates
|
|
8
|
+
* with the facilitator to validate and settle payments, and only allows the
|
|
9
|
+
* request to proceed if payment is successful.
|
|
10
|
+
*
|
|
11
|
+
* @param args - Configuration including facilitator URL and accepted payment types
|
|
12
|
+
* @returns An Express middleware function
|
|
13
|
+
*/
|
|
4
14
|
export declare function createMiddleware(args: createMiddlewareArgs): Promise<(req: Request, res: Response, next: NextFunction) => Promise<Response<any, Record<string, any>> | undefined>>;
|
|
5
15
|
export {};
|
|
6
16
|
//# sourceMappingURL=express.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../../src/express.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,oBAAoB,
|
|
1
|
+
{"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../../src/express.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,oBAAoB,EAG1B,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAE/D,KAAK,oBAAoB,GAAG,oBAAoB,CAAC;AAEjD;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,oBAAoB,iBAO5C,OAAO,OAAO,QAAQ,QAAQ,YAAY,8DA+B9D"}
|
package/dist/src/express.js
CHANGED
|
@@ -1,13 +1,39 @@
|
|
|
1
|
-
import { handleMiddlewareRequest, createPaymentRequiredResponseCache, } from "./common.js";
|
|
1
|
+
import { handleMiddlewareRequest, createPaymentRequiredResponseCache, resolveSupportedVersions, } from "./common.js";
|
|
2
|
+
/**
|
|
3
|
+
* Creates Express middleware that gates routes behind x402 payment.
|
|
4
|
+
*
|
|
5
|
+
* The middleware intercepts requests, checks for payment headers, communicates
|
|
6
|
+
* with the facilitator to validate and settle payments, and only allows the
|
|
7
|
+
* request to proceed if payment is successful.
|
|
8
|
+
*
|
|
9
|
+
* @param args - Configuration including facilitator URL and accepted payment types
|
|
10
|
+
* @returns An Express middleware function
|
|
11
|
+
*/
|
|
2
12
|
export async function createMiddleware(args) {
|
|
3
|
-
|
|
13
|
+
// Validate configuration at creation time
|
|
14
|
+
const supportedVersions = resolveSupportedVersions(args.supportedVersions);
|
|
15
|
+
const { getPaymentRequiredResponse, getPaymentRequiredResponseV2 } = createPaymentRequiredResponseCache(args.cacheConfig);
|
|
4
16
|
return async (req, res, next) => {
|
|
5
17
|
return await handleMiddlewareRequest({
|
|
6
18
|
...args,
|
|
19
|
+
supportedVersions,
|
|
7
20
|
resource: `${req.protocol}://${req.headers.host}${req.path}`,
|
|
8
21
|
getPaymentRequiredResponse,
|
|
22
|
+
getPaymentRequiredResponseV2,
|
|
9
23
|
getHeader: (key) => req.header(key),
|
|
10
|
-
|
|
24
|
+
setResponseHeader: (key, value) => res.setHeader(key, value),
|
|
25
|
+
sendJSONResponse: (status, body, headers) => {
|
|
26
|
+
res.status(status);
|
|
27
|
+
if (headers) {
|
|
28
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
29
|
+
res.setHeader(key, value);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (body) {
|
|
33
|
+
return res.json(body);
|
|
34
|
+
}
|
|
35
|
+
return res.end();
|
|
36
|
+
},
|
|
11
37
|
body: async ({ settle }) => {
|
|
12
38
|
const settleResult = await settle();
|
|
13
39
|
if (!settleResult.success) {
|
package/dist/src/hono.d.ts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import { type CommonMiddlewareArgs } from "./common.js";
|
|
2
2
|
import type { MiddlewareHandler } from "hono";
|
|
3
|
+
/**
|
|
4
|
+
* Configuration arguments for creating Hono x402 middleware.
|
|
5
|
+
*/
|
|
3
6
|
type CreateMiddlewareArgs = {
|
|
7
|
+
/** If true, verifies payment before running the handler, then settles after. */
|
|
4
8
|
verifyBeforeSettle?: boolean;
|
|
5
9
|
} & CommonMiddlewareArgs;
|
|
10
|
+
/**
|
|
11
|
+
* Creates Hono middleware that gates routes behind x402 payment.
|
|
12
|
+
*
|
|
13
|
+
* The middleware intercepts requests, checks for payment headers, communicates
|
|
14
|
+
* with the facilitator to validate and settle payments, and only allows the
|
|
15
|
+
* request to proceed if payment is successful.
|
|
16
|
+
*
|
|
17
|
+
* @param args - Configuration including facilitator URL and accepted payment types
|
|
18
|
+
* @returns A Hono middleware handler
|
|
19
|
+
*/
|
|
6
20
|
export declare function createMiddleware(args: CreateMiddlewareArgs): Promise<MiddlewareHandler>;
|
|
7
21
|
export {};
|
|
8
22
|
//# sourceMappingURL=hono.d.ts.map
|
package/dist/src/hono.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hono.d.ts","sourceRoot":"","sources":["../../src/hono.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,oBAAoB,
|
|
1
|
+
{"version":3,"file":"hono.d.ts","sourceRoot":"","sources":["../../src/hono.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,oBAAoB,EAG1B,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAE9C;;GAEG;AACH,KAAK,oBAAoB,GAAG;IAC1B,gFAAgF;IAChF,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,GAAG,oBAAoB,CAAC;AAEzB;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,oBAAoB,GACzB,OAAO,CAAC,iBAAiB,CAAC,CAiE5B"}
|