@x402/extensions 2.6.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.
Files changed (34) hide show
  1. package/README.md +27 -22
  2. package/dist/cjs/bazaar/index.d.ts +1 -1
  3. package/dist/cjs/bazaar/index.js +137 -7
  4. package/dist/cjs/bazaar/index.js.map +1 -1
  5. package/dist/cjs/{index-CtOzXcjN.d.ts → index-DihVrF7v.d.ts} +49 -9
  6. package/dist/cjs/index.d.ts +33 -23
  7. package/dist/cjs/index.js +1015 -20
  8. package/dist/cjs/index.js.map +1 -1
  9. package/dist/cjs/offer-receipt/index.d.ts +702 -0
  10. package/dist/cjs/offer-receipt/index.js +909 -0
  11. package/dist/cjs/offer-receipt/index.js.map +1 -0
  12. package/dist/cjs/sign-in-with-x/index.d.ts +7 -1
  13. package/dist/cjs/sign-in-with-x/index.js +5 -5
  14. package/dist/cjs/sign-in-with-x/index.js.map +1 -1
  15. package/dist/esm/bazaar/index.d.mts +1 -1
  16. package/dist/esm/bazaar/index.mjs +5 -1
  17. package/dist/esm/{chunk-O34SGKEP.mjs → chunk-5UYCX7A7.mjs} +6 -6
  18. package/dist/esm/chunk-5UYCX7A7.mjs.map +1 -0
  19. package/dist/esm/{chunk-ANAQVNUK.mjs → chunk-I2JLOI4L.mjs} +136 -8
  20. package/dist/esm/chunk-I2JLOI4L.mjs.map +1 -0
  21. package/dist/esm/chunk-TYR4QHVX.mjs +828 -0
  22. package/dist/esm/chunk-TYR4QHVX.mjs.map +1 -0
  23. package/dist/esm/{index-CtOzXcjN.d.mts → index-DihVrF7v.d.mts} +49 -9
  24. package/dist/esm/index.d.mts +33 -23
  25. package/dist/esm/index.mjs +102 -10
  26. package/dist/esm/index.mjs.map +1 -1
  27. package/dist/esm/offer-receipt/index.d.mts +702 -0
  28. package/dist/esm/offer-receipt/index.mjs +97 -0
  29. package/dist/esm/offer-receipt/index.mjs.map +1 -0
  30. package/dist/esm/sign-in-with-x/index.d.mts +7 -1
  31. package/dist/esm/sign-in-with-x/index.mjs +1 -1
  32. package/package.json +14 -2
  33. package/dist/esm/chunk-ANAQVNUK.mjs.map +0 -1
  34. package/dist/esm/chunk-O34SGKEP.mjs.map +0 -1
package/README.md CHANGED
@@ -471,7 +471,7 @@ The Sign-In-With-X extension implements [CAIP-122](https://chainagnostic.org/CAI
471
471
  1. Server returns 402 with `sign-in-with-x` extension containing challenge parameters
472
472
  2. Client signs the CAIP-122 message with their wallet
473
473
  3. Client sends signed proof in `SIGN-IN-WITH-X` header
474
- 4. Server verifies signature and grants access if wallet has previous payment
474
+ 4. Server verifies signature and grants access either because the route is auth-only or because the wallet has previously paid
475
475
 
476
476
  ### Server Usage
477
477
 
@@ -495,7 +495,7 @@ const resourceServer = new x402ResourceServer(facilitatorClient)
495
495
  .registerExtension(siwxResourceServerExtension) // Refreshes nonce/timestamps per request
496
496
  .onAfterSettle(createSIWxSettleHook({ storage })); // Records payments
497
497
 
498
- // 2. Declare SIWX support in routes (network/domain/uri derived automatically)
498
+ // 2. Declare SIWX support in routes
499
499
  const routes = {
500
500
  "GET /data": {
501
501
  accepts: [{scheme: "exact", price: "$0.01", network: "eip155:8453", payTo}],
@@ -503,11 +503,19 @@ const routes = {
503
503
  statement: 'Sign in to access your purchased content',
504
504
  }),
505
505
  },
506
+ "GET /profile": {
507
+ accepts: [],
508
+ extensions: declareSIWxExtension({
509
+ network: ["eip155:8453", "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"],
510
+ statement: 'Sign in to view your profile',
511
+ expirationSeconds: 300,
512
+ }),
513
+ },
506
514
  };
507
515
 
508
516
  // 3. Verify incoming SIWX proofs
509
517
  const httpServer = new x402HTTPResourceServer(resourceServer, routes)
510
- .onProtectedRequest(createSIWxRequestHook({ storage })); // Grants access if paid
518
+ .onProtectedRequest(createSIWxRequestHook({ storage })); // Grants access when SIWX auth is sufficient
511
519
 
512
520
  // Optional: Enable smart wallet support (EIP-1271/EIP-6492)
513
521
  import { createPublicClient, http } from 'viem';
@@ -524,7 +532,7 @@ const httpServerWithSmartWallets = new x402HTTPResourceServer(resourceServer, ro
524
532
  The hooks automatically:
525
533
  - **siwxResourceServerExtension**: Derives `network` from `accepts`, `domain`/`uri` from request URL, refreshes `nonce`/`issuedAt`/`expirationTime` per request
526
534
  - **createSIWxSettleHook**: Records payment when settlement succeeds
527
- - **createSIWxRequestHook**: Validates and verifies SIWX proofs, grants access if wallet has paid
535
+ - **createSIWxRequestHook**: Validates and verifies SIWX proofs, grants access for auth-only routes or when the wallet has paid
528
536
 
529
537
  #### Manual Usage (Advanced)
530
538
 
@@ -534,18 +542,15 @@ import {
534
542
  parseSIWxHeader,
535
543
  validateSIWxMessage,
536
544
  verifySIWxSignature,
537
- SIGN_IN_WITH_X,
538
545
  } from '@x402/extensions/sign-in-with-x';
539
546
 
540
547
  // 1. Declare in PaymentRequired response
541
- const extensions = {
542
- [SIGN_IN_WITH_X]: declareSIWxExtension({
543
- domain: 'api.example.com',
544
- resourceUri: 'https://api.example.com/data',
545
- network: 'eip155:8453',
546
- statement: 'Sign in to access your purchased content',
547
- }),
548
- };
548
+ const extensions = declareSIWxExtension({
549
+ domain: 'api.example.com',
550
+ resourceUri: 'https://api.example.com/data',
551
+ network: 'eip155:8453',
552
+ statement: 'Sign in to access your purchased content',
553
+ });
549
554
 
550
555
  // 2. Verify incoming proof
551
556
  async function handleRequest(request: Request) {
@@ -571,10 +576,8 @@ async function handleRequest(request: Request) {
571
576
  }
572
577
 
573
578
  // verification.address is the verified wallet
574
- // Check if this wallet has paid before
575
- const hasPaid = await checkPaymentHistory(verification.address);
576
- if (hasPaid) {
577
- // Grant access without payment
579
+ if (await isAuthOnlyRoute(request) || await checkPaymentHistory(verification.address)) {
580
+ // Grant access
578
581
  }
579
582
  }
580
583
  ```
@@ -612,15 +615,15 @@ import {
612
615
  // 1. Get extension and network from 402 response
613
616
  const paymentRequired = await response.json();
614
617
  const extension = paymentRequired.extensions['sign-in-with-x'];
615
- const paymentNetwork = paymentRequired.accepts[0].network; // e.g., "eip155:8453"
618
+ const paymentNetwork = paymentRequired.accepts[0]?.network; // undefined for auth-only routes
616
619
 
617
620
  // 2. Find matching chain in supportedChains
618
- const matchingChain = extension.supportedChains.find(
619
- chain => chain.chainId === paymentNetwork
620
- );
621
+ const matchingChain = paymentNetwork
622
+ ? extension.supportedChains.find(chain => chain.chainId === paymentNetwork)
623
+ : extension.supportedChains[0];
621
624
 
622
625
  if (!matchingChain) {
623
- // Payment network not supported for SIWX
626
+ // No chain supported by this signer / route combination
624
627
  throw new Error('Chain not supported');
625
628
  }
626
629
 
@@ -664,6 +667,8 @@ declareSIWxExtension({
664
667
  - `resourceUri` → from request URL
665
668
  - `domain` → parsed from resourceUri
666
669
 
670
+ For auth-only routes declared with `accepts: []`, `network` cannot be inferred from payment requirements and should be provided explicitly.
671
+
667
672
  **Multi-chain support:** When `network` is an array (or multiple networks in `accepts`), `supportedChains` will contain one entry per network.
668
673
 
669
674
  #### `parseSIWxHeader(header)`
@@ -1,3 +1,3 @@
1
- export { k as BAZAAR, A as BazaarClientExtension, c as BodyDiscoveryExtension, B as BodyDiscoveryInfo, g as DeclareBodyDiscoveryExtensionConfig, i as DeclareDiscoveryExtensionConfig, j as DeclareDiscoveryExtensionInput, h as DeclareMcpDiscoveryExtensionConfig, f as DeclareQueryDiscoveryExtensionConfig, s as DiscoveredHTTPResource, t as DiscoveredMCPResource, u as DiscoveredResource, e as DiscoveryExtension, D as DiscoveryInfo, C as DiscoveryResource, E as DiscoveryResourcesResponse, L as ListDiscoveryResourcesParams, d as McpDiscoveryExtension, M as McpDiscoveryInfo, a as QueryDiscoveryExtension, Q as QueryDiscoveryInfo, V as ValidationResult, b as bazaarResourceServerExtension, o as declareDiscoveryExtension, p as extractDiscoveryInfo, q as extractDiscoveryInfoFromExtension, w as extractDiscoveryInfoV1, y as extractResourceMetadataV1, n as isBodyExtensionConfig, x as isDiscoverableV1, l as isMcpExtensionConfig, m as isQueryExtensionConfig, r as validateAndExtract, v as validateDiscoveryExtension, z as withBazaar } from '../index-CtOzXcjN.js';
1
+ export { k as BAZAAR, E as BazaarClientExtension, c as BodyDiscoveryExtension, B as BodyDiscoveryInfo, g as DeclareBodyDiscoveryExtensionConfig, i as DeclareDiscoveryExtensionConfig, j as DeclareDiscoveryExtensionInput, h as DeclareMcpDiscoveryExtensionConfig, f as DeclareQueryDiscoveryExtensionConfig, u as DiscoveredHTTPResource, w as DiscoveredMCPResource, x as DiscoveredResource, e as DiscoveryExtension, D as DiscoveryInfo, F as DiscoveryResource, G as DiscoveryResourcesResponse, L as ListDiscoveryResourcesParams, d as McpDiscoveryExtension, M as McpDiscoveryInfo, a as QueryDiscoveryExtension, Q as QueryDiscoveryInfo, V as ValidationResult, b as bazaarResourceServerExtension, o as declareDiscoveryExtension, r as extractDiscoveryInfo, s as extractDiscoveryInfoFromExtension, y as extractDiscoveryInfoV1, A as extractResourceMetadataV1, n as isBodyExtensionConfig, z as isDiscoverableV1, l as isMcpExtensionConfig, m as isQueryExtensionConfig, p as isValidRouteTemplate, t as validateAndExtract, v as validateDiscoveryExtension, q as validateRouteTemplate, C as withBazaar } from '../index-DihVrF7v.js';
2
2
  import '@x402/core/types';
3
3
  import '@x402/core/http';
@@ -41,8 +41,10 @@ __export(bazaar_exports, {
41
41
  isDiscoverableV1: () => isDiscoverableV1,
42
42
  isMcpExtensionConfig: () => isMcpExtensionConfig,
43
43
  isQueryExtensionConfig: () => isQueryExtensionConfig,
44
+ isValidRouteTemplate: () => isValidRouteTemplate,
44
45
  validateAndExtract: () => validateAndExtract,
45
46
  validateDiscoveryExtension: () => validateDiscoveryExtension,
47
+ validateRouteTemplate: () => validateRouteTemplate,
46
48
  withBazaar: () => withBazaar
47
49
  });
48
50
  module.exports = __toCommonJS(bazaar_exports);
@@ -68,6 +70,8 @@ function createQueryDiscoveryExtension({
68
70
  method,
69
71
  input = {},
70
72
  inputSchema = { properties: {} },
73
+ pathParams,
74
+ pathParamsSchema,
71
75
  output
72
76
  }) {
73
77
  return {
@@ -75,7 +79,8 @@ function createQueryDiscoveryExtension({
75
79
  input: {
76
80
  type: "http",
77
81
  ...method ? { method } : {},
78
- ...input ? { queryParams: input } : {}
82
+ ...input ? { queryParams: input } : {},
83
+ ...pathParams ? { pathParams } : {}
79
84
  },
80
85
  ...output?.example ? {
81
86
  output: {
@@ -104,9 +109,18 @@ function createQueryDiscoveryExtension({
104
109
  type: "object",
105
110
  ...typeof inputSchema === "object" ? inputSchema : {}
106
111
  }
112
+ } : {},
113
+ ...pathParamsSchema ? {
114
+ pathParams: {
115
+ type: "object",
116
+ ...typeof pathParamsSchema === "object" ? pathParamsSchema : {}
117
+ }
107
118
  } : {}
108
119
  },
109
120
  required: ["type"],
121
+ // pathParams and method are not declared here at schema build time --
122
+ // the server extension's enrichDeclaration adds them to both info and schema
123
+ // atomically at request time, keeping data and schema consistent.
110
124
  additionalProperties: false
111
125
  },
112
126
  ...output?.example ? {
@@ -133,6 +147,8 @@ function createBodyDiscoveryExtension({
133
147
  method,
134
148
  input = {},
135
149
  inputSchema = { properties: {} },
150
+ pathParams,
151
+ pathParamsSchema,
136
152
  bodyType,
137
153
  output
138
154
  }) {
@@ -142,7 +158,8 @@ function createBodyDiscoveryExtension({
142
158
  type: "http",
143
159
  ...method ? { method } : {},
144
160
  bodyType,
145
- body: input
161
+ body: input,
162
+ ...pathParams ? { pathParams } : {}
146
163
  },
147
164
  ...output?.example ? {
148
165
  output: {
@@ -170,9 +187,18 @@ function createBodyDiscoveryExtension({
170
187
  type: "string",
171
188
  enum: ["json", "form-data", "text"]
172
189
  },
173
- body: inputSchema
190
+ body: inputSchema,
191
+ ...pathParamsSchema ? {
192
+ pathParams: {
193
+ type: "object",
194
+ ...typeof pathParamsSchema === "object" ? pathParamsSchema : {}
195
+ }
196
+ } : {}
174
197
  },
175
198
  required: ["type", "bodyType", "body"],
199
+ // pathParams and method are not declared here at schema build time --
200
+ // the server extension's enrichDeclaration adds them to both info and schema
201
+ // atomically at request time, keeping data and schema consistent.
176
202
  additionalProperties: false
177
203
  },
178
204
  ...output?.example ? {
@@ -293,9 +319,57 @@ function declareDiscoveryExtension(config) {
293
319
  }
294
320
 
295
321
  // src/bazaar/server.ts
322
+ var BRACKET_PARAM_REGEX = /\[([^\]]+)\]/;
323
+ var BRACKET_PARAM_REGEX_ALL = /\[([^\]]+)\]/g;
324
+ var COLON_PARAM_REGEX = /:([a-zA-Z_][a-zA-Z0-9_]*)/;
296
325
  function isHTTPRequestContext(ctx) {
297
326
  return ctx !== null && typeof ctx === "object" && "method" in ctx && "adapter" in ctx;
298
327
  }
328
+ function normalizeWildcardPattern(pattern) {
329
+ if (!pattern.includes("*")) {
330
+ return pattern;
331
+ }
332
+ let counter = 0;
333
+ return pattern.split("/").map((seg) => {
334
+ if (seg === "*") {
335
+ counter++;
336
+ return `:var${counter}`;
337
+ }
338
+ return seg;
339
+ }).join("/");
340
+ }
341
+ function extractDynamicRouteInfo(routePattern, urlPath) {
342
+ const hasBracket = BRACKET_PARAM_REGEX.test(routePattern);
343
+ const hasColon = COLON_PARAM_REGEX.test(routePattern);
344
+ if (!hasBracket && !hasColon) {
345
+ return null;
346
+ }
347
+ const normalizedPattern = hasBracket ? routePattern.replace(BRACKET_PARAM_REGEX_ALL, ":$1") : routePattern;
348
+ const pathParams = extractPathParams(normalizedPattern, urlPath, false);
349
+ return { routeTemplate: normalizedPattern, pathParams };
350
+ }
351
+ function extractPathParams(routePattern, urlPath, isBracket) {
352
+ const paramNames = [];
353
+ const splitRegex = isBracket ? BRACKET_PARAM_REGEX : COLON_PARAM_REGEX;
354
+ const parts = routePattern.split(splitRegex);
355
+ const regexParts = [];
356
+ parts.forEach((part, i) => {
357
+ if (i % 2 === 0) {
358
+ regexParts.push(part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
359
+ } else {
360
+ paramNames.push(part);
361
+ regexParts.push("([^/]+)");
362
+ }
363
+ });
364
+ const regex = new RegExp(`^${regexParts.join("")}$`);
365
+ const match = urlPath.match(regex);
366
+ if (!match) return {};
367
+ const result = {};
368
+ paramNames.forEach((name, idx) => {
369
+ result[name] = match[idx + 1];
370
+ });
371
+ return result;
372
+ }
299
373
  var bazaarResourceServerExtension = {
300
374
  key: BAZAAR.key,
301
375
  enrichDeclaration: (declaration, transportContext) => {
@@ -315,7 +389,7 @@ var bazaarResourceServerExtension = {
315
389
  enum: [method]
316
390
  }
317
391
  };
318
- return {
392
+ const enrichedResult = {
319
393
  ...extension,
320
394
  info: {
321
395
  ...extension.info || {},
@@ -339,6 +413,37 @@ var bazaarResourceServerExtension = {
339
413
  }
340
414
  }
341
415
  };
416
+ const rawRoutePattern = transportContext.routePattern;
417
+ const routePattern = rawRoutePattern ? normalizeWildcardPattern(rawRoutePattern) : void 0;
418
+ const dynamicRoute = routePattern ? extractDynamicRouteInfo(routePattern, transportContext.adapter.getPath()) : null;
419
+ if (dynamicRoute) {
420
+ const inputSchemaProps = enrichedResult.schema?.properties?.input?.properties || {};
421
+ const hasPathParamsInSchema = "pathParams" in inputSchemaProps;
422
+ return {
423
+ ...enrichedResult,
424
+ routeTemplate: dynamicRoute.routeTemplate,
425
+ info: {
426
+ ...enrichedResult.info,
427
+ input: { ...enrichedResult.info.input, pathParams: dynamicRoute.pathParams }
428
+ },
429
+ ...!hasPathParamsInSchema ? {
430
+ schema: {
431
+ ...enrichedResult.schema,
432
+ properties: {
433
+ ...enrichedResult.schema?.properties,
434
+ input: {
435
+ ...enrichedResult.schema?.properties?.input,
436
+ properties: {
437
+ ...inputSchemaProps,
438
+ pathParams: { type: "object" }
439
+ }
440
+ }
441
+ }
442
+ }
443
+ } : {}
444
+ };
445
+ }
446
+ return enrichedResult;
342
447
  }
343
448
  };
344
449
 
@@ -460,6 +565,23 @@ function extractResourceMetadataV1(paymentRequirements) {
460
565
  }
461
566
 
462
567
  // src/bazaar/facilitator.ts
568
+ var ROUTE_TEMPLATE_REGEX = /^\/[a-zA-Z0-9_/:.\-~%]+$/;
569
+ function isValidRouteTemplate(value) {
570
+ if (!value) return false;
571
+ if (!ROUTE_TEMPLATE_REGEX.test(value)) return false;
572
+ let decoded;
573
+ try {
574
+ decoded = decodeURIComponent(value);
575
+ } catch {
576
+ return false;
577
+ }
578
+ if (decoded.includes("..")) return false;
579
+ if (decoded.includes("://")) return false;
580
+ return true;
581
+ }
582
+ function validateRouteTemplate(value) {
583
+ return isValidRouteTemplate(value) ? value : void 0;
584
+ }
463
585
  function validateDiscoveryExtension(extension) {
464
586
  try {
465
587
  const ajv = new import__.default({ strict: false, allErrors: true });
@@ -485,12 +607,18 @@ function validateDiscoveryExtension(extension) {
485
607
  function extractDiscoveryInfo(paymentPayload, paymentRequirements, validate = true) {
486
608
  let discoveryInfo = null;
487
609
  let resourceUrl;
610
+ let routeTemplate;
488
611
  if (paymentPayload.x402Version === 2) {
489
612
  resourceUrl = paymentPayload.resource?.url ?? "";
490
613
  if (paymentPayload.extensions) {
491
614
  const bazaarExtension = paymentPayload.extensions[BAZAAR.key];
492
615
  if (bazaarExtension && typeof bazaarExtension === "object") {
493
616
  try {
617
+ const rawExt = bazaarExtension;
618
+ const rawTemplate = typeof rawExt.routeTemplate === "string" ? rawExt.routeTemplate : void 0;
619
+ if (isValidRouteTemplate(rawTemplate)) {
620
+ routeTemplate = rawTemplate;
621
+ }
494
622
  const extension = bazaarExtension;
495
623
  if (validate) {
496
624
  const result = validateDiscoveryExtension(extension);
@@ -520,7 +648,7 @@ function extractDiscoveryInfo(paymentPayload, paymentRequirements, validate = tr
520
648
  return null;
521
649
  }
522
650
  const url = new URL(resourceUrl);
523
- const normalizedResourceUrl = `${url.origin}${url.pathname}`;
651
+ const canonicalUrl = routeTemplate ? `${url.origin}${routeTemplate}` : `${url.origin}${url.pathname}`;
524
652
  let description;
525
653
  let mimeType;
526
654
  if (paymentPayload.x402Version === 2) {
@@ -532,7 +660,7 @@ function extractDiscoveryInfo(paymentPayload, paymentRequirements, validate = tr
532
660
  mimeType = requirementsV1.mimeType;
533
661
  }
534
662
  const base = {
535
- resourceUrl: normalizedResourceUrl,
663
+ resourceUrl: canonicalUrl,
536
664
  description,
537
665
  mimeType,
538
666
  x402Version: paymentPayload.x402Version,
@@ -541,7 +669,7 @@ function extractDiscoveryInfo(paymentPayload, paymentRequirements, validate = tr
541
669
  if (discoveryInfo.input.type === "mcp") {
542
670
  return { ...base, toolName: discoveryInfo.input.toolName };
543
671
  }
544
- return { ...base, method: discoveryInfo.input.method };
672
+ return { ...base, routeTemplate, method: discoveryInfo.input.method };
545
673
  }
546
674
  function extractDiscoveryInfoFromExtension(extension, validate = true) {
547
675
  if (validate) {
@@ -622,8 +750,10 @@ function withBazaar(client) {
622
750
  isDiscoverableV1,
623
751
  isMcpExtensionConfig,
624
752
  isQueryExtensionConfig,
753
+ isValidRouteTemplate,
625
754
  validateAndExtract,
626
755
  validateDiscoveryExtension,
756
+ validateRouteTemplate,
627
757
  withBazaar
628
758
  });
629
759
  //# sourceMappingURL=index.js.map