@agentcash/router 0.6.8 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -58,6 +58,8 @@ import { createRouter } from '@agentcash/router';
58
58
 
59
59
  export const router = createRouter({
60
60
  payeeAddress: process.env.X402_PAYEE_ADDRESS!,
61
+ baseUrl: process.env.NEXT_PUBLIC_BASE_URL!,
62
+ strictRoutes: true, // recommended
61
63
  });
62
64
  ```
63
65
 
@@ -70,7 +72,7 @@ export const router = createRouter({
70
72
  import { router } from '@/lib/routes';
71
73
  import { searchSchema, searchResponseSchema } from '@/lib/schemas';
72
74
 
73
- export const POST = router.route('search')
75
+ export const POST = router.route({ path: 'search' })
74
76
  .paid('0.01')
75
77
  .body(searchSchema)
76
78
  .output(searchResponseSchema)
@@ -81,7 +83,7 @@ export const POST = router.route('search')
81
83
  **SIWX-authenticated route**
82
84
 
83
85
  ```typescript
84
- export const GET = router.route('inbox/status')
86
+ export const GET = router.route({ path: 'inbox/status' })
85
87
  .siwx()
86
88
  .query(statusQuerySchema)
87
89
  .handler(async ({ query, wallet }) => getStatus(query, wallet));
@@ -90,7 +92,7 @@ export const GET = router.route('inbox/status')
90
92
  **Unprotected route**
91
93
 
92
94
  ```typescript
93
- export const GET = router.route('health')
95
+ export const GET = router.route({ path: 'health' })
94
96
  .unprotected()
95
97
  .handler(async () => ({ status: 'ok' }));
96
98
  ```
@@ -101,14 +103,25 @@ export const GET = router.route('health')
101
103
  // app/.well-known/x402/route.ts
102
104
  import { router } from '@/lib/routes';
103
105
  import '@/lib/routes/barrel'; // ensures all routes are imported
104
- export const GET = router.wellKnown();
106
+ export const GET = router.wellKnown({ methodHints: 'non-default' });
105
107
 
106
108
  // app/openapi.json/route.ts
107
109
  import { router } from '@/lib/routes';
108
110
  import '@/lib/routes/barrel';
109
- export const GET = router.openapi({ title: 'My API', version: '1.0.0' });
111
+ export const GET = router.openapi({
112
+ title: 'My API',
113
+ version: '1.0.0',
114
+ llmsTxtUrl: 'https://my-api.dev/llms.txt',
115
+ ownershipProofs: ['did:example:proof'],
116
+ });
110
117
  ```
111
118
 
119
+ OpenAPI output follows the discovery contract:
120
+
121
+ - Paid signaling via `responses.402` + `x-payment-info`
122
+ - Auth signaling via `security` + `components.securitySchemes`
123
+ - Optional top-level metadata via `x-discovery` (`llmsTxtUrl`, `ownershipProofs`)
124
+
112
125
  ## API
113
126
 
114
127
  ### `createRouter(config)`
@@ -118,11 +131,29 @@ Creates a `ServiceRouter` instance.
118
131
  | Option | Type | Default | Description |
119
132
  |--------|------|---------|-------------|
120
133
  | `payeeAddress` | `string` | **required** | Wallet address to receive payments |
134
+ | `baseUrl` | `string` | **required** | Service origin used for discovery/OpenAPI/realm |
121
135
  | `network` | `string` | `'eip155:8453'` | Blockchain network |
122
136
  | `plugin` | `RouterPlugin` | `undefined` | Observability plugin |
123
137
  | `prices` | `Record<string, string>` | `undefined` | Central pricing map (auto-applied) |
124
138
  | `siwx.nonceStore` | `NonceStore` | `MemoryNonceStore` | Custom nonce store |
125
139
  | `mpp` | `{ secretKey, currency, recipient? }` | `undefined` | MPP config |
140
+ | `strictRoutes` | `boolean` | `false` | Enforce `route({ path })` and prevent key/path divergence |
141
+
142
+ ### Path-First Routing
143
+
144
+ Use path-first route definitions to keep runtime, OpenAPI, and discovery aligned:
145
+
146
+ ```typescript
147
+ router.route({ path: 'flightaware/airports/id/flights/arrivals', method: 'GET' })
148
+ ```
149
+
150
+ If you need a custom internal key (legacy pricing map), you can pass:
151
+
152
+ ```typescript
153
+ router.route({ path: 'public/path', key: 'legacy/key' })
154
+ ```
155
+
156
+ In `strictRoutes` mode, custom keys are rejected to prevent discovery drift.
126
157
 
127
158
  ### Route Builder
128
159
 
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") {
1255
+ throw new Error(
1256
+ `route '${this._key}': Cannot combine .unprotected() and .siwx() on the same route.`
1257
+ );
1258
+ }
1259
+ if (this._apiKeyResolver) {
1199
1260
  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.`
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 = {}) {
@@ -1331,10 +1466,12 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
1331
1466
  }
1332
1467
  const x402Set = /* @__PURE__ */ new Set();
1333
1468
  const mppSet = /* @__PURE__ */ new Set();
1469
+ const methodHints = options.methodHints ?? "non-default";
1334
1470
  for (const [key, entry] of registry.entries()) {
1335
1471
  const url = `${normalizedBase}/api/${entry.path ?? key}`;
1336
- if (entry.authMode !== "unprotected") x402Set.add(url);
1337
- if (entry.protocols.includes("mpp")) mppSet.add(url);
1472
+ const resource = toDiscoveryResource(entry.method, url, methodHints);
1473
+ if (entry.authMode !== "unprotected") x402Set.add(resource);
1474
+ if (entry.protocols.includes("mpp")) mppSet.add(resource);
1338
1475
  }
1339
1476
  let instructions;
1340
1477
  if (typeof options.instructions === "function") {
@@ -1368,6 +1505,12 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
1368
1505
  });
1369
1506
  };
1370
1507
  }
1508
+ function toDiscoveryResource(method, url, mode) {
1509
+ if (mode === "off") return url;
1510
+ if (mode === "always") return `${method} ${url}`;
1511
+ const isDefaultProbeMethod = method === "GET" || method === "POST";
1512
+ return isDefaultProbeMethod ? url : `${method} ${url}`;
1513
+ }
1371
1514
 
1372
1515
  // src/discovery/openapi.ts
1373
1516
  var import_server4 = require("next/server");
@@ -1384,14 +1527,41 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, options) {
1384
1527
  const { createDocument } = await import("zod-openapi");
1385
1528
  const paths = {};
1386
1529
  const tagSet = /* @__PURE__ */ new Set();
1530
+ let requiresSiwxScheme = false;
1531
+ let requiresApiKeyScheme = false;
1387
1532
  for (const [key, entry] of registry.entries()) {
1388
1533
  const apiPath = `/api/${entry.path ?? key}`;
1389
1534
  const method = entry.method.toLowerCase();
1390
1535
  const tag = deriveTag(key);
1391
1536
  tagSet.add(tag);
1392
- paths[apiPath] = { ...paths[apiPath], [method]: buildOperation(key, entry, tag) };
1537
+ const built = buildOperation(key, entry, tag);
1538
+ if (built.requiresSiwxScheme) requiresSiwxScheme = true;
1539
+ if (built.requiresApiKeyScheme) requiresApiKeyScheme = true;
1540
+ paths[apiPath] = { ...paths[apiPath], [method]: built.operation };
1541
+ }
1542
+ const securitySchemes = {};
1543
+ if (requiresSiwxScheme) {
1544
+ securitySchemes.siwx = {
1545
+ type: "apiKey",
1546
+ in: "header",
1547
+ name: "SIGN-IN-WITH-X"
1548
+ };
1549
+ }
1550
+ if (requiresApiKeyScheme) {
1551
+ securitySchemes.apiKey = {
1552
+ type: "apiKey",
1553
+ in: "header",
1554
+ name: "X-API-Key"
1555
+ };
1393
1556
  }
1394
- cached = createDocument({
1557
+ const discoveryMetadata = {};
1558
+ if (options.ownershipProofs && options.ownershipProofs.length > 0) {
1559
+ discoveryMetadata.ownershipProofs = options.ownershipProofs;
1560
+ }
1561
+ if (options.llmsTxtUrl) {
1562
+ discoveryMetadata.llmsTxtUrl = options.llmsTxtUrl;
1563
+ }
1564
+ const openApiDocument = {
1395
1565
  openapi: "3.1.0",
1396
1566
  info: {
1397
1567
  title: options.title,
@@ -1401,8 +1571,17 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, options) {
1401
1571
  },
1402
1572
  servers: [{ url: (options.baseUrl ?? normalizedBase).replace(/\/+$/, "") }],
1403
1573
  tags: Array.from(tagSet).sort().map((name) => ({ name })),
1574
+ ...Object.keys(securitySchemes).length > 0 ? {
1575
+ components: {
1576
+ securitySchemes
1577
+ }
1578
+ } : {},
1579
+ ...Object.keys(discoveryMetadata).length > 0 ? {
1580
+ "x-discovery": discoveryMetadata
1581
+ } : {},
1404
1582
  paths
1405
- });
1583
+ };
1584
+ cached = createDocument(openApiDocument);
1406
1585
  return import_server4.NextResponse.json(cached);
1407
1586
  };
1408
1587
  }
@@ -1411,19 +1590,10 @@ function deriveTag(routeKey) {
1411
1590
  }
1412
1591
  function buildOperation(routeKey, entry, tag) {
1413
1592
  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
- }
1593
+ const paymentRequired = Boolean(entry.pricing) || entry.authMode === "paid";
1594
+ const requiresSiwxScheme = entry.authMode === "siwx" || Boolean(entry.siwxEnabled);
1595
+ const requiresApiKeyScheme = Boolean(entry.apiKeyResolver) && entry.authMode !== "siwx";
1596
+ const pricingInfo = buildPricingInfo(entry);
1427
1597
  const operation = {
1428
1598
  operationId: routeKey.replace(/\//g, "_"),
1429
1599
  summary: entry.description ?? routeKey,
@@ -1437,19 +1607,29 @@ function buildOperation(routeKey, entry, tag) {
1437
1607
  }
1438
1608
  }
1439
1609
  },
1440
- ...entry.authMode === "paid" && {
1610
+ ...(paymentRequired || requiresSiwxScheme) && {
1441
1611
  "402": {
1442
- description: "Payment Required"
1612
+ description: requiresSiwxScheme ? "Authentication Required" : "Payment Required"
1613
+ }
1614
+ },
1615
+ ...requiresApiKeyScheme && {
1616
+ "401": {
1617
+ description: "Unauthorized"
1443
1618
  }
1444
1619
  }
1445
1620
  }
1446
1621
  };
1447
- if (price !== void 0 || protocols) {
1622
+ if (paymentRequired && (pricingInfo || protocols)) {
1448
1623
  operation["x-payment-info"] = {
1449
- ...price !== void 0 && { price },
1624
+ ...pricingInfo ?? {},
1450
1625
  ...protocols && { protocols }
1451
1626
  };
1452
1627
  }
1628
+ if (requiresSiwxScheme) {
1629
+ operation.security = [{ siwx: [] }];
1630
+ } else if (requiresApiKeyScheme) {
1631
+ operation.security = [{ apiKey: [] }];
1632
+ }
1453
1633
  if (entry.bodySchema) {
1454
1634
  operation.requestBody = {
1455
1635
  required: true,
@@ -1463,13 +1643,57 @@ function buildOperation(routeKey, entry, tag) {
1463
1643
  query: entry.querySchema
1464
1644
  };
1465
1645
  }
1466
- return operation;
1646
+ return {
1647
+ operation,
1648
+ requiresSiwxScheme,
1649
+ requiresApiKeyScheme
1650
+ };
1651
+ }
1652
+ function buildPricingInfo(entry) {
1653
+ if (!entry.pricing) return void 0;
1654
+ if (typeof entry.pricing === "string") {
1655
+ return {
1656
+ pricingMode: "fixed",
1657
+ price: entry.pricing
1658
+ };
1659
+ }
1660
+ if (typeof entry.pricing === "function") {
1661
+ return {
1662
+ pricingMode: "quote",
1663
+ ...entry.minPrice ? { minPrice: entry.minPrice } : {},
1664
+ ...entry.maxPrice ? { maxPrice: entry.maxPrice } : {}
1665
+ };
1666
+ }
1667
+ if ("tiers" in entry.pricing) {
1668
+ const tierPrices = Object.values(entry.pricing.tiers).map((tier) => parseFloat(tier.price));
1669
+ const min = Math.min(...tierPrices);
1670
+ const max = Math.max(...tierPrices);
1671
+ if (Number.isFinite(min) && Number.isFinite(max)) {
1672
+ if (min === max) {
1673
+ return {
1674
+ pricingMode: "fixed",
1675
+ price: String(min)
1676
+ };
1677
+ }
1678
+ return {
1679
+ pricingMode: "range",
1680
+ minPrice: String(min),
1681
+ maxPrice: String(max)
1682
+ };
1683
+ }
1684
+ return {
1685
+ pricingMode: "quote",
1686
+ ...entry.maxPrice ? { maxPrice: entry.maxPrice } : {}
1687
+ };
1688
+ }
1689
+ return void 0;
1467
1690
  }
1468
1691
 
1469
1692
  // src/index.ts
1470
1693
  function createRouter(config) {
1471
1694
  const registry = new RouteRegistry();
1472
1695
  const nonceStore = config.siwx?.nonceStore ?? new MemoryNonceStore();
1696
+ const entitlementStore = config.siwx?.entitlementStore ?? new MemoryEntitlementStore();
1473
1697
  const network = config.network ?? "eip155:8453";
1474
1698
  if (!config.baseUrl) {
1475
1699
  throw new Error(
@@ -1518,6 +1742,7 @@ function createRouter(config) {
1518
1742
  initPromise: Promise.resolve(),
1519
1743
  plugin: config.plugin,
1520
1744
  nonceStore,
1745
+ entitlementStore,
1521
1746
  payeeAddress: config.payeeAddress,
1522
1747
  network,
1523
1748
  mppx: null
@@ -1566,8 +1791,26 @@ function createRouter(config) {
1566
1791
  })();
1567
1792
  const pricesKeys = config.prices ? Object.keys(config.prices) : void 0;
1568
1793
  return {
1569
- route(key) {
1570
- const builder = new RouteBuilder(key, registry, deps);
1794
+ route(keyOrDefinition) {
1795
+ const isDefinition = typeof keyOrDefinition !== "string";
1796
+ if (config.strictRoutes && !isDefinition) {
1797
+ throw new Error(
1798
+ "[router] strictRoutes=true requires route({ path }) form. Replace route('my/key') with route({ path: 'my/key' })."
1799
+ );
1800
+ }
1801
+ const definition = isDefinition ? keyOrDefinition : { path: keyOrDefinition, key: keyOrDefinition };
1802
+ const normalizedPath = normalizePath(definition.path);
1803
+ const key = definition.key ?? normalizedPath;
1804
+ if (config.strictRoutes && definition.key && definition.key !== definition.path) {
1805
+ throw new Error(
1806
+ `[router] strictRoutes=true forbids key/path divergence for route '${definition.path}'. Remove custom \`key\` or make it equal to \`path\`.`
1807
+ );
1808
+ }
1809
+ let builder = new RouteBuilder(key, registry, deps);
1810
+ builder = builder.path(normalizedPath);
1811
+ if (definition.method) {
1812
+ builder = builder.method(definition.method);
1813
+ }
1571
1814
  if (config.prices && key in config.prices) {
1572
1815
  const options = config.protocols ? { protocols: config.protocols } : void 0;
1573
1816
  return builder.paid(config.prices[key], options);
@@ -1599,15 +1842,23 @@ function createRouter(config) {
1599
1842
  registry
1600
1843
  };
1601
1844
  }
1845
+ function normalizePath(path) {
1846
+ let normalized = path.trim();
1847
+ normalized = normalized.replace(/^\/+/, "");
1848
+ normalized = normalized.replace(/^api\/+/, "");
1849
+ return normalized.replace(/\/+$/, "");
1850
+ }
1602
1851
  // Annotate the CommonJS export names for ESM import in node:
1603
1852
  0 && (module.exports = {
1604
1853
  HttpError,
1854
+ MemoryEntitlementStore,
1605
1855
  MemoryNonceStore,
1606
1856
  RouteBuilder,
1607
1857
  RouteRegistry,
1608
1858
  SIWX_CHALLENGE_EXPIRY_MS,
1609
1859
  SIWX_ERROR_MESSAGES,
1610
1860
  consolePlugin,
1861
+ createRedisEntitlementStore,
1611
1862
  createRedisNonceStore,
1612
1863
  createRouter
1613
1864
  });