@x402/extensions 2.7.0 → 2.8.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/cjs/index.js CHANGED
@@ -124,6 +124,7 @@ __export(src_exports, {
124
124
  isQueryExtensionConfig: () => isQueryExtensionConfig,
125
125
  isSolanaSigner: () => isSolanaSigner,
126
126
  isValidPaymentId: () => isValidPaymentId,
127
+ isValidRouteTemplate: () => isValidRouteTemplate,
127
128
  parseSIWxHeader: () => parseSIWxHeader,
128
129
  paymentIdentifierResourceServerExtension: () => paymentIdentifierResourceServerExtension,
129
130
  paymentIdentifierSchema: () => paymentIdentifierSchema,
@@ -140,6 +141,7 @@ __export(src_exports, {
140
141
  validateErc20ApprovalGasSponsoringInfo: () => validateErc20ApprovalGasSponsoringInfo,
141
142
  validatePaymentIdentifier: () => validatePaymentIdentifier,
142
143
  validatePaymentIdentifierRequirement: () => validatePaymentIdentifierRequirement,
144
+ validateRouteTemplate: () => validateRouteTemplate,
143
145
  validateSIWxMessage: () => validateSIWxMessage,
144
146
  verifyEVMSignature: () => verifyEVMSignature,
145
147
  verifyOfferSignatureEIP712: () => verifyOfferSignatureEIP712,
@@ -175,6 +177,8 @@ function createQueryDiscoveryExtension({
175
177
  method,
176
178
  input = {},
177
179
  inputSchema = { properties: {} },
180
+ pathParams,
181
+ pathParamsSchema,
178
182
  output
179
183
  }) {
180
184
  return {
@@ -182,7 +186,8 @@ function createQueryDiscoveryExtension({
182
186
  input: {
183
187
  type: "http",
184
188
  ...method ? { method } : {},
185
- ...input ? { queryParams: input } : {}
189
+ ...input ? { queryParams: input } : {},
190
+ ...pathParams ? { pathParams } : {}
186
191
  },
187
192
  ...output?.example ? {
188
193
  output: {
@@ -211,9 +216,18 @@ function createQueryDiscoveryExtension({
211
216
  type: "object",
212
217
  ...typeof inputSchema === "object" ? inputSchema : {}
213
218
  }
219
+ } : {},
220
+ ...pathParamsSchema ? {
221
+ pathParams: {
222
+ type: "object",
223
+ ...typeof pathParamsSchema === "object" ? pathParamsSchema : {}
224
+ }
214
225
  } : {}
215
226
  },
216
227
  required: ["type"],
228
+ // pathParams and method are not declared here at schema build time --
229
+ // the server extension's enrichDeclaration adds them to both info and schema
230
+ // atomically at request time, keeping data and schema consistent.
217
231
  additionalProperties: false
218
232
  },
219
233
  ...output?.example ? {
@@ -240,6 +254,8 @@ function createBodyDiscoveryExtension({
240
254
  method,
241
255
  input = {},
242
256
  inputSchema = { properties: {} },
257
+ pathParams,
258
+ pathParamsSchema,
243
259
  bodyType,
244
260
  output
245
261
  }) {
@@ -249,7 +265,8 @@ function createBodyDiscoveryExtension({
249
265
  type: "http",
250
266
  ...method ? { method } : {},
251
267
  bodyType,
252
- body: input
268
+ body: input,
269
+ ...pathParams ? { pathParams } : {}
253
270
  },
254
271
  ...output?.example ? {
255
272
  output: {
@@ -277,9 +294,18 @@ function createBodyDiscoveryExtension({
277
294
  type: "string",
278
295
  enum: ["json", "form-data", "text"]
279
296
  },
280
- body: inputSchema
297
+ body: inputSchema,
298
+ ...pathParamsSchema ? {
299
+ pathParams: {
300
+ type: "object",
301
+ ...typeof pathParamsSchema === "object" ? pathParamsSchema : {}
302
+ }
303
+ } : {}
281
304
  },
282
305
  required: ["type", "bodyType", "body"],
306
+ // pathParams and method are not declared here at schema build time --
307
+ // the server extension's enrichDeclaration adds them to both info and schema
308
+ // atomically at request time, keeping data and schema consistent.
283
309
  additionalProperties: false
284
310
  },
285
311
  ...output?.example ? {
@@ -400,9 +426,57 @@ function declareDiscoveryExtension(config) {
400
426
  }
401
427
 
402
428
  // src/bazaar/server.ts
429
+ var BRACKET_PARAM_REGEX = /\[([^\]]+)\]/;
430
+ var BRACKET_PARAM_REGEX_ALL = /\[([^\]]+)\]/g;
431
+ var COLON_PARAM_REGEX = /:([a-zA-Z_][a-zA-Z0-9_]*)/;
403
432
  function isHTTPRequestContext(ctx) {
404
433
  return ctx !== null && typeof ctx === "object" && "method" in ctx && "adapter" in ctx;
405
434
  }
435
+ function normalizeWildcardPattern(pattern) {
436
+ if (!pattern.includes("*")) {
437
+ return pattern;
438
+ }
439
+ let counter = 0;
440
+ return pattern.split("/").map((seg) => {
441
+ if (seg === "*") {
442
+ counter++;
443
+ return `:var${counter}`;
444
+ }
445
+ return seg;
446
+ }).join("/");
447
+ }
448
+ function extractDynamicRouteInfo(routePattern, urlPath) {
449
+ const hasBracket = BRACKET_PARAM_REGEX.test(routePattern);
450
+ const hasColon = COLON_PARAM_REGEX.test(routePattern);
451
+ if (!hasBracket && !hasColon) {
452
+ return null;
453
+ }
454
+ const normalizedPattern = hasBracket ? routePattern.replace(BRACKET_PARAM_REGEX_ALL, ":$1") : routePattern;
455
+ const pathParams = extractPathParams(normalizedPattern, urlPath, false);
456
+ return { routeTemplate: normalizedPattern, pathParams };
457
+ }
458
+ function extractPathParams(routePattern, urlPath, isBracket) {
459
+ const paramNames = [];
460
+ const splitRegex = isBracket ? BRACKET_PARAM_REGEX : COLON_PARAM_REGEX;
461
+ const parts = routePattern.split(splitRegex);
462
+ const regexParts = [];
463
+ parts.forEach((part, i) => {
464
+ if (i % 2 === 0) {
465
+ regexParts.push(part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
466
+ } else {
467
+ paramNames.push(part);
468
+ regexParts.push("([^/]+)");
469
+ }
470
+ });
471
+ const regex = new RegExp(`^${regexParts.join("")}$`);
472
+ const match = urlPath.match(regex);
473
+ if (!match) return {};
474
+ const result = {};
475
+ paramNames.forEach((name, idx) => {
476
+ result[name] = match[idx + 1];
477
+ });
478
+ return result;
479
+ }
406
480
  var bazaarResourceServerExtension = {
407
481
  key: BAZAAR.key,
408
482
  enrichDeclaration: (declaration, transportContext) => {
@@ -422,7 +496,7 @@ var bazaarResourceServerExtension = {
422
496
  enum: [method]
423
497
  }
424
498
  };
425
- return {
499
+ const enrichedResult = {
426
500
  ...extension,
427
501
  info: {
428
502
  ...extension.info || {},
@@ -446,6 +520,37 @@ var bazaarResourceServerExtension = {
446
520
  }
447
521
  }
448
522
  };
523
+ const rawRoutePattern = transportContext.routePattern;
524
+ const routePattern = rawRoutePattern ? normalizeWildcardPattern(rawRoutePattern) : void 0;
525
+ const dynamicRoute = routePattern ? extractDynamicRouteInfo(routePattern, transportContext.adapter.getPath()) : null;
526
+ if (dynamicRoute) {
527
+ const inputSchemaProps = enrichedResult.schema?.properties?.input?.properties || {};
528
+ const hasPathParamsInSchema = "pathParams" in inputSchemaProps;
529
+ return {
530
+ ...enrichedResult,
531
+ routeTemplate: dynamicRoute.routeTemplate,
532
+ info: {
533
+ ...enrichedResult.info,
534
+ input: { ...enrichedResult.info.input, pathParams: dynamicRoute.pathParams }
535
+ },
536
+ ...!hasPathParamsInSchema ? {
537
+ schema: {
538
+ ...enrichedResult.schema,
539
+ properties: {
540
+ ...enrichedResult.schema?.properties,
541
+ input: {
542
+ ...enrichedResult.schema?.properties?.input,
543
+ properties: {
544
+ ...inputSchemaProps,
545
+ pathParams: { type: "object" }
546
+ }
547
+ }
548
+ }
549
+ }
550
+ } : {}
551
+ };
552
+ }
553
+ return enrichedResult;
449
554
  }
450
555
  };
451
556
 
@@ -567,6 +672,23 @@ function extractResourceMetadataV1(paymentRequirements) {
567
672
  }
568
673
 
569
674
  // src/bazaar/facilitator.ts
675
+ var ROUTE_TEMPLATE_REGEX = /^\/[a-zA-Z0-9_/:.\-~%]+$/;
676
+ function isValidRouteTemplate(value) {
677
+ if (!value) return false;
678
+ if (!ROUTE_TEMPLATE_REGEX.test(value)) return false;
679
+ let decoded;
680
+ try {
681
+ decoded = decodeURIComponent(value);
682
+ } catch {
683
+ return false;
684
+ }
685
+ if (decoded.includes("..")) return false;
686
+ if (decoded.includes("://")) return false;
687
+ return true;
688
+ }
689
+ function validateRouteTemplate(value) {
690
+ return isValidRouteTemplate(value) ? value : void 0;
691
+ }
570
692
  function validateDiscoveryExtension(extension) {
571
693
  try {
572
694
  const ajv = new import__.default({ strict: false, allErrors: true });
@@ -592,12 +714,18 @@ function validateDiscoveryExtension(extension) {
592
714
  function extractDiscoveryInfo(paymentPayload, paymentRequirements, validate = true) {
593
715
  let discoveryInfo = null;
594
716
  let resourceUrl;
717
+ let routeTemplate;
595
718
  if (paymentPayload.x402Version === 2) {
596
719
  resourceUrl = paymentPayload.resource?.url ?? "";
597
720
  if (paymentPayload.extensions) {
598
721
  const bazaarExtension = paymentPayload.extensions[BAZAAR.key];
599
722
  if (bazaarExtension && typeof bazaarExtension === "object") {
600
723
  try {
724
+ const rawExt = bazaarExtension;
725
+ const rawTemplate = typeof rawExt.routeTemplate === "string" ? rawExt.routeTemplate : void 0;
726
+ if (isValidRouteTemplate(rawTemplate)) {
727
+ routeTemplate = rawTemplate;
728
+ }
601
729
  const extension = bazaarExtension;
602
730
  if (validate) {
603
731
  const result = validateDiscoveryExtension(extension);
@@ -627,7 +755,7 @@ function extractDiscoveryInfo(paymentPayload, paymentRequirements, validate = tr
627
755
  return null;
628
756
  }
629
757
  const url = new URL(resourceUrl);
630
- const normalizedResourceUrl = `${url.origin}${url.pathname}`;
758
+ const canonicalUrl = routeTemplate ? `${url.origin}${routeTemplate}` : `${url.origin}${url.pathname}`;
631
759
  let description;
632
760
  let mimeType;
633
761
  if (paymentPayload.x402Version === 2) {
@@ -639,7 +767,7 @@ function extractDiscoveryInfo(paymentPayload, paymentRequirements, validate = tr
639
767
  mimeType = requirementsV1.mimeType;
640
768
  }
641
769
  const base = {
642
- resourceUrl: normalizedResourceUrl,
770
+ resourceUrl: canonicalUrl,
643
771
  description,
644
772
  mimeType,
645
773
  x402Version: paymentPayload.x402Version,
@@ -648,7 +776,7 @@ function extractDiscoveryInfo(paymentPayload, paymentRequirements, validate = tr
648
776
  if (discoveryInfo.input.type === "mcp") {
649
777
  return { ...base, toolName: discoveryInfo.input.toolName };
650
778
  }
651
- return { ...base, method: discoveryInfo.input.method };
779
+ return { ...base, routeTemplate, method: discoveryInfo.input.method };
652
780
  }
653
781
  function extractDiscoveryInfoFromExtension(extension, validate = true) {
654
782
  if (validate) {
@@ -1386,7 +1514,7 @@ function createSIWxRequestHook(options) {
1386
1514
  "SIWxStorage nonce tracking requires both hasUsedNonce and recordNonce to be implemented"
1387
1515
  );
1388
1516
  }
1389
- return async (context) => {
1517
+ return async (context, routeConfig) => {
1390
1518
  const header = context.adapter.getHeader(SIGN_IN_WITH_X) || context.adapter.getHeader(SIGN_IN_WITH_X.toLowerCase());
1391
1519
  if (!header) return;
1392
1520
  try {
@@ -1409,8 +1537,9 @@ function createSIWxRequestHook(options) {
1409
1537
  return;
1410
1538
  }
1411
1539
  }
1412
- const hasPaid = await storage.hasPaid(context.path, verification.address);
1413
- if (hasPaid) {
1540
+ const isAuthOnly = Array.isArray(routeConfig?.accepts) && routeConfig.accepts.length === 0;
1541
+ const shouldGrant = isAuthOnly || await storage.hasPaid(context.path, verification.address);
1542
+ if (shouldGrant) {
1414
1543
  if (storage.recordNonce) {
1415
1544
  await storage.recordNonce(payload.nonce);
1416
1545
  }
@@ -2713,6 +2842,7 @@ function validateErc20ApprovalGasSponsoringInfo(info) {
2713
2842
  isQueryExtensionConfig,
2714
2843
  isSolanaSigner,
2715
2844
  isValidPaymentId,
2845
+ isValidRouteTemplate,
2716
2846
  parseSIWxHeader,
2717
2847
  paymentIdentifierResourceServerExtension,
2718
2848
  paymentIdentifierSchema,
@@ -2729,6 +2859,7 @@ function validateErc20ApprovalGasSponsoringInfo(info) {
2729
2859
  validateErc20ApprovalGasSponsoringInfo,
2730
2860
  validatePaymentIdentifier,
2731
2861
  validatePaymentIdentifierRequirement,
2862
+ validateRouteTemplate,
2732
2863
  validateSIWxMessage,
2733
2864
  verifyEVMSignature,
2734
2865
  verifyOfferSignatureEIP712,