@faremeter/middleware 0.15.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.
@@ -1,26 +1,69 @@
1
1
  import { isValidationError } from "@faremeter/types";
2
- import { x402PaymentRequiredResponse, x402PaymentHeaderToPayload, x402VerifyRequest, x402VerifyResponse, x402SettleRequest, x402SettleResponse, } from "@faremeter/types/x402";
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
- export function findMatchingPaymentRequirements(accepts, payload) {
6
- let possible;
7
- if (payload.asset !== undefined) {
8
- // Narrow based on the asset if available.
9
- possible = accepts.filter((x) => x.network === payload.network &&
10
- x.scheme === payload.scheme &&
11
- x.asset === payload.asset);
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
- else {
14
- // Otherwise fall back to the behavior in coinbase/x402.
15
- possible = accepts.filter((x) => x.network === payload.network && x.scheme === payload.scheme);
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,12 +72,61 @@ 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) {
124
+ const fetchFn = args.fetch ?? fetch;
33
125
  const accepts = args.accepts.map((x) => ({
34
126
  ...x,
35
127
  resource: x.resource ?? args.resource,
36
128
  }));
37
- const t = await fetch(`${args.facilitatorURL}/accepts`, {
129
+ const t = await fetchFn(`${args.facilitatorURL}/accepts`, {
38
130
  method: "POST",
39
131
  headers: {
40
132
  Accept: "application/json",
@@ -43,45 +135,243 @@ export async function getPaymentRequiredResponse(args) {
43
135
  body: JSON.stringify({
44
136
  x402Version: 1,
45
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,
46
166
  }),
47
167
  });
48
168
  gateGetPaymentRequiredResponse(t);
49
169
  const response = x402PaymentRequiredResponse(await t.json());
50
170
  if (isValidationError(response)) {
51
- throw new Error(`invalid payment requirements from facilitator: ${response.summary}`);
171
+ throw new Error(`invalid v2 payment requirements from facilitator: ${response.summary}`);
52
172
  }
53
173
  return response;
54
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
+ */
55
200
  export async function handleMiddlewareRequest(args) {
56
201
  const accepts = args.accepts.flat();
57
- const paymentRequiredResponse = await args.getPaymentRequiredResponse({
58
- accepts,
59
- facilitatorURL: args.facilitatorURL,
60
- resource: args.resource,
61
- });
62
- const sendPaymentRequired = () => args.sendJSONResponse(402, paymentRequiredResponse);
63
- const paymentHeader = args.getHeader("X-PAYMENT");
64
- if (!paymentHeader) {
65
- return sendPaymentRequired();
202
+ const fetchFn = args.fetch ?? fetch;
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
+ }
66
227
  }
67
- const paymentPayload = x402PaymentHeaderToPayload(paymentHeader);
68
- if (isValidationError(paymentPayload)) {
69
- logger.debug(`couldn't validate client payload: ${paymentPayload.summary}`);
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) {
70
256
  return sendPaymentRequired();
71
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) {
72
283
  const paymentRequirements = findMatchingPaymentRequirements(paymentRequiredResponse.accepts, paymentPayload);
73
284
  if (!paymentRequirements) {
74
- logger.warning(`couldn't find matching payment requirements for payload`, paymentPayload);
285
+ logger.warning(`couldn't find matching payment requirements for v1 payload`, paymentPayload);
75
286
  return sendPaymentRequired();
76
287
  }
77
288
  const settle = async () => {
78
289
  const settleRequest = {
79
- x402Version: 1,
80
290
  paymentHeader,
81
291
  paymentPayload,
82
292
  paymentRequirements,
83
293
  };
84
- const t = await fetch(`${args.facilitatorURL}/settle`, {
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
+ };
374
+ const t = await fetchFn(`${args.facilitatorURL}/settle`, {
85
375
  method: "POST",
86
376
  headers: {
87
377
  Accept: "application/json",
@@ -91,23 +381,25 @@ export async function handleMiddlewareRequest(args) {
91
381
  });
92
382
  const settlementResponse = x402SettleResponse(await t.json());
93
383
  if (isValidationError(settlementResponse)) {
94
- const msg = `error getting response from facilitator for settlement: ${settlementResponse.summary}`;
384
+ const msg = `error getting response from facilitator for v2 settlement: ${settlementResponse.summary}`;
95
385
  logger.error(msg);
96
386
  throw new Error(msg);
97
387
  }
388
+ if (args.setResponseHeader) {
389
+ args.setResponseHeader(V2_PAYMENT_RESPONSE_HEADER, btoa(JSON.stringify(settlementResponse)));
390
+ }
98
391
  if (!settlementResponse.success) {
99
- logger.warning("failed to settle payment: {error}", settlementResponse);
100
- return sendPaymentRequired();
392
+ logger.warning("failed to settle v2 payment: {errorReason}", settlementResponse);
393
+ return { success: false, errorResponse: sendPaymentRequired() };
101
394
  }
395
+ return { success: true, facilitatorResponse: settlementResponse };
102
396
  };
103
397
  const verify = async () => {
104
398
  const verifyRequest = {
105
- x402Version: 1,
106
- paymentHeader,
107
399
  paymentPayload,
108
400
  paymentRequirements,
109
401
  };
110
- const t = await fetch(`${args.facilitatorURL}/verify`, {
402
+ const t = await fetchFn(`${args.facilitatorURL}/verify`, {
111
403
  method: "POST",
112
404
  headers: {
113
405
  Accept: "application/json",
@@ -117,36 +409,57 @@ export async function handleMiddlewareRequest(args) {
117
409
  });
118
410
  const verifyResponse = x402VerifyResponse(await t.json());
119
411
  if (isValidationError(verifyResponse)) {
120
- const msg = `error getting response from facilitator for verification: ${verifyResponse.summary}`;
412
+ const msg = `error getting response from facilitator for v2 verification: ${verifyResponse.summary}`;
121
413
  logger.error(msg);
122
414
  throw new Error(msg);
123
415
  }
124
416
  if (!verifyResponse.isValid) {
125
- logger.warning("failed to settle payment: {invalidReason}", verifyResponse);
126
- return sendPaymentRequired();
417
+ logger.warning("failed to verify v2 payment: {invalidReason}", verifyResponse);
418
+ return { success: false, errorResponse: sendPaymentRequired() };
127
419
  }
420
+ return { success: true, facilitatorResponse: verifyResponse };
128
421
  };
129
422
  return await args.body({
423
+ protocolVersion: 2,
130
424
  paymentRequirements,
131
425
  paymentPayload,
132
426
  settle,
133
427
  verify,
134
428
  });
135
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
+ */
136
439
  export function createPaymentRequiredResponseCache(opts = {}) {
137
440
  if (opts.disable) {
138
441
  logger.warning("payment required response cache disabled");
139
442
  return {
140
443
  getPaymentRequiredResponse,
444
+ getPaymentRequiredResponseV2,
141
445
  };
142
446
  }
143
- const cache = new AgedLRUCache(opts);
447
+ const v1Cache = new AgedLRUCache(opts);
448
+ const v2Cache = new AgedLRUCache(opts);
144
449
  return {
145
450
  getPaymentRequiredResponse: async (args) => {
146
- let response = cache.get(args.accepts);
451
+ let response = v1Cache.get(args.accepts);
147
452
  if (response === undefined) {
148
453
  response = await getPaymentRequiredResponse(args);
149
- cache.put(args.accepts, response);
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);
150
463
  }
151
464
  return response;
152
465
  },
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env pnpm tsx
2
+ export {};
3
+ //# sourceMappingURL=common.test.d.ts.map
@@ -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
+ });
@@ -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,EAE1B,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAE/D,KAAK,oBAAoB,GAAG,oBAAoB,CAAC;AAEjD,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,oBAAoB,iBAK5C,OAAO,OAAO,QAAQ,QAAQ,YAAY,8DAiB9D"}
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"}
@@ -1,17 +1,43 @@
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
- const { getPaymentRequiredResponse } = createPaymentRequiredResponseCache(args.cacheConfig);
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
- sendJSONResponse: (status, body) => res.status(status).json(body),
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
- const response = await settle();
13
- if (response !== undefined) {
14
- return response;
38
+ const settleResult = await settle();
39
+ if (!settleResult.success) {
40
+ return settleResult.errorResponse;
15
41
  }
16
42
  next();
17
43
  },
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"hono.d.ts","sourceRoot":"","sources":["../../src/hono.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,oBAAoB,EAE1B,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAE9C,KAAK,oBAAoB,GAAG;IAC1B,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,GAAG,oBAAoB,CAAC;AAEzB,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,oBAAoB,GACzB,OAAO,CAAC,iBAAiB,CAAC,CAoD5B"}
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"}