@agentcash/router 0.6.7 → 0.7.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/README.md CHANGED
@@ -106,9 +106,20 @@ export const GET = router.wellKnown();
106
106
  // app/openapi.json/route.ts
107
107
  import { router } from '@/lib/routes';
108
108
  import '@/lib/routes/barrel';
109
- export const GET = router.openapi({ title: 'My API', version: '1.0.0' });
109
+ export const GET = router.openapi({
110
+ title: 'My API',
111
+ version: '1.0.0',
112
+ llmsTxtUrl: 'https://my-api.dev/llms.txt',
113
+ ownershipProofs: ['did:example:proof'],
114
+ });
110
115
  ```
111
116
 
117
+ OpenAPI output follows the discovery contract:
118
+
119
+ - Paid signaling via `responses.402` + `x-payment-info`
120
+ - Auth signaling via `security` + `components.securitySchemes`
121
+ - Optional top-level metadata via `x-discovery` (`llmsTxtUrl`, `ownershipProofs`)
122
+
112
123
  ## API
113
124
 
114
125
  ### `createRouter(config)`
package/dist/index.cjs CHANGED
@@ -74,12 +74,14 @@ var init_server = __esm({
74
74
  var index_exports = {};
75
75
  __export(index_exports, {
76
76
  HttpError: () => HttpError,
77
+ MemoryEntitlementStore: () => MemoryEntitlementStore,
77
78
  MemoryNonceStore: () => MemoryNonceStore,
78
79
  RouteBuilder: () => RouteBuilder,
79
80
  RouteRegistry: () => RouteRegistry,
80
81
  SIWX_CHALLENGE_EXPIRY_MS: () => SIWX_CHALLENGE_EXPIRY_MS,
81
82
  SIWX_ERROR_MESSAGES: () => SIWX_ERROR_MESSAGES,
82
83
  consolePlugin: () => consolePlugin,
84
+ createRedisEntitlementStore: () => createRedisEntitlementStore,
83
85
  createRedisNonceStore: () => createRedisNonceStore,
84
86
  createRouter: () => createRouter
85
87
  });
@@ -620,7 +622,7 @@ function createRequestHandler(routeEntry, handler, deps) {
620
622
  }
621
623
  }
622
624
  }
623
- if (routeEntry.authMode === "siwx") {
625
+ if (routeEntry.authMode === "siwx" || routeEntry.siwxEnabled) {
624
626
  if (routeEntry.validateFn && routeEntry.bodySchema && !request.headers.get("SIGN-IN-WITH-X")) {
625
627
  const requestForValidation = request.clone();
626
628
  const earlyBodyResult = await parseBody(requestForValidation, routeEntry);
@@ -634,7 +636,8 @@ function createRequestHandler(routeEntry, handler, deps) {
634
636
  }
635
637
  }
636
638
  }
637
- if (!request.headers.get("SIGN-IN-WITH-X")) {
639
+ const siwxHeader = request.headers.get("SIGN-IN-WITH-X");
640
+ if (!siwxHeader && routeEntry.authMode === "siwx") {
638
641
  const url = new URL(request.url);
639
642
  const nonce = crypto.randomUUID().replace(/-/g, "");
640
643
  const siwxInfo = {
@@ -690,23 +693,41 @@ function createRequestHandler(routeEntry, handler, deps) {
690
693
  firePluginResponse(deps, pluginCtx, meta, response);
691
694
  return response;
692
695
  }
693
- const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
694
- if (!siwx.valid) {
695
- const response = import_server2.NextResponse.json(
696
- { error: siwx.code, message: SIWX_ERROR_MESSAGES[siwx.code] },
697
- { status: 402 }
698
- );
699
- firePluginResponse(deps, pluginCtx, meta, response);
700
- return response;
696
+ if (siwxHeader) {
697
+ const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
698
+ if (!siwx.valid) {
699
+ if (routeEntry.authMode === "siwx") {
700
+ const response = import_server2.NextResponse.json(
701
+ { error: siwx.code, message: SIWX_ERROR_MESSAGES[siwx.code] },
702
+ { status: 402 }
703
+ );
704
+ firePluginResponse(deps, pluginCtx, meta, response);
705
+ return response;
706
+ }
707
+ } else {
708
+ const wallet = siwx.wallet.toLowerCase();
709
+ pluginCtx.setVerifiedWallet(wallet);
710
+ if (routeEntry.authMode === "siwx") {
711
+ firePluginHook(deps.plugin, "onAuthVerified", pluginCtx, {
712
+ authMode: "siwx",
713
+ wallet,
714
+ route: routeEntry.key
715
+ });
716
+ return handleAuth(wallet, void 0);
717
+ }
718
+ if (routeEntry.siwxEnabled && routeEntry.pricing) {
719
+ const entitled = await deps.entitlementStore.has(routeEntry.key, wallet);
720
+ if (entitled) {
721
+ firePluginHook(deps.plugin, "onAuthVerified", pluginCtx, {
722
+ authMode: "siwx",
723
+ wallet,
724
+ route: routeEntry.key
725
+ });
726
+ return handleAuth(wallet, account);
727
+ }
728
+ }
729
+ }
701
730
  }
702
- const wallet = siwx.wallet.toLowerCase();
703
- pluginCtx.setVerifiedWallet(wallet);
704
- firePluginHook(deps.plugin, "onAuthVerified", pluginCtx, {
705
- authMode: "siwx",
706
- wallet,
707
- route: routeEntry.key
708
- });
709
- return handleAuth(wallet, void 0);
710
731
  }
711
732
  if (!protocol || protocol === "siwx") {
712
733
  if (routeEntry.pricing) {
@@ -814,6 +835,17 @@ function createRequestHandler(routeEntry, handler, deps) {
814
835
  verifyPayload,
815
836
  verifyRequirements
816
837
  );
838
+ if (routeEntry.siwxEnabled) {
839
+ try {
840
+ await deps.entitlementStore.grant(routeEntry.key, wallet);
841
+ } catch (error) {
842
+ firePluginHook(deps.plugin, "onAlert", pluginCtx, {
843
+ level: "warn",
844
+ message: `Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`,
845
+ route: routeEntry.key
846
+ });
847
+ }
848
+ }
817
849
  response.headers.set("PAYMENT-RESPONSE", settle.encoded);
818
850
  firePluginHook(deps.plugin, "onPaymentSettled", pluginCtx, {
819
851
  protocol: "x402",
@@ -892,6 +924,17 @@ function createRequestHandler(routeEntry, handler, deps) {
892
924
  body.data
893
925
  );
894
926
  if (response.status < 400) {
927
+ if (routeEntry.siwxEnabled) {
928
+ try {
929
+ await deps.entitlementStore.grant(routeEntry.key, wallet);
930
+ } catch (error) {
931
+ firePluginHook(deps.plugin, "onAlert", pluginCtx, {
932
+ level: "warn",
933
+ message: `Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`,
934
+ route: routeEntry.key
935
+ });
936
+ }
937
+ }
895
938
  const receiptResponse = mppResult.withReceipt(response);
896
939
  finalize(receiptResponse, rawResult, meta, pluginCtx, body.data);
897
940
  return receiptResponse;
@@ -1016,6 +1059,18 @@ async function build402(request, routeEntry, deps, meta, pluginCtx, bodyData) {
1016
1059
  }
1017
1060
  } catch {
1018
1061
  }
1062
+ if (routeEntry.siwxEnabled) {
1063
+ try {
1064
+ const siwxExtension = await buildSIWXExtension();
1065
+ if (siwxExtension && typeof siwxExtension === "object" && !Array.isArray(siwxExtension)) {
1066
+ extensions = {
1067
+ ...extensions ?? {},
1068
+ ...siwxExtension
1069
+ };
1070
+ }
1071
+ } catch {
1072
+ }
1073
+ }
1019
1074
  if (routeEntry.protocols.includes("x402") && deps.x402Server) {
1020
1075
  try {
1021
1076
  const payTo = await resolvePayTo(routeEntry, request, deps.payeeAddress);
@@ -1120,6 +1175,8 @@ var RouteBuilder = class {
1120
1175
  /** @internal */
1121
1176
  _pricing;
1122
1177
  /** @internal */
1178
+ _siwxEnabled = false;
1179
+ /** @internal */
1123
1180
  _protocols = ["x402"];
1124
1181
  /** @internal */
1125
1182
  _maxPrice;
@@ -1159,15 +1216,14 @@ var RouteBuilder = class {
1159
1216
  return next;
1160
1217
  }
1161
1218
  paid(pricing, options) {
1162
- if (this._authMode === "siwx") {
1163
- throw new Error(
1164
- `route '${this._key}': Cannot combine .paid() and .siwx() on the same route. Paid routes get wallet identity from the payment proof. Use separate routes if you need both payment and SIWX auth.`
1165
- );
1166
- }
1167
1219
  const next = this.fork();
1168
1220
  next._authMode = "paid";
1169
1221
  next._pricing = pricing;
1170
- if (options?.protocols) next._protocols = options.protocols;
1222
+ if (options?.protocols) {
1223
+ next._protocols = options.protocols;
1224
+ } else if (next._protocols.length === 0) {
1225
+ next._protocols = ["x402"];
1226
+ }
1171
1227
  if (options?.maxPrice) next._maxPrice = options.maxPrice;
1172
1228
  if (options?.minPrice) next._minPrice = options.minPrice;
1173
1229
  if (options?.payTo) next._payTo = options.payTo;
@@ -1195,17 +1251,33 @@ var RouteBuilder = class {
1195
1251
  return next;
1196
1252
  }
1197
1253
  siwx() {
1198
- if (this._authMode === "paid") {
1254
+ if (this._authMode === "unprotected") {
1199
1255
  throw new Error(
1200
- `route '${this._key}': Cannot combine .paid() and .siwx() on the same route. Paid routes get wallet identity from the payment proof. Use separate routes if you need both payment and SIWX auth.`
1256
+ `route '${this._key}': Cannot combine .unprotected() and .siwx() on the same route.`
1257
+ );
1258
+ }
1259
+ if (this._apiKeyResolver) {
1260
+ throw new Error(
1261
+ `route '${this._key}': Combining .siwx() and .apiKey() is not supported on the same route.`
1201
1262
  );
1202
1263
  }
1203
1264
  const next = this.fork();
1265
+ next._siwxEnabled = true;
1266
+ if (next._authMode === "paid" || next._pricing) {
1267
+ next._authMode = "paid";
1268
+ if (next._protocols.length === 0) next._protocols = ["x402"];
1269
+ return next;
1270
+ }
1204
1271
  next._authMode = "siwx";
1205
1272
  next._protocols = [];
1206
1273
  return next;
1207
1274
  }
1208
1275
  apiKey(resolver) {
1276
+ if (this._siwxEnabled) {
1277
+ throw new Error(
1278
+ `route '${this._key}': Combining .apiKey() and .siwx() is not supported on the same route.`
1279
+ );
1280
+ }
1209
1281
  const next = this.fork();
1210
1282
  next._authMode = "apiKey";
1211
1283
  next._apiKeyResolver = resolver;
@@ -1298,6 +1370,7 @@ var RouteBuilder = class {
1298
1370
  const entry = {
1299
1371
  key: this._key,
1300
1372
  authMode: this._authMode,
1373
+ siwxEnabled: this._siwxEnabled,
1301
1374
  pricing: this._pricing,
1302
1375
  protocols: this._protocols,
1303
1376
  bodySchema: this._bodySchema,
@@ -1319,6 +1392,68 @@ var RouteBuilder = class {
1319
1392
  }
1320
1393
  };
1321
1394
 
1395
+ // src/auth/entitlement.ts
1396
+ var MemoryEntitlementStore = class {
1397
+ routeToWallets = /* @__PURE__ */ new Map();
1398
+ async has(route, wallet) {
1399
+ const wallets = this.routeToWallets.get(route);
1400
+ if (!wallets) return false;
1401
+ return wallets.has(wallet.toLowerCase());
1402
+ }
1403
+ async grant(route, wallet) {
1404
+ const normalizedWallet = wallet.toLowerCase();
1405
+ let wallets = this.routeToWallets.get(route);
1406
+ if (!wallets) {
1407
+ wallets = /* @__PURE__ */ new Set();
1408
+ this.routeToWallets.set(route, wallets);
1409
+ }
1410
+ wallets.add(normalizedWallet);
1411
+ }
1412
+ };
1413
+ function detectRedisClientType2(client) {
1414
+ if (!client || typeof client !== "object") {
1415
+ throw new Error(
1416
+ "createRedisEntitlementStore requires a Redis client. Supported: @upstash/redis, ioredis."
1417
+ );
1418
+ }
1419
+ if ("options" in client && "status" in client) return "ioredis";
1420
+ const constructor = client.constructor?.name;
1421
+ if (constructor === "Redis" && "url" in client) return "upstash";
1422
+ if (typeof client.sadd === "function" && typeof client.sismember === "function") {
1423
+ return "upstash";
1424
+ }
1425
+ throw new Error("Unrecognized Redis client for entitlement store.");
1426
+ }
1427
+ function createRedisEntitlementStore(client, options) {
1428
+ const clientType = detectRedisClientType2(client);
1429
+ const prefix = options?.prefix ?? "siwx:entitlement:";
1430
+ return {
1431
+ async has(route, wallet) {
1432
+ const key = `${prefix}${route}`;
1433
+ const normalizedWallet = wallet.toLowerCase();
1434
+ if (clientType === "upstash") {
1435
+ const redis2 = client;
1436
+ const result2 = await redis2.sismember(key, normalizedWallet);
1437
+ return result2 === 1 || result2 === true;
1438
+ }
1439
+ const redis = client;
1440
+ const result = await redis.sismember(key, normalizedWallet);
1441
+ return result === 1;
1442
+ },
1443
+ async grant(route, wallet) {
1444
+ const key = `${prefix}${route}`;
1445
+ const normalizedWallet = wallet.toLowerCase();
1446
+ if (clientType === "upstash") {
1447
+ const redis2 = client;
1448
+ await redis2.sadd(key, normalizedWallet);
1449
+ return;
1450
+ }
1451
+ const redis = client;
1452
+ await redis.sadd(key, normalizedWallet);
1453
+ }
1454
+ };
1455
+ }
1456
+
1322
1457
  // src/discovery/well-known.ts
1323
1458
  var import_server3 = require("next/server");
1324
1459
  function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
@@ -1384,14 +1519,41 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, options) {
1384
1519
  const { createDocument } = await import("zod-openapi");
1385
1520
  const paths = {};
1386
1521
  const tagSet = /* @__PURE__ */ new Set();
1522
+ let requiresSiwxScheme = false;
1523
+ let requiresApiKeyScheme = false;
1387
1524
  for (const [key, entry] of registry.entries()) {
1388
1525
  const apiPath = `/api/${entry.path ?? key}`;
1389
1526
  const method = entry.method.toLowerCase();
1390
1527
  const tag = deriveTag(key);
1391
1528
  tagSet.add(tag);
1392
- paths[apiPath] = { ...paths[apiPath], [method]: buildOperation(key, entry, tag) };
1529
+ const built = buildOperation(key, entry, tag);
1530
+ if (built.requiresSiwxScheme) requiresSiwxScheme = true;
1531
+ if (built.requiresApiKeyScheme) requiresApiKeyScheme = true;
1532
+ paths[apiPath] = { ...paths[apiPath], [method]: built.operation };
1533
+ }
1534
+ const securitySchemes = {};
1535
+ if (requiresSiwxScheme) {
1536
+ securitySchemes.siwx = {
1537
+ type: "apiKey",
1538
+ in: "header",
1539
+ name: "SIGN-IN-WITH-X"
1540
+ };
1541
+ }
1542
+ if (requiresApiKeyScheme) {
1543
+ securitySchemes.apiKey = {
1544
+ type: "apiKey",
1545
+ in: "header",
1546
+ name: "X-API-Key"
1547
+ };
1548
+ }
1549
+ const discoveryMetadata = {};
1550
+ if (options.ownershipProofs && options.ownershipProofs.length > 0) {
1551
+ discoveryMetadata.ownershipProofs = options.ownershipProofs;
1393
1552
  }
1394
- cached = createDocument({
1553
+ if (options.llmsTxtUrl) {
1554
+ discoveryMetadata.llmsTxtUrl = options.llmsTxtUrl;
1555
+ }
1556
+ const openApiDocument = {
1395
1557
  openapi: "3.1.0",
1396
1558
  info: {
1397
1559
  title: options.title,
@@ -1401,8 +1563,17 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, options) {
1401
1563
  },
1402
1564
  servers: [{ url: (options.baseUrl ?? normalizedBase).replace(/\/+$/, "") }],
1403
1565
  tags: Array.from(tagSet).sort().map((name) => ({ name })),
1566
+ ...Object.keys(securitySchemes).length > 0 ? {
1567
+ components: {
1568
+ securitySchemes
1569
+ }
1570
+ } : {},
1571
+ ...Object.keys(discoveryMetadata).length > 0 ? {
1572
+ "x-discovery": discoveryMetadata
1573
+ } : {},
1404
1574
  paths
1405
- });
1575
+ };
1576
+ cached = createDocument(openApiDocument);
1406
1577
  return import_server4.NextResponse.json(cached);
1407
1578
  };
1408
1579
  }
@@ -1411,19 +1582,10 @@ function deriveTag(routeKey) {
1411
1582
  }
1412
1583
  function buildOperation(routeKey, entry, tag) {
1413
1584
  const protocols = entry.protocols.length > 0 ? entry.protocols : void 0;
1414
- let price;
1415
- if (typeof entry.pricing === "string") {
1416
- price = entry.pricing;
1417
- } else if (typeof entry.pricing === "object" && "tiers" in entry.pricing) {
1418
- const tierPrices = Object.values(entry.pricing.tiers).map((t) => parseFloat(t.price));
1419
- const min = Math.min(...tierPrices);
1420
- const max = Math.max(...tierPrices);
1421
- price = min === max ? String(min) : `${min}-${max}`;
1422
- } else if (entry.minPrice && entry.maxPrice) {
1423
- price = `${entry.minPrice}-${entry.maxPrice}`;
1424
- } else if (entry.maxPrice) {
1425
- price = entry.maxPrice;
1426
- }
1585
+ const paymentRequired = Boolean(entry.pricing) || entry.authMode === "paid";
1586
+ const requiresSiwxScheme = entry.authMode === "siwx" || Boolean(entry.siwxEnabled);
1587
+ const requiresApiKeyScheme = Boolean(entry.apiKeyResolver) && entry.authMode !== "siwx";
1588
+ const pricingInfo = buildPricingInfo(entry);
1427
1589
  const operation = {
1428
1590
  operationId: routeKey.replace(/\//g, "_"),
1429
1591
  summary: entry.description ?? routeKey,
@@ -1437,19 +1599,29 @@ function buildOperation(routeKey, entry, tag) {
1437
1599
  }
1438
1600
  }
1439
1601
  },
1440
- ...entry.authMode === "paid" && {
1602
+ ...(paymentRequired || requiresSiwxScheme) && {
1441
1603
  "402": {
1442
- description: "Payment Required"
1604
+ description: requiresSiwxScheme ? "Authentication Required" : "Payment Required"
1605
+ }
1606
+ },
1607
+ ...requiresApiKeyScheme && {
1608
+ "401": {
1609
+ description: "Unauthorized"
1443
1610
  }
1444
1611
  }
1445
1612
  }
1446
1613
  };
1447
- if (price !== void 0 || protocols) {
1614
+ if (paymentRequired && (pricingInfo || protocols)) {
1448
1615
  operation["x-payment-info"] = {
1449
- ...price !== void 0 && { price },
1616
+ ...pricingInfo ?? {},
1450
1617
  ...protocols && { protocols }
1451
1618
  };
1452
1619
  }
1620
+ if (requiresSiwxScheme) {
1621
+ operation.security = [{ siwx: [] }];
1622
+ } else if (requiresApiKeyScheme) {
1623
+ operation.security = [{ apiKey: [] }];
1624
+ }
1453
1625
  if (entry.bodySchema) {
1454
1626
  operation.requestBody = {
1455
1627
  required: true,
@@ -1463,13 +1635,57 @@ function buildOperation(routeKey, entry, tag) {
1463
1635
  query: entry.querySchema
1464
1636
  };
1465
1637
  }
1466
- return operation;
1638
+ return {
1639
+ operation,
1640
+ requiresSiwxScheme,
1641
+ requiresApiKeyScheme
1642
+ };
1643
+ }
1644
+ function buildPricingInfo(entry) {
1645
+ if (!entry.pricing) return void 0;
1646
+ if (typeof entry.pricing === "string") {
1647
+ return {
1648
+ pricingMode: "fixed",
1649
+ price: entry.pricing
1650
+ };
1651
+ }
1652
+ if (typeof entry.pricing === "function") {
1653
+ return {
1654
+ pricingMode: "quote",
1655
+ ...entry.minPrice ? { minPrice: entry.minPrice } : {},
1656
+ ...entry.maxPrice ? { maxPrice: entry.maxPrice } : {}
1657
+ };
1658
+ }
1659
+ if ("tiers" in entry.pricing) {
1660
+ const tierPrices = Object.values(entry.pricing.tiers).map((tier) => parseFloat(tier.price));
1661
+ const min = Math.min(...tierPrices);
1662
+ const max = Math.max(...tierPrices);
1663
+ if (Number.isFinite(min) && Number.isFinite(max)) {
1664
+ if (min === max) {
1665
+ return {
1666
+ pricingMode: "fixed",
1667
+ price: String(min)
1668
+ };
1669
+ }
1670
+ return {
1671
+ pricingMode: "range",
1672
+ minPrice: String(min),
1673
+ maxPrice: String(max)
1674
+ };
1675
+ }
1676
+ return {
1677
+ pricingMode: "quote",
1678
+ ...entry.maxPrice ? { maxPrice: entry.maxPrice } : {}
1679
+ };
1680
+ }
1681
+ return void 0;
1467
1682
  }
1468
1683
 
1469
1684
  // src/index.ts
1470
1685
  function createRouter(config) {
1471
1686
  const registry = new RouteRegistry();
1472
1687
  const nonceStore = config.siwx?.nonceStore ?? new MemoryNonceStore();
1688
+ const entitlementStore = config.siwx?.entitlementStore ?? new MemoryEntitlementStore();
1473
1689
  const network = config.network ?? "eip155:8453";
1474
1690
  if (!config.baseUrl) {
1475
1691
  throw new Error(
@@ -1518,6 +1734,7 @@ function createRouter(config) {
1518
1734
  initPromise: Promise.resolve(),
1519
1735
  plugin: config.plugin,
1520
1736
  nonceStore,
1737
+ entitlementStore,
1521
1738
  payeeAddress: config.payeeAddress,
1522
1739
  network,
1523
1740
  mppx: null
@@ -1602,12 +1819,14 @@ function createRouter(config) {
1602
1819
  // Annotate the CommonJS export names for ESM import in node:
1603
1820
  0 && (module.exports = {
1604
1821
  HttpError,
1822
+ MemoryEntitlementStore,
1605
1823
  MemoryNonceStore,
1606
1824
  RouteBuilder,
1607
1825
  RouteRegistry,
1608
1826
  SIWX_CHALLENGE_EXPIRY_MS,
1609
1827
  SIWX_ERROR_MESSAGES,
1610
1828
  consolePlugin,
1829
+ createRedisEntitlementStore,
1611
1830
  createRedisNonceStore,
1612
1831
  createRouter
1613
1832
  });
package/dist/index.d.cts CHANGED
@@ -3,6 +3,29 @@ import { ZodType } from 'zod';
3
3
  import { PaymentRequirements, PaymentRequired, SettleResponse } from '@x402/core/types';
4
4
  export { S as SIWX_ERROR_MESSAGES, a as SiwxErrorCode } from './siwx-BMlja_nt.cjs';
5
5
 
6
+ interface EntitlementStore {
7
+ has(route: string, wallet: string): Promise<boolean>;
8
+ grant(route: string, wallet: string): Promise<void>;
9
+ }
10
+ /**
11
+ * In-memory SIWX entitlement store.
12
+ *
13
+ * Suitable for development and tests. Not durable across server restarts.
14
+ */
15
+ declare class MemoryEntitlementStore implements EntitlementStore {
16
+ private readonly routeToWallets;
17
+ has(route: string, wallet: string): Promise<boolean>;
18
+ grant(route: string, wallet: string): Promise<void>;
19
+ }
20
+ interface RedisEntitlementStoreOptions {
21
+ /** Key prefix. Default: 'siwx:entitlement:' */
22
+ prefix?: string;
23
+ }
24
+ /**
25
+ * Redis-backed entitlement store for paid+SIWX acceleration.
26
+ */
27
+ declare function createRedisEntitlementStore(client: unknown, options?: RedisEntitlementStoreOptions): EntitlementStore;
28
+
6
29
  /**
7
30
  * SIWX challenge expiry in milliseconds.
8
31
  * Currently not configurable per-route — this is a known limitation.
@@ -216,6 +239,11 @@ interface ProviderQuotaEvent {
216
239
  interface RouteEntry {
217
240
  key: string;
218
241
  authMode: AuthMode;
242
+ /**
243
+ * Enables SIWX acceleration on paid routes.
244
+ * When true, valid SIWX proofs can bypass repeat payment if entitlement exists.
245
+ */
246
+ siwxEnabled?: boolean;
219
247
  pricing?: PricingConfig;
220
248
  protocols: ProtocolType[];
221
249
  bodySchema?: ZodType;
@@ -247,6 +275,7 @@ interface RouterConfig {
247
275
  plugin?: RouterPlugin;
248
276
  siwx?: {
249
277
  nonceStore?: NonceStore;
278
+ entitlementStore?: EntitlementStore;
250
279
  };
251
280
  prices?: Record<string, string>;
252
281
  mpp?: {
@@ -297,6 +326,8 @@ interface OpenAPIOptions {
297
326
  name?: string;
298
327
  url?: string;
299
328
  };
329
+ llmsTxtUrl?: string;
330
+ ownershipProofs?: string[];
300
331
  }
301
332
 
302
333
  interface OrchestrateDeps {
@@ -306,6 +337,7 @@ interface OrchestrateDeps {
306
337
  mppInitError?: string;
307
338
  plugin?: RouterPlugin;
308
339
  nonceStore: NonceStore;
340
+ entitlementStore: EntitlementStore;
309
341
  payeeAddress: string;
310
342
  network: string;
311
343
  mppx?: {
@@ -329,6 +361,7 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, HasAuth extend
329
361
  /** @internal */ readonly _deps: OrchestrateDeps;
330
362
  /** @internal */ _authMode: AuthMode | null;
331
363
  /** @internal */ _pricing: PricingConfig | undefined;
364
+ /** @internal */ _siwxEnabled: boolean;
332
365
  /** @internal */ _protocols: ProtocolType[];
333
366
  /** @internal */ _maxPrice: string | undefined;
334
367
  /** @internal */ _minPrice: string | undefined;
@@ -357,7 +390,7 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, HasAuth extend
357
390
  }>;
358
391
  default?: string;
359
392
  }, options?: PaidOptions): RouteBuilder<TBody, TQuery, True, True, HasBody>;
360
- siwx(): HasAuth extends true ? never : RouteBuilder<TBody, TQuery, True, False, HasBody>;
393
+ siwx(): RouteBuilder<TBody, TQuery, True, False, HasBody>;
361
394
  apiKey(resolver: (key: string) => unknown | Promise<unknown>): RouteBuilder<TBody, TQuery, True, NeedsBody, HasBody>;
362
395
  unprotected(): RouteBuilder<TBody, TQuery, True, False, HasBody>;
363
396
  provider(name: string, config?: ProviderConfig): this;
@@ -414,4 +447,4 @@ declare function createRouter<const P extends Record<string, string> = Record<ne
414
447
  prices?: P;
415
448
  }): ServiceRouter<Extract<keyof P, string>>;
416
449
 
417
- export { type AlertEvent, type AlertFn, type AlertLevel, type AuthEvent, type AuthMode, type ErrorEvent, type HandlerContext, HttpError, MemoryNonceStore, type MonitorEntry, type NonceStore, type OveragePolicy, type PaidOptions, type PaymentEvent, type PluginContext, type PricingConfig, type ProtocolType, type ProviderConfig, type ProviderQuotaEvent, type QuotaInfo, type QuotaLevel, type RedisNonceStoreOptions, type RequestMeta, type ResponseMeta, RouteBuilder, type RouteEntry, RouteRegistry, type RouterConfig, type RouterPlugin, SIWX_CHALLENGE_EXPIRY_MS, type ServiceRouter, type SettlementEvent, type TierConfig, type X402Server, consolePlugin, createRedisNonceStore, createRouter };
450
+ export { type AlertEvent, type AlertFn, type AlertLevel, type AuthEvent, type AuthMode, type EntitlementStore, type ErrorEvent, type HandlerContext, HttpError, MemoryEntitlementStore, MemoryNonceStore, type MonitorEntry, type NonceStore, type OveragePolicy, type PaidOptions, type PaymentEvent, type PluginContext, type PricingConfig, type ProtocolType, type ProviderConfig, type ProviderQuotaEvent, type QuotaInfo, type QuotaLevel, type RedisEntitlementStoreOptions, type RedisNonceStoreOptions, type RequestMeta, type ResponseMeta, RouteBuilder, type RouteEntry, RouteRegistry, type RouterConfig, type RouterPlugin, SIWX_CHALLENGE_EXPIRY_MS, type ServiceRouter, type SettlementEvent, type TierConfig, type X402Server, consolePlugin, createRedisEntitlementStore, createRedisNonceStore, createRouter };
package/dist/index.d.ts CHANGED
@@ -3,6 +3,29 @@ import { ZodType } from 'zod';
3
3
  import { PaymentRequirements, PaymentRequired, SettleResponse } from '@x402/core/types';
4
4
  export { S as SIWX_ERROR_MESSAGES, a as SiwxErrorCode } from './siwx-BMlja_nt.js';
5
5
 
6
+ interface EntitlementStore {
7
+ has(route: string, wallet: string): Promise<boolean>;
8
+ grant(route: string, wallet: string): Promise<void>;
9
+ }
10
+ /**
11
+ * In-memory SIWX entitlement store.
12
+ *
13
+ * Suitable for development and tests. Not durable across server restarts.
14
+ */
15
+ declare class MemoryEntitlementStore implements EntitlementStore {
16
+ private readonly routeToWallets;
17
+ has(route: string, wallet: string): Promise<boolean>;
18
+ grant(route: string, wallet: string): Promise<void>;
19
+ }
20
+ interface RedisEntitlementStoreOptions {
21
+ /** Key prefix. Default: 'siwx:entitlement:' */
22
+ prefix?: string;
23
+ }
24
+ /**
25
+ * Redis-backed entitlement store for paid+SIWX acceleration.
26
+ */
27
+ declare function createRedisEntitlementStore(client: unknown, options?: RedisEntitlementStoreOptions): EntitlementStore;
28
+
6
29
  /**
7
30
  * SIWX challenge expiry in milliseconds.
8
31
  * Currently not configurable per-route — this is a known limitation.
@@ -216,6 +239,11 @@ interface ProviderQuotaEvent {
216
239
  interface RouteEntry {
217
240
  key: string;
218
241
  authMode: AuthMode;
242
+ /**
243
+ * Enables SIWX acceleration on paid routes.
244
+ * When true, valid SIWX proofs can bypass repeat payment if entitlement exists.
245
+ */
246
+ siwxEnabled?: boolean;
219
247
  pricing?: PricingConfig;
220
248
  protocols: ProtocolType[];
221
249
  bodySchema?: ZodType;
@@ -247,6 +275,7 @@ interface RouterConfig {
247
275
  plugin?: RouterPlugin;
248
276
  siwx?: {
249
277
  nonceStore?: NonceStore;
278
+ entitlementStore?: EntitlementStore;
250
279
  };
251
280
  prices?: Record<string, string>;
252
281
  mpp?: {
@@ -297,6 +326,8 @@ interface OpenAPIOptions {
297
326
  name?: string;
298
327
  url?: string;
299
328
  };
329
+ llmsTxtUrl?: string;
330
+ ownershipProofs?: string[];
300
331
  }
301
332
 
302
333
  interface OrchestrateDeps {
@@ -306,6 +337,7 @@ interface OrchestrateDeps {
306
337
  mppInitError?: string;
307
338
  plugin?: RouterPlugin;
308
339
  nonceStore: NonceStore;
340
+ entitlementStore: EntitlementStore;
309
341
  payeeAddress: string;
310
342
  network: string;
311
343
  mppx?: {
@@ -329,6 +361,7 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, HasAuth extend
329
361
  /** @internal */ readonly _deps: OrchestrateDeps;
330
362
  /** @internal */ _authMode: AuthMode | null;
331
363
  /** @internal */ _pricing: PricingConfig | undefined;
364
+ /** @internal */ _siwxEnabled: boolean;
332
365
  /** @internal */ _protocols: ProtocolType[];
333
366
  /** @internal */ _maxPrice: string | undefined;
334
367
  /** @internal */ _minPrice: string | undefined;
@@ -357,7 +390,7 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, HasAuth extend
357
390
  }>;
358
391
  default?: string;
359
392
  }, options?: PaidOptions): RouteBuilder<TBody, TQuery, True, True, HasBody>;
360
- siwx(): HasAuth extends true ? never : RouteBuilder<TBody, TQuery, True, False, HasBody>;
393
+ siwx(): RouteBuilder<TBody, TQuery, True, False, HasBody>;
361
394
  apiKey(resolver: (key: string) => unknown | Promise<unknown>): RouteBuilder<TBody, TQuery, True, NeedsBody, HasBody>;
362
395
  unprotected(): RouteBuilder<TBody, TQuery, True, False, HasBody>;
363
396
  provider(name: string, config?: ProviderConfig): this;
@@ -414,4 +447,4 @@ declare function createRouter<const P extends Record<string, string> = Record<ne
414
447
  prices?: P;
415
448
  }): ServiceRouter<Extract<keyof P, string>>;
416
449
 
417
- export { type AlertEvent, type AlertFn, type AlertLevel, type AuthEvent, type AuthMode, type ErrorEvent, type HandlerContext, HttpError, MemoryNonceStore, type MonitorEntry, type NonceStore, type OveragePolicy, type PaidOptions, type PaymentEvent, type PluginContext, type PricingConfig, type ProtocolType, type ProviderConfig, type ProviderQuotaEvent, type QuotaInfo, type QuotaLevel, type RedisNonceStoreOptions, type RequestMeta, type ResponseMeta, RouteBuilder, type RouteEntry, RouteRegistry, type RouterConfig, type RouterPlugin, SIWX_CHALLENGE_EXPIRY_MS, type ServiceRouter, type SettlementEvent, type TierConfig, type X402Server, consolePlugin, createRedisNonceStore, createRouter };
450
+ export { type AlertEvent, type AlertFn, type AlertLevel, type AuthEvent, type AuthMode, type EntitlementStore, type ErrorEvent, type HandlerContext, HttpError, MemoryEntitlementStore, MemoryNonceStore, type MonitorEntry, type NonceStore, type OveragePolicy, type PaidOptions, type PaymentEvent, type PluginContext, type PricingConfig, type ProtocolType, type ProviderConfig, type ProviderQuotaEvent, type QuotaInfo, type QuotaLevel, type RedisEntitlementStoreOptions, type RedisNonceStoreOptions, type RequestMeta, type ResponseMeta, RouteBuilder, type RouteEntry, RouteRegistry, type RouterConfig, type RouterPlugin, SIWX_CHALLENGE_EXPIRY_MS, type ServiceRouter, type SettlementEvent, type TierConfig, type X402Server, consolePlugin, createRedisEntitlementStore, createRedisNonceStore, createRouter };
package/dist/index.js CHANGED
@@ -583,7 +583,7 @@ function createRequestHandler(routeEntry, handler, deps) {
583
583
  }
584
584
  }
585
585
  }
586
- if (routeEntry.authMode === "siwx") {
586
+ if (routeEntry.authMode === "siwx" || routeEntry.siwxEnabled) {
587
587
  if (routeEntry.validateFn && routeEntry.bodySchema && !request.headers.get("SIGN-IN-WITH-X")) {
588
588
  const requestForValidation = request.clone();
589
589
  const earlyBodyResult = await parseBody(requestForValidation, routeEntry);
@@ -597,7 +597,8 @@ function createRequestHandler(routeEntry, handler, deps) {
597
597
  }
598
598
  }
599
599
  }
600
- if (!request.headers.get("SIGN-IN-WITH-X")) {
600
+ const siwxHeader = request.headers.get("SIGN-IN-WITH-X");
601
+ if (!siwxHeader && routeEntry.authMode === "siwx") {
601
602
  const url = new URL(request.url);
602
603
  const nonce = crypto.randomUUID().replace(/-/g, "");
603
604
  const siwxInfo = {
@@ -653,23 +654,41 @@ function createRequestHandler(routeEntry, handler, deps) {
653
654
  firePluginResponse(deps, pluginCtx, meta, response);
654
655
  return response;
655
656
  }
656
- const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
657
- if (!siwx.valid) {
658
- const response = NextResponse2.json(
659
- { error: siwx.code, message: SIWX_ERROR_MESSAGES[siwx.code] },
660
- { status: 402 }
661
- );
662
- firePluginResponse(deps, pluginCtx, meta, response);
663
- return response;
657
+ if (siwxHeader) {
658
+ const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
659
+ if (!siwx.valid) {
660
+ if (routeEntry.authMode === "siwx") {
661
+ const response = NextResponse2.json(
662
+ { error: siwx.code, message: SIWX_ERROR_MESSAGES[siwx.code] },
663
+ { status: 402 }
664
+ );
665
+ firePluginResponse(deps, pluginCtx, meta, response);
666
+ return response;
667
+ }
668
+ } else {
669
+ const wallet = siwx.wallet.toLowerCase();
670
+ pluginCtx.setVerifiedWallet(wallet);
671
+ if (routeEntry.authMode === "siwx") {
672
+ firePluginHook(deps.plugin, "onAuthVerified", pluginCtx, {
673
+ authMode: "siwx",
674
+ wallet,
675
+ route: routeEntry.key
676
+ });
677
+ return handleAuth(wallet, void 0);
678
+ }
679
+ if (routeEntry.siwxEnabled && routeEntry.pricing) {
680
+ const entitled = await deps.entitlementStore.has(routeEntry.key, wallet);
681
+ if (entitled) {
682
+ firePluginHook(deps.plugin, "onAuthVerified", pluginCtx, {
683
+ authMode: "siwx",
684
+ wallet,
685
+ route: routeEntry.key
686
+ });
687
+ return handleAuth(wallet, account);
688
+ }
689
+ }
690
+ }
664
691
  }
665
- const wallet = siwx.wallet.toLowerCase();
666
- pluginCtx.setVerifiedWallet(wallet);
667
- firePluginHook(deps.plugin, "onAuthVerified", pluginCtx, {
668
- authMode: "siwx",
669
- wallet,
670
- route: routeEntry.key
671
- });
672
- return handleAuth(wallet, void 0);
673
692
  }
674
693
  if (!protocol || protocol === "siwx") {
675
694
  if (routeEntry.pricing) {
@@ -777,6 +796,17 @@ function createRequestHandler(routeEntry, handler, deps) {
777
796
  verifyPayload,
778
797
  verifyRequirements
779
798
  );
799
+ if (routeEntry.siwxEnabled) {
800
+ try {
801
+ await deps.entitlementStore.grant(routeEntry.key, wallet);
802
+ } catch (error) {
803
+ firePluginHook(deps.plugin, "onAlert", pluginCtx, {
804
+ level: "warn",
805
+ message: `Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`,
806
+ route: routeEntry.key
807
+ });
808
+ }
809
+ }
780
810
  response.headers.set("PAYMENT-RESPONSE", settle.encoded);
781
811
  firePluginHook(deps.plugin, "onPaymentSettled", pluginCtx, {
782
812
  protocol: "x402",
@@ -855,6 +885,17 @@ function createRequestHandler(routeEntry, handler, deps) {
855
885
  body.data
856
886
  );
857
887
  if (response.status < 400) {
888
+ if (routeEntry.siwxEnabled) {
889
+ try {
890
+ await deps.entitlementStore.grant(routeEntry.key, wallet);
891
+ } catch (error) {
892
+ firePluginHook(deps.plugin, "onAlert", pluginCtx, {
893
+ level: "warn",
894
+ message: `Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`,
895
+ route: routeEntry.key
896
+ });
897
+ }
898
+ }
858
899
  const receiptResponse = mppResult.withReceipt(response);
859
900
  finalize(receiptResponse, rawResult, meta, pluginCtx, body.data);
860
901
  return receiptResponse;
@@ -979,6 +1020,18 @@ async function build402(request, routeEntry, deps, meta, pluginCtx, bodyData) {
979
1020
  }
980
1021
  } catch {
981
1022
  }
1023
+ if (routeEntry.siwxEnabled) {
1024
+ try {
1025
+ const siwxExtension = await buildSIWXExtension();
1026
+ if (siwxExtension && typeof siwxExtension === "object" && !Array.isArray(siwxExtension)) {
1027
+ extensions = {
1028
+ ...extensions ?? {},
1029
+ ...siwxExtension
1030
+ };
1031
+ }
1032
+ } catch {
1033
+ }
1034
+ }
982
1035
  if (routeEntry.protocols.includes("x402") && deps.x402Server) {
983
1036
  try {
984
1037
  const payTo = await resolvePayTo(routeEntry, request, deps.payeeAddress);
@@ -1083,6 +1136,8 @@ var RouteBuilder = class {
1083
1136
  /** @internal */
1084
1137
  _pricing;
1085
1138
  /** @internal */
1139
+ _siwxEnabled = false;
1140
+ /** @internal */
1086
1141
  _protocols = ["x402"];
1087
1142
  /** @internal */
1088
1143
  _maxPrice;
@@ -1122,15 +1177,14 @@ var RouteBuilder = class {
1122
1177
  return next;
1123
1178
  }
1124
1179
  paid(pricing, options) {
1125
- if (this._authMode === "siwx") {
1126
- throw new Error(
1127
- `route '${this._key}': Cannot combine .paid() and .siwx() on the same route. Paid routes get wallet identity from the payment proof. Use separate routes if you need both payment and SIWX auth.`
1128
- );
1129
- }
1130
1180
  const next = this.fork();
1131
1181
  next._authMode = "paid";
1132
1182
  next._pricing = pricing;
1133
- if (options?.protocols) next._protocols = options.protocols;
1183
+ if (options?.protocols) {
1184
+ next._protocols = options.protocols;
1185
+ } else if (next._protocols.length === 0) {
1186
+ next._protocols = ["x402"];
1187
+ }
1134
1188
  if (options?.maxPrice) next._maxPrice = options.maxPrice;
1135
1189
  if (options?.minPrice) next._minPrice = options.minPrice;
1136
1190
  if (options?.payTo) next._payTo = options.payTo;
@@ -1158,17 +1212,33 @@ var RouteBuilder = class {
1158
1212
  return next;
1159
1213
  }
1160
1214
  siwx() {
1161
- if (this._authMode === "paid") {
1215
+ if (this._authMode === "unprotected") {
1162
1216
  throw new Error(
1163
- `route '${this._key}': Cannot combine .paid() and .siwx() on the same route. Paid routes get wallet identity from the payment proof. Use separate routes if you need both payment and SIWX auth.`
1217
+ `route '${this._key}': Cannot combine .unprotected() and .siwx() on the same route.`
1218
+ );
1219
+ }
1220
+ if (this._apiKeyResolver) {
1221
+ throw new Error(
1222
+ `route '${this._key}': Combining .siwx() and .apiKey() is not supported on the same route.`
1164
1223
  );
1165
1224
  }
1166
1225
  const next = this.fork();
1226
+ next._siwxEnabled = true;
1227
+ if (next._authMode === "paid" || next._pricing) {
1228
+ next._authMode = "paid";
1229
+ if (next._protocols.length === 0) next._protocols = ["x402"];
1230
+ return next;
1231
+ }
1167
1232
  next._authMode = "siwx";
1168
1233
  next._protocols = [];
1169
1234
  return next;
1170
1235
  }
1171
1236
  apiKey(resolver) {
1237
+ if (this._siwxEnabled) {
1238
+ throw new Error(
1239
+ `route '${this._key}': Combining .apiKey() and .siwx() is not supported on the same route.`
1240
+ );
1241
+ }
1172
1242
  const next = this.fork();
1173
1243
  next._authMode = "apiKey";
1174
1244
  next._apiKeyResolver = resolver;
@@ -1261,6 +1331,7 @@ var RouteBuilder = class {
1261
1331
  const entry = {
1262
1332
  key: this._key,
1263
1333
  authMode: this._authMode,
1334
+ siwxEnabled: this._siwxEnabled,
1264
1335
  pricing: this._pricing,
1265
1336
  protocols: this._protocols,
1266
1337
  bodySchema: this._bodySchema,
@@ -1282,6 +1353,68 @@ var RouteBuilder = class {
1282
1353
  }
1283
1354
  };
1284
1355
 
1356
+ // src/auth/entitlement.ts
1357
+ var MemoryEntitlementStore = class {
1358
+ routeToWallets = /* @__PURE__ */ new Map();
1359
+ async has(route, wallet) {
1360
+ const wallets = this.routeToWallets.get(route);
1361
+ if (!wallets) return false;
1362
+ return wallets.has(wallet.toLowerCase());
1363
+ }
1364
+ async grant(route, wallet) {
1365
+ const normalizedWallet = wallet.toLowerCase();
1366
+ let wallets = this.routeToWallets.get(route);
1367
+ if (!wallets) {
1368
+ wallets = /* @__PURE__ */ new Set();
1369
+ this.routeToWallets.set(route, wallets);
1370
+ }
1371
+ wallets.add(normalizedWallet);
1372
+ }
1373
+ };
1374
+ function detectRedisClientType2(client) {
1375
+ if (!client || typeof client !== "object") {
1376
+ throw new Error(
1377
+ "createRedisEntitlementStore requires a Redis client. Supported: @upstash/redis, ioredis."
1378
+ );
1379
+ }
1380
+ if ("options" in client && "status" in client) return "ioredis";
1381
+ const constructor = client.constructor?.name;
1382
+ if (constructor === "Redis" && "url" in client) return "upstash";
1383
+ if (typeof client.sadd === "function" && typeof client.sismember === "function") {
1384
+ return "upstash";
1385
+ }
1386
+ throw new Error("Unrecognized Redis client for entitlement store.");
1387
+ }
1388
+ function createRedisEntitlementStore(client, options) {
1389
+ const clientType = detectRedisClientType2(client);
1390
+ const prefix = options?.prefix ?? "siwx:entitlement:";
1391
+ return {
1392
+ async has(route, wallet) {
1393
+ const key = `${prefix}${route}`;
1394
+ const normalizedWallet = wallet.toLowerCase();
1395
+ if (clientType === "upstash") {
1396
+ const redis2 = client;
1397
+ const result2 = await redis2.sismember(key, normalizedWallet);
1398
+ return result2 === 1 || result2 === true;
1399
+ }
1400
+ const redis = client;
1401
+ const result = await redis.sismember(key, normalizedWallet);
1402
+ return result === 1;
1403
+ },
1404
+ async grant(route, wallet) {
1405
+ const key = `${prefix}${route}`;
1406
+ const normalizedWallet = wallet.toLowerCase();
1407
+ if (clientType === "upstash") {
1408
+ const redis2 = client;
1409
+ await redis2.sadd(key, normalizedWallet);
1410
+ return;
1411
+ }
1412
+ const redis = client;
1413
+ await redis.sadd(key, normalizedWallet);
1414
+ }
1415
+ };
1416
+ }
1417
+
1285
1418
  // src/discovery/well-known.ts
1286
1419
  import { NextResponse as NextResponse3 } from "next/server";
1287
1420
  function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
@@ -1347,14 +1480,41 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, options) {
1347
1480
  const { createDocument } = await import("zod-openapi");
1348
1481
  const paths = {};
1349
1482
  const tagSet = /* @__PURE__ */ new Set();
1483
+ let requiresSiwxScheme = false;
1484
+ let requiresApiKeyScheme = false;
1350
1485
  for (const [key, entry] of registry.entries()) {
1351
1486
  const apiPath = `/api/${entry.path ?? key}`;
1352
1487
  const method = entry.method.toLowerCase();
1353
1488
  const tag = deriveTag(key);
1354
1489
  tagSet.add(tag);
1355
- paths[apiPath] = { ...paths[apiPath], [method]: buildOperation(key, entry, tag) };
1490
+ const built = buildOperation(key, entry, tag);
1491
+ if (built.requiresSiwxScheme) requiresSiwxScheme = true;
1492
+ if (built.requiresApiKeyScheme) requiresApiKeyScheme = true;
1493
+ paths[apiPath] = { ...paths[apiPath], [method]: built.operation };
1494
+ }
1495
+ const securitySchemes = {};
1496
+ if (requiresSiwxScheme) {
1497
+ securitySchemes.siwx = {
1498
+ type: "apiKey",
1499
+ in: "header",
1500
+ name: "SIGN-IN-WITH-X"
1501
+ };
1502
+ }
1503
+ if (requiresApiKeyScheme) {
1504
+ securitySchemes.apiKey = {
1505
+ type: "apiKey",
1506
+ in: "header",
1507
+ name: "X-API-Key"
1508
+ };
1509
+ }
1510
+ const discoveryMetadata = {};
1511
+ if (options.ownershipProofs && options.ownershipProofs.length > 0) {
1512
+ discoveryMetadata.ownershipProofs = options.ownershipProofs;
1356
1513
  }
1357
- cached = createDocument({
1514
+ if (options.llmsTxtUrl) {
1515
+ discoveryMetadata.llmsTxtUrl = options.llmsTxtUrl;
1516
+ }
1517
+ const openApiDocument = {
1358
1518
  openapi: "3.1.0",
1359
1519
  info: {
1360
1520
  title: options.title,
@@ -1364,8 +1524,17 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, options) {
1364
1524
  },
1365
1525
  servers: [{ url: (options.baseUrl ?? normalizedBase).replace(/\/+$/, "") }],
1366
1526
  tags: Array.from(tagSet).sort().map((name) => ({ name })),
1527
+ ...Object.keys(securitySchemes).length > 0 ? {
1528
+ components: {
1529
+ securitySchemes
1530
+ }
1531
+ } : {},
1532
+ ...Object.keys(discoveryMetadata).length > 0 ? {
1533
+ "x-discovery": discoveryMetadata
1534
+ } : {},
1367
1535
  paths
1368
- });
1536
+ };
1537
+ cached = createDocument(openApiDocument);
1369
1538
  return NextResponse4.json(cached);
1370
1539
  };
1371
1540
  }
@@ -1374,19 +1543,10 @@ function deriveTag(routeKey) {
1374
1543
  }
1375
1544
  function buildOperation(routeKey, entry, tag) {
1376
1545
  const protocols = entry.protocols.length > 0 ? entry.protocols : void 0;
1377
- let price;
1378
- if (typeof entry.pricing === "string") {
1379
- price = entry.pricing;
1380
- } else if (typeof entry.pricing === "object" && "tiers" in entry.pricing) {
1381
- const tierPrices = Object.values(entry.pricing.tiers).map((t) => parseFloat(t.price));
1382
- const min = Math.min(...tierPrices);
1383
- const max = Math.max(...tierPrices);
1384
- price = min === max ? String(min) : `${min}-${max}`;
1385
- } else if (entry.minPrice && entry.maxPrice) {
1386
- price = `${entry.minPrice}-${entry.maxPrice}`;
1387
- } else if (entry.maxPrice) {
1388
- price = entry.maxPrice;
1389
- }
1546
+ const paymentRequired = Boolean(entry.pricing) || entry.authMode === "paid";
1547
+ const requiresSiwxScheme = entry.authMode === "siwx" || Boolean(entry.siwxEnabled);
1548
+ const requiresApiKeyScheme = Boolean(entry.apiKeyResolver) && entry.authMode !== "siwx";
1549
+ const pricingInfo = buildPricingInfo(entry);
1390
1550
  const operation = {
1391
1551
  operationId: routeKey.replace(/\//g, "_"),
1392
1552
  summary: entry.description ?? routeKey,
@@ -1400,19 +1560,29 @@ function buildOperation(routeKey, entry, tag) {
1400
1560
  }
1401
1561
  }
1402
1562
  },
1403
- ...entry.authMode === "paid" && {
1563
+ ...(paymentRequired || requiresSiwxScheme) && {
1404
1564
  "402": {
1405
- description: "Payment Required"
1565
+ description: requiresSiwxScheme ? "Authentication Required" : "Payment Required"
1566
+ }
1567
+ },
1568
+ ...requiresApiKeyScheme && {
1569
+ "401": {
1570
+ description: "Unauthorized"
1406
1571
  }
1407
1572
  }
1408
1573
  }
1409
1574
  };
1410
- if (price !== void 0 || protocols) {
1575
+ if (paymentRequired && (pricingInfo || protocols)) {
1411
1576
  operation["x-payment-info"] = {
1412
- ...price !== void 0 && { price },
1577
+ ...pricingInfo ?? {},
1413
1578
  ...protocols && { protocols }
1414
1579
  };
1415
1580
  }
1581
+ if (requiresSiwxScheme) {
1582
+ operation.security = [{ siwx: [] }];
1583
+ } else if (requiresApiKeyScheme) {
1584
+ operation.security = [{ apiKey: [] }];
1585
+ }
1416
1586
  if (entry.bodySchema) {
1417
1587
  operation.requestBody = {
1418
1588
  required: true,
@@ -1426,13 +1596,57 @@ function buildOperation(routeKey, entry, tag) {
1426
1596
  query: entry.querySchema
1427
1597
  };
1428
1598
  }
1429
- return operation;
1599
+ return {
1600
+ operation,
1601
+ requiresSiwxScheme,
1602
+ requiresApiKeyScheme
1603
+ };
1604
+ }
1605
+ function buildPricingInfo(entry) {
1606
+ if (!entry.pricing) return void 0;
1607
+ if (typeof entry.pricing === "string") {
1608
+ return {
1609
+ pricingMode: "fixed",
1610
+ price: entry.pricing
1611
+ };
1612
+ }
1613
+ if (typeof entry.pricing === "function") {
1614
+ return {
1615
+ pricingMode: "quote",
1616
+ ...entry.minPrice ? { minPrice: entry.minPrice } : {},
1617
+ ...entry.maxPrice ? { maxPrice: entry.maxPrice } : {}
1618
+ };
1619
+ }
1620
+ if ("tiers" in entry.pricing) {
1621
+ const tierPrices = Object.values(entry.pricing.tiers).map((tier) => parseFloat(tier.price));
1622
+ const min = Math.min(...tierPrices);
1623
+ const max = Math.max(...tierPrices);
1624
+ if (Number.isFinite(min) && Number.isFinite(max)) {
1625
+ if (min === max) {
1626
+ return {
1627
+ pricingMode: "fixed",
1628
+ price: String(min)
1629
+ };
1630
+ }
1631
+ return {
1632
+ pricingMode: "range",
1633
+ minPrice: String(min),
1634
+ maxPrice: String(max)
1635
+ };
1636
+ }
1637
+ return {
1638
+ pricingMode: "quote",
1639
+ ...entry.maxPrice ? { maxPrice: entry.maxPrice } : {}
1640
+ };
1641
+ }
1642
+ return void 0;
1430
1643
  }
1431
1644
 
1432
1645
  // src/index.ts
1433
1646
  function createRouter(config) {
1434
1647
  const registry = new RouteRegistry();
1435
1648
  const nonceStore = config.siwx?.nonceStore ?? new MemoryNonceStore();
1649
+ const entitlementStore = config.siwx?.entitlementStore ?? new MemoryEntitlementStore();
1436
1650
  const network = config.network ?? "eip155:8453";
1437
1651
  if (!config.baseUrl) {
1438
1652
  throw new Error(
@@ -1481,6 +1695,7 @@ function createRouter(config) {
1481
1695
  initPromise: Promise.resolve(),
1482
1696
  plugin: config.plugin,
1483
1697
  nonceStore,
1698
+ entitlementStore,
1484
1699
  payeeAddress: config.payeeAddress,
1485
1700
  network,
1486
1701
  mppx: null
@@ -1564,12 +1779,14 @@ function createRouter(config) {
1564
1779
  }
1565
1780
  export {
1566
1781
  HttpError,
1782
+ MemoryEntitlementStore,
1567
1783
  MemoryNonceStore,
1568
1784
  RouteBuilder,
1569
1785
  RouteRegistry,
1570
1786
  SIWX_CHALLENGE_EXPIRY_MS,
1571
1787
  SIWX_ERROR_MESSAGES,
1572
1788
  consolePlugin,
1789
+ createRedisEntitlementStore,
1573
1790
  createRedisNonceStore,
1574
1791
  createRouter
1575
1792
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentcash/router",
3
- "version": "0.6.7",
3
+ "version": "0.7.0",
4
4
  "description": "Unified route builder for Next.js App Router APIs with x402, MPP, SIWX, and API key auth",
5
5
  "type": "module",
6
6
  "exports": {
@@ -48,7 +48,7 @@
48
48
  "@x402/evm": "^2.3.0",
49
49
  "@x402/extensions": "^2.3.0",
50
50
  "eslint": "^10.0.0",
51
- "mppx": "^0.3.4",
51
+ "mppx": "^0.3.9",
52
52
  "next": "^15.0.0",
53
53
  "prettier": "^3.8.1",
54
54
  "react": "^19.0.0",