@agentcash/router 0.7.1 → 1.0.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/dist/index.cjs CHANGED
@@ -90,33 +90,46 @@ module.exports = __toCommonJS(index_exports);
90
90
  // src/registry.ts
91
91
  var RouteRegistry = class {
92
92
  routes = /* @__PURE__ */ new Map();
93
- // Silently overwrites on duplicate key. Next.js module loading order is
94
- // non-deterministic during build discovery stubs and real handlers may
95
- // register the same route key in either order. Last writer wins.
93
+ // Internal map key includes the HTTP method so that POST and DELETE on the
94
+ // same path coexist. Within the same path+method, last-write-wins is still
95
+ // intentional Next.js module loading order is non-deterministic during
96
+ // build and discovery stubs may register the same route in either order.
96
97
  // Prior art: ElysiaJS uses the same pattern (silent overwrite in router.history).
98
+ mapKey(entry) {
99
+ return `${entry.key}:${entry.method}`;
100
+ }
97
101
  register(entry) {
98
- if (this.routes.has(entry.key) && typeof process !== "undefined" && process.env?.["NODE_ENV"] !== "production") {
102
+ const k = this.mapKey(entry);
103
+ if (this.routes.has(k) && typeof process !== "undefined" && process.env?.["NODE_ENV"] !== "production") {
99
104
  console.warn(
100
- `[agentcash/router] route '${entry.key}' registered twice \u2014 overwriting (this is expected for discovery stubs during next build)`
105
+ `[agentcash/router] route '${entry.key}' (${entry.method}) registered twice \u2014 overwriting (this is expected for discovery stubs during next build)`
101
106
  );
102
107
  }
103
- this.routes.set(entry.key, entry);
108
+ this.routes.set(k, entry);
104
109
  }
110
+ // Accepts either a compound key ("site/domain:DELETE") or a path-only key
111
+ // ("site/domain") — path-only returns the first registered method for that path.
105
112
  get(key) {
106
- return this.routes.get(key);
113
+ const direct = this.routes.get(key);
114
+ if (direct) return direct;
115
+ for (const entry of this.routes.values()) {
116
+ if (entry.key === key) return entry;
117
+ }
118
+ return void 0;
107
119
  }
108
120
  entries() {
109
121
  return this.routes.entries();
110
122
  }
111
123
  has(key) {
112
- return this.routes.has(key);
124
+ return this.get(key) !== void 0;
113
125
  }
114
126
  get size() {
115
127
  return this.routes.size;
116
128
  }
117
129
  validate(expectedKeys) {
118
130
  if (!expectedKeys) return;
119
- const missing = expectedKeys.filter((k) => !this.routes.has(k));
131
+ const registeredPathKeys = new Set([...this.routes.values()].map((e) => e.key));
132
+ const missing = expectedKeys.filter((k) => !registeredPathKeys.has(k));
120
133
  if (missing.length > 0) {
121
134
  throw new Error(
122
135
  `route${missing.length > 1 ? "s" : ""} ${missing.map((k) => `'${k}'`).join(", ")} in prices map but not registered \u2014 add to barrel imports`
@@ -1456,7 +1469,15 @@ function createRedisEntitlementStore(client, options) {
1456
1469
 
1457
1470
  // src/discovery/well-known.ts
1458
1471
  var import_server3 = require("next/server");
1459
- function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
1472
+
1473
+ // src/discovery/utils/guidance.ts
1474
+ async function resolveGuidance(discovery) {
1475
+ if (typeof discovery.guidance === "function") return discovery.guidance();
1476
+ return discovery.guidance;
1477
+ }
1478
+
1479
+ // src/discovery/well-known.ts
1480
+ function createWellKnownHandler(registry, baseUrl, pricesKeys, discovery) {
1460
1481
  const normalizedBase = baseUrl.replace(/\/+$/, "");
1461
1482
  let validated = false;
1462
1483
  return async (_request) => {
@@ -1466,19 +1487,14 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
1466
1487
  }
1467
1488
  const x402Set = /* @__PURE__ */ new Set();
1468
1489
  const mppSet = /* @__PURE__ */ new Set();
1469
- const methodHints = options.methodHints ?? "non-default";
1470
- for (const [key, entry] of registry.entries()) {
1471
- const url = `${normalizedBase}/api/${entry.path ?? key}`;
1490
+ const methodHints = discovery.methodHints ?? "non-default";
1491
+ for (const [, entry] of registry.entries()) {
1492
+ const url = `${normalizedBase}/api/${entry.path ?? entry.key}`;
1472
1493
  const resource = toDiscoveryResource(entry.method, url, methodHints);
1473
1494
  if (entry.authMode !== "unprotected") x402Set.add(resource);
1474
1495
  if (entry.protocols.includes("mpp")) mppSet.add(resource);
1475
1496
  }
1476
- let instructions;
1477
- if (typeof options.instructions === "function") {
1478
- instructions = await options.instructions();
1479
- } else if (typeof options.instructions === "string") {
1480
- instructions = options.instructions;
1481
- }
1497
+ const instructions = await resolveGuidance(discovery);
1482
1498
  const body = {
1483
1499
  version: 1,
1484
1500
  resources: Array.from(x402Set)
@@ -1487,11 +1503,11 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
1487
1503
  if (mppResources.length > 0) {
1488
1504
  body.mppResources = mppResources;
1489
1505
  }
1490
- if (options.description) {
1491
- body.description = options.description;
1506
+ if (discovery.description) {
1507
+ body.description = discovery.description;
1492
1508
  }
1493
- if (options.ownershipProofs) {
1494
- body.ownershipProofs = options.ownershipProofs;
1509
+ if (discovery.ownershipProofs) {
1510
+ body.ownershipProofs = discovery.ownershipProofs;
1495
1511
  }
1496
1512
  if (instructions) {
1497
1513
  body.instructions = instructions;
@@ -1514,7 +1530,7 @@ function toDiscoveryResource(method, url, mode) {
1514
1530
 
1515
1531
  // src/discovery/openapi.ts
1516
1532
  var import_server4 = require("next/server");
1517
- function createOpenAPIHandler(registry, baseUrl, pricesKeys, options) {
1533
+ function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
1518
1534
  const normalizedBase = baseUrl.replace(/\/+$/, "");
1519
1535
  let cached = null;
1520
1536
  let validated = false;
@@ -1529,12 +1545,12 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, options) {
1529
1545
  const tagSet = /* @__PURE__ */ new Set();
1530
1546
  let requiresSiwxScheme = false;
1531
1547
  let requiresApiKeyScheme = false;
1532
- for (const [key, entry] of registry.entries()) {
1533
- const apiPath = `/api/${entry.path ?? key}`;
1548
+ for (const [, entry] of registry.entries()) {
1549
+ const apiPath = `/api/${entry.path ?? entry.key}`;
1534
1550
  const method = entry.method.toLowerCase();
1535
- const tag = deriveTag(key);
1551
+ const tag = deriveTag(entry.key);
1536
1552
  tagSet.add(tag);
1537
- const built = buildOperation(key, entry, tag);
1553
+ const built = buildOperation(entry.key, entry, tag);
1538
1554
  if (built.requiresSiwxScheme) requiresSiwxScheme = true;
1539
1555
  if (built.requiresApiKeyScheme) requiresApiKeyScheme = true;
1540
1556
  paths[apiPath] = { ...paths[apiPath], [method]: built.operation };
@@ -1555,21 +1571,20 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, options) {
1555
1571
  };
1556
1572
  }
1557
1573
  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;
1574
+ if (discovery.ownershipProofs && discovery.ownershipProofs.length > 0) {
1575
+ discoveryMetadata.ownershipProofs = discovery.ownershipProofs;
1563
1576
  }
1577
+ const guidance = await resolveGuidance(discovery);
1564
1578
  const openApiDocument = {
1565
1579
  openapi: "3.1.0",
1566
1580
  info: {
1567
- title: options.title,
1568
- description: options.description,
1569
- version: options.version,
1570
- ...options.contact && { contact: options.contact }
1581
+ title: discovery.title,
1582
+ description: discovery.description,
1583
+ version: discovery.version,
1584
+ guidance,
1585
+ ...discovery.contact && { contact: discovery.contact }
1571
1586
  },
1572
- servers: [{ url: (options.baseUrl ?? normalizedBase).replace(/\/+$/, "") }],
1587
+ servers: [{ url: (discovery.serverUrl ?? normalizedBase).replace(/\/+$/, "") }],
1573
1588
  tags: Array.from(tagSet).sort().map((name) => ({ name })),
1574
1589
  ...Object.keys(securitySchemes).length > 0 ? {
1575
1590
  components: {
@@ -1689,6 +1704,20 @@ function buildPricingInfo(entry) {
1689
1704
  return void 0;
1690
1705
  }
1691
1706
 
1707
+ // src/discovery/llms-txt.ts
1708
+ var import_server5 = require("next/server");
1709
+ function createLlmsTxtHandler(discovery) {
1710
+ return async (_request) => {
1711
+ const guidance = await resolveGuidance(discovery) ?? "";
1712
+ return new import_server5.NextResponse(guidance, {
1713
+ headers: {
1714
+ "Content-Type": "text/plain; charset=utf-8",
1715
+ "Access-Control-Allow-Origin": "*"
1716
+ }
1717
+ });
1718
+ };
1719
+ }
1720
+
1692
1721
  // src/index.ts
1693
1722
  function createRouter(config) {
1694
1723
  const registry = new RouteRegistry();
@@ -1817,11 +1846,14 @@ function createRouter(config) {
1817
1846
  }
1818
1847
  return builder;
1819
1848
  },
1820
- wellKnown(options) {
1821
- return createWellKnownHandler(registry, resolvedBaseUrl, pricesKeys, options);
1849
+ wellKnown() {
1850
+ return createWellKnownHandler(registry, resolvedBaseUrl, pricesKeys, config.discovery);
1851
+ },
1852
+ openapi() {
1853
+ return createOpenAPIHandler(registry, resolvedBaseUrl, pricesKeys, config.discovery);
1822
1854
  },
1823
- openapi(options) {
1824
- return createOpenAPIHandler(registry, resolvedBaseUrl, pricesKeys, options);
1855
+ llmsTxt() {
1856
+ return createLlmsTxtHandler(config.discovery);
1825
1857
  },
1826
1858
  monitors() {
1827
1859
  const result = [];
package/dist/index.d.cts CHANGED
@@ -280,6 +280,21 @@ interface RouteEntry {
280
280
  providerConfig?: ProviderConfig;
281
281
  validateFn?: (body: unknown) => void | Promise<void>;
282
282
  }
283
+ interface DiscoveryConfig {
284
+ title: string;
285
+ version: string;
286
+ description?: string;
287
+ contact?: {
288
+ name?: string;
289
+ url?: string;
290
+ };
291
+ ownershipProofs?: string[];
292
+ methodHints?: 'off' | 'non-default' | 'always';
293
+ /** Natural language guidance for agents. Served as wellknown `instructions` and `/llms.txt`. */
294
+ guidance?: string | (() => string | Promise<string>);
295
+ /** Override the OpenAPI `servers` URL. Defaults to `RouterConfig.baseUrl`. Use when the public API hostname differs from the payment realm URL. */
296
+ serverUrl?: string;
297
+ }
283
298
  interface RouterConfig {
284
299
  payeeAddress: string;
285
300
  /**
@@ -329,10 +344,12 @@ interface RouterConfig {
329
344
  * This prevents discovery/openapi drift caused by shorthand internal keys.
330
345
  */
331
346
  strictRoutes?: boolean;
347
+ discovery: DiscoveryConfig;
332
348
  }
333
349
 
334
350
  declare class RouteRegistry {
335
351
  private routes;
352
+ private mapKey;
336
353
  register(entry: RouteEntry): void;
337
354
  get(key: string): RouteEntry | undefined;
338
355
  entries(): IterableIterator<[string, RouteEntry]>;
@@ -341,34 +358,6 @@ declare class RouteRegistry {
341
358
  validate(expectedKeys?: string[]): void;
342
359
  }
343
360
 
344
- interface WellKnownOptions {
345
- description?: string;
346
- instructions?: string | (() => string | Promise<string>);
347
- ownershipProofs?: string[];
348
- /**
349
- * Whether to include explicit HTTP method prefixes in `resources`.
350
- * - `off`: always emit plain URLs.
351
- * - `non-default`: emit `METHOD url` for PUT/PATCH/DELETE routes.
352
- * - `always`: emit `METHOD url` for all routes.
353
- *
354
- * @default 'non-default'
355
- */
356
- methodHints?: 'off' | 'non-default' | 'always';
357
- }
358
-
359
- interface OpenAPIOptions {
360
- title: string;
361
- version: string;
362
- description?: string;
363
- baseUrl?: string;
364
- contact?: {
365
- name?: string;
366
- url?: string;
367
- };
368
- llmsTxtUrl?: string;
369
- ownershipProofs?: string[];
370
- }
371
-
372
361
  interface OrchestrateDeps {
373
362
  x402Server: X402Server | null;
374
363
  initPromise: Promise<void>;
@@ -477,8 +466,9 @@ interface MonitorEntry {
477
466
  }
478
467
  interface ServiceRouter<TPriceKeys extends string = never> {
479
468
  route<K extends string>(keyOrDefinition: K | RouteDefinition<K>): [K] extends [TPriceKeys] ? RouteBuilder<undefined, undefined, true, false, false> : RouteBuilder<undefined, undefined, false, false, false>;
480
- wellKnown(options?: WellKnownOptions): (request: NextRequest) => Promise<NextResponse>;
481
- openapi(options: OpenAPIOptions): (request: NextRequest) => Promise<NextResponse>;
469
+ wellKnown(): (request: NextRequest) => Promise<NextResponse>;
470
+ openapi(): (request: NextRequest) => Promise<NextResponse>;
471
+ llmsTxt(): (request: NextRequest) => Promise<NextResponse>;
482
472
  monitors(): MonitorEntry[];
483
473
  registry: RouteRegistry;
484
474
  }
@@ -486,4 +476,4 @@ declare function createRouter<const P extends Record<string, string> = Record<ne
486
476
  prices?: P;
487
477
  }): ServiceRouter<Extract<keyof P, string>>;
488
478
 
489
- 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 };
479
+ export { type AlertEvent, type AlertFn, type AlertLevel, type AuthEvent, type AuthMode, type DiscoveryConfig, 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
@@ -280,6 +280,21 @@ interface RouteEntry {
280
280
  providerConfig?: ProviderConfig;
281
281
  validateFn?: (body: unknown) => void | Promise<void>;
282
282
  }
283
+ interface DiscoveryConfig {
284
+ title: string;
285
+ version: string;
286
+ description?: string;
287
+ contact?: {
288
+ name?: string;
289
+ url?: string;
290
+ };
291
+ ownershipProofs?: string[];
292
+ methodHints?: 'off' | 'non-default' | 'always';
293
+ /** Natural language guidance for agents. Served as wellknown `instructions` and `/llms.txt`. */
294
+ guidance?: string | (() => string | Promise<string>);
295
+ /** Override the OpenAPI `servers` URL. Defaults to `RouterConfig.baseUrl`. Use when the public API hostname differs from the payment realm URL. */
296
+ serverUrl?: string;
297
+ }
283
298
  interface RouterConfig {
284
299
  payeeAddress: string;
285
300
  /**
@@ -329,10 +344,12 @@ interface RouterConfig {
329
344
  * This prevents discovery/openapi drift caused by shorthand internal keys.
330
345
  */
331
346
  strictRoutes?: boolean;
347
+ discovery: DiscoveryConfig;
332
348
  }
333
349
 
334
350
  declare class RouteRegistry {
335
351
  private routes;
352
+ private mapKey;
336
353
  register(entry: RouteEntry): void;
337
354
  get(key: string): RouteEntry | undefined;
338
355
  entries(): IterableIterator<[string, RouteEntry]>;
@@ -341,34 +358,6 @@ declare class RouteRegistry {
341
358
  validate(expectedKeys?: string[]): void;
342
359
  }
343
360
 
344
- interface WellKnownOptions {
345
- description?: string;
346
- instructions?: string | (() => string | Promise<string>);
347
- ownershipProofs?: string[];
348
- /**
349
- * Whether to include explicit HTTP method prefixes in `resources`.
350
- * - `off`: always emit plain URLs.
351
- * - `non-default`: emit `METHOD url` for PUT/PATCH/DELETE routes.
352
- * - `always`: emit `METHOD url` for all routes.
353
- *
354
- * @default 'non-default'
355
- */
356
- methodHints?: 'off' | 'non-default' | 'always';
357
- }
358
-
359
- interface OpenAPIOptions {
360
- title: string;
361
- version: string;
362
- description?: string;
363
- baseUrl?: string;
364
- contact?: {
365
- name?: string;
366
- url?: string;
367
- };
368
- llmsTxtUrl?: string;
369
- ownershipProofs?: string[];
370
- }
371
-
372
361
  interface OrchestrateDeps {
373
362
  x402Server: X402Server | null;
374
363
  initPromise: Promise<void>;
@@ -477,8 +466,9 @@ interface MonitorEntry {
477
466
  }
478
467
  interface ServiceRouter<TPriceKeys extends string = never> {
479
468
  route<K extends string>(keyOrDefinition: K | RouteDefinition<K>): [K] extends [TPriceKeys] ? RouteBuilder<undefined, undefined, true, false, false> : RouteBuilder<undefined, undefined, false, false, false>;
480
- wellKnown(options?: WellKnownOptions): (request: NextRequest) => Promise<NextResponse>;
481
- openapi(options: OpenAPIOptions): (request: NextRequest) => Promise<NextResponse>;
469
+ wellKnown(): (request: NextRequest) => Promise<NextResponse>;
470
+ openapi(): (request: NextRequest) => Promise<NextResponse>;
471
+ llmsTxt(): (request: NextRequest) => Promise<NextResponse>;
482
472
  monitors(): MonitorEntry[];
483
473
  registry: RouteRegistry;
484
474
  }
@@ -486,4 +476,4 @@ declare function createRouter<const P extends Record<string, string> = Record<ne
486
476
  prices?: P;
487
477
  }): ServiceRouter<Extract<keyof P, string>>;
488
478
 
489
- 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 };
479
+ export { type AlertEvent, type AlertFn, type AlertLevel, type AuthEvent, type AuthMode, type DiscoveryConfig, 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
@@ -51,33 +51,46 @@ var init_server = __esm({
51
51
  // src/registry.ts
52
52
  var RouteRegistry = class {
53
53
  routes = /* @__PURE__ */ new Map();
54
- // Silently overwrites on duplicate key. Next.js module loading order is
55
- // non-deterministic during build discovery stubs and real handlers may
56
- // register the same route key in either order. Last writer wins.
54
+ // Internal map key includes the HTTP method so that POST and DELETE on the
55
+ // same path coexist. Within the same path+method, last-write-wins is still
56
+ // intentional Next.js module loading order is non-deterministic during
57
+ // build and discovery stubs may register the same route in either order.
57
58
  // Prior art: ElysiaJS uses the same pattern (silent overwrite in router.history).
59
+ mapKey(entry) {
60
+ return `${entry.key}:${entry.method}`;
61
+ }
58
62
  register(entry) {
59
- if (this.routes.has(entry.key) && typeof process !== "undefined" && process.env?.["NODE_ENV"] !== "production") {
63
+ const k = this.mapKey(entry);
64
+ if (this.routes.has(k) && typeof process !== "undefined" && process.env?.["NODE_ENV"] !== "production") {
60
65
  console.warn(
61
- `[agentcash/router] route '${entry.key}' registered twice \u2014 overwriting (this is expected for discovery stubs during next build)`
66
+ `[agentcash/router] route '${entry.key}' (${entry.method}) registered twice \u2014 overwriting (this is expected for discovery stubs during next build)`
62
67
  );
63
68
  }
64
- this.routes.set(entry.key, entry);
69
+ this.routes.set(k, entry);
65
70
  }
71
+ // Accepts either a compound key ("site/domain:DELETE") or a path-only key
72
+ // ("site/domain") — path-only returns the first registered method for that path.
66
73
  get(key) {
67
- return this.routes.get(key);
74
+ const direct = this.routes.get(key);
75
+ if (direct) return direct;
76
+ for (const entry of this.routes.values()) {
77
+ if (entry.key === key) return entry;
78
+ }
79
+ return void 0;
68
80
  }
69
81
  entries() {
70
82
  return this.routes.entries();
71
83
  }
72
84
  has(key) {
73
- return this.routes.has(key);
85
+ return this.get(key) !== void 0;
74
86
  }
75
87
  get size() {
76
88
  return this.routes.size;
77
89
  }
78
90
  validate(expectedKeys) {
79
91
  if (!expectedKeys) return;
80
- const missing = expectedKeys.filter((k) => !this.routes.has(k));
92
+ const registeredPathKeys = new Set([...this.routes.values()].map((e) => e.key));
93
+ const missing = expectedKeys.filter((k) => !registeredPathKeys.has(k));
81
94
  if (missing.length > 0) {
82
95
  throw new Error(
83
96
  `route${missing.length > 1 ? "s" : ""} ${missing.map((k) => `'${k}'`).join(", ")} in prices map but not registered \u2014 add to barrel imports`
@@ -1417,7 +1430,15 @@ function createRedisEntitlementStore(client, options) {
1417
1430
 
1418
1431
  // src/discovery/well-known.ts
1419
1432
  import { NextResponse as NextResponse3 } from "next/server";
1420
- function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
1433
+
1434
+ // src/discovery/utils/guidance.ts
1435
+ async function resolveGuidance(discovery) {
1436
+ if (typeof discovery.guidance === "function") return discovery.guidance();
1437
+ return discovery.guidance;
1438
+ }
1439
+
1440
+ // src/discovery/well-known.ts
1441
+ function createWellKnownHandler(registry, baseUrl, pricesKeys, discovery) {
1421
1442
  const normalizedBase = baseUrl.replace(/\/+$/, "");
1422
1443
  let validated = false;
1423
1444
  return async (_request) => {
@@ -1427,19 +1448,14 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
1427
1448
  }
1428
1449
  const x402Set = /* @__PURE__ */ new Set();
1429
1450
  const mppSet = /* @__PURE__ */ new Set();
1430
- const methodHints = options.methodHints ?? "non-default";
1431
- for (const [key, entry] of registry.entries()) {
1432
- const url = `${normalizedBase}/api/${entry.path ?? key}`;
1451
+ const methodHints = discovery.methodHints ?? "non-default";
1452
+ for (const [, entry] of registry.entries()) {
1453
+ const url = `${normalizedBase}/api/${entry.path ?? entry.key}`;
1433
1454
  const resource = toDiscoveryResource(entry.method, url, methodHints);
1434
1455
  if (entry.authMode !== "unprotected") x402Set.add(resource);
1435
1456
  if (entry.protocols.includes("mpp")) mppSet.add(resource);
1436
1457
  }
1437
- let instructions;
1438
- if (typeof options.instructions === "function") {
1439
- instructions = await options.instructions();
1440
- } else if (typeof options.instructions === "string") {
1441
- instructions = options.instructions;
1442
- }
1458
+ const instructions = await resolveGuidance(discovery);
1443
1459
  const body = {
1444
1460
  version: 1,
1445
1461
  resources: Array.from(x402Set)
@@ -1448,11 +1464,11 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
1448
1464
  if (mppResources.length > 0) {
1449
1465
  body.mppResources = mppResources;
1450
1466
  }
1451
- if (options.description) {
1452
- body.description = options.description;
1467
+ if (discovery.description) {
1468
+ body.description = discovery.description;
1453
1469
  }
1454
- if (options.ownershipProofs) {
1455
- body.ownershipProofs = options.ownershipProofs;
1470
+ if (discovery.ownershipProofs) {
1471
+ body.ownershipProofs = discovery.ownershipProofs;
1456
1472
  }
1457
1473
  if (instructions) {
1458
1474
  body.instructions = instructions;
@@ -1475,7 +1491,7 @@ function toDiscoveryResource(method, url, mode) {
1475
1491
 
1476
1492
  // src/discovery/openapi.ts
1477
1493
  import { NextResponse as NextResponse4 } from "next/server";
1478
- function createOpenAPIHandler(registry, baseUrl, pricesKeys, options) {
1494
+ function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
1479
1495
  const normalizedBase = baseUrl.replace(/\/+$/, "");
1480
1496
  let cached = null;
1481
1497
  let validated = false;
@@ -1490,12 +1506,12 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, options) {
1490
1506
  const tagSet = /* @__PURE__ */ new Set();
1491
1507
  let requiresSiwxScheme = false;
1492
1508
  let requiresApiKeyScheme = false;
1493
- for (const [key, entry] of registry.entries()) {
1494
- const apiPath = `/api/${entry.path ?? key}`;
1509
+ for (const [, entry] of registry.entries()) {
1510
+ const apiPath = `/api/${entry.path ?? entry.key}`;
1495
1511
  const method = entry.method.toLowerCase();
1496
- const tag = deriveTag(key);
1512
+ const tag = deriveTag(entry.key);
1497
1513
  tagSet.add(tag);
1498
- const built = buildOperation(key, entry, tag);
1514
+ const built = buildOperation(entry.key, entry, tag);
1499
1515
  if (built.requiresSiwxScheme) requiresSiwxScheme = true;
1500
1516
  if (built.requiresApiKeyScheme) requiresApiKeyScheme = true;
1501
1517
  paths[apiPath] = { ...paths[apiPath], [method]: built.operation };
@@ -1516,21 +1532,20 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, options) {
1516
1532
  };
1517
1533
  }
1518
1534
  const discoveryMetadata = {};
1519
- if (options.ownershipProofs && options.ownershipProofs.length > 0) {
1520
- discoveryMetadata.ownershipProofs = options.ownershipProofs;
1521
- }
1522
- if (options.llmsTxtUrl) {
1523
- discoveryMetadata.llmsTxtUrl = options.llmsTxtUrl;
1535
+ if (discovery.ownershipProofs && discovery.ownershipProofs.length > 0) {
1536
+ discoveryMetadata.ownershipProofs = discovery.ownershipProofs;
1524
1537
  }
1538
+ const guidance = await resolveGuidance(discovery);
1525
1539
  const openApiDocument = {
1526
1540
  openapi: "3.1.0",
1527
1541
  info: {
1528
- title: options.title,
1529
- description: options.description,
1530
- version: options.version,
1531
- ...options.contact && { contact: options.contact }
1542
+ title: discovery.title,
1543
+ description: discovery.description,
1544
+ version: discovery.version,
1545
+ guidance,
1546
+ ...discovery.contact && { contact: discovery.contact }
1532
1547
  },
1533
- servers: [{ url: (options.baseUrl ?? normalizedBase).replace(/\/+$/, "") }],
1548
+ servers: [{ url: (discovery.serverUrl ?? normalizedBase).replace(/\/+$/, "") }],
1534
1549
  tags: Array.from(tagSet).sort().map((name) => ({ name })),
1535
1550
  ...Object.keys(securitySchemes).length > 0 ? {
1536
1551
  components: {
@@ -1650,6 +1665,20 @@ function buildPricingInfo(entry) {
1650
1665
  return void 0;
1651
1666
  }
1652
1667
 
1668
+ // src/discovery/llms-txt.ts
1669
+ import { NextResponse as NextResponse5 } from "next/server";
1670
+ function createLlmsTxtHandler(discovery) {
1671
+ return async (_request) => {
1672
+ const guidance = await resolveGuidance(discovery) ?? "";
1673
+ return new NextResponse5(guidance, {
1674
+ headers: {
1675
+ "Content-Type": "text/plain; charset=utf-8",
1676
+ "Access-Control-Allow-Origin": "*"
1677
+ }
1678
+ });
1679
+ };
1680
+ }
1681
+
1653
1682
  // src/index.ts
1654
1683
  function createRouter(config) {
1655
1684
  const registry = new RouteRegistry();
@@ -1778,11 +1807,14 @@ function createRouter(config) {
1778
1807
  }
1779
1808
  return builder;
1780
1809
  },
1781
- wellKnown(options) {
1782
- return createWellKnownHandler(registry, resolvedBaseUrl, pricesKeys, options);
1810
+ wellKnown() {
1811
+ return createWellKnownHandler(registry, resolvedBaseUrl, pricesKeys, config.discovery);
1812
+ },
1813
+ openapi() {
1814
+ return createOpenAPIHandler(registry, resolvedBaseUrl, pricesKeys, config.discovery);
1783
1815
  },
1784
- openapi(options) {
1785
- return createOpenAPIHandler(registry, resolvedBaseUrl, pricesKeys, options);
1816
+ llmsTxt() {
1817
+ return createLlmsTxtHandler(config.discovery);
1786
1818
  },
1787
1819
  monitors() {
1788
1820
  const result = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentcash/router",
3
- "version": "0.7.1",
3
+ "version": "1.0.1",
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": {