@agentcash/router 0.7.0 → 1.0.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 +24 -4
- package/dist/index.cjs +81 -30
- package/dist/index.d.cts +52 -24
- package/dist/index.d.ts +52 -24
- package/dist/index.js +81 -30
- package/package.json +2 -2
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,7 +103,7 @@ 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';
|
|
@@ -129,11 +131,29 @@ Creates a `ServiceRouter` instance.
|
|
|
129
131
|
| Option | Type | Default | Description |
|
|
130
132
|
|--------|------|---------|-------------|
|
|
131
133
|
| `payeeAddress` | `string` | **required** | Wallet address to receive payments |
|
|
134
|
+
| `baseUrl` | `string` | **required** | Service origin used for discovery/OpenAPI/realm |
|
|
132
135
|
| `network` | `string` | `'eip155:8453'` | Blockchain network |
|
|
133
136
|
| `plugin` | `RouterPlugin` | `undefined` | Observability plugin |
|
|
134
137
|
| `prices` | `Record<string, string>` | `undefined` | Central pricing map (auto-applied) |
|
|
135
138
|
| `siwx.nonceStore` | `NonceStore` | `MemoryNonceStore` | Custom nonce store |
|
|
136
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.
|
|
137
157
|
|
|
138
158
|
### Route Builder
|
|
139
159
|
|
package/dist/index.cjs
CHANGED
|
@@ -1456,7 +1456,15 @@ function createRedisEntitlementStore(client, options) {
|
|
|
1456
1456
|
|
|
1457
1457
|
// src/discovery/well-known.ts
|
|
1458
1458
|
var import_server3 = require("next/server");
|
|
1459
|
-
|
|
1459
|
+
|
|
1460
|
+
// src/discovery/utils/guidance.ts
|
|
1461
|
+
async function resolveGuidance(discovery) {
|
|
1462
|
+
if (typeof discovery.guidance === "function") return discovery.guidance();
|
|
1463
|
+
return discovery.guidance;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// src/discovery/well-known.ts
|
|
1467
|
+
function createWellKnownHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
1460
1468
|
const normalizedBase = baseUrl.replace(/\/+$/, "");
|
|
1461
1469
|
let validated = false;
|
|
1462
1470
|
return async (_request) => {
|
|
@@ -1466,17 +1474,14 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
|
|
|
1466
1474
|
}
|
|
1467
1475
|
const x402Set = /* @__PURE__ */ new Set();
|
|
1468
1476
|
const mppSet = /* @__PURE__ */ new Set();
|
|
1477
|
+
const methodHints = discovery.methodHints ?? "non-default";
|
|
1469
1478
|
for (const [key, entry] of registry.entries()) {
|
|
1470
1479
|
const url = `${normalizedBase}/api/${entry.path ?? key}`;
|
|
1471
|
-
|
|
1472
|
-
if (entry.
|
|
1473
|
-
|
|
1474
|
-
let instructions;
|
|
1475
|
-
if (typeof options.instructions === "function") {
|
|
1476
|
-
instructions = await options.instructions();
|
|
1477
|
-
} else if (typeof options.instructions === "string") {
|
|
1478
|
-
instructions = options.instructions;
|
|
1480
|
+
const resource = toDiscoveryResource(entry.method, url, methodHints);
|
|
1481
|
+
if (entry.authMode !== "unprotected") x402Set.add(resource);
|
|
1482
|
+
if (entry.protocols.includes("mpp")) mppSet.add(resource);
|
|
1479
1483
|
}
|
|
1484
|
+
const instructions = await resolveGuidance(discovery);
|
|
1480
1485
|
const body = {
|
|
1481
1486
|
version: 1,
|
|
1482
1487
|
resources: Array.from(x402Set)
|
|
@@ -1485,11 +1490,11 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
|
|
|
1485
1490
|
if (mppResources.length > 0) {
|
|
1486
1491
|
body.mppResources = mppResources;
|
|
1487
1492
|
}
|
|
1488
|
-
if (
|
|
1489
|
-
body.description =
|
|
1493
|
+
if (discovery.description) {
|
|
1494
|
+
body.description = discovery.description;
|
|
1490
1495
|
}
|
|
1491
|
-
if (
|
|
1492
|
-
body.ownershipProofs =
|
|
1496
|
+
if (discovery.ownershipProofs) {
|
|
1497
|
+
body.ownershipProofs = discovery.ownershipProofs;
|
|
1493
1498
|
}
|
|
1494
1499
|
if (instructions) {
|
|
1495
1500
|
body.instructions = instructions;
|
|
@@ -1503,10 +1508,16 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
|
|
|
1503
1508
|
});
|
|
1504
1509
|
};
|
|
1505
1510
|
}
|
|
1511
|
+
function toDiscoveryResource(method, url, mode) {
|
|
1512
|
+
if (mode === "off") return url;
|
|
1513
|
+
if (mode === "always") return `${method} ${url}`;
|
|
1514
|
+
const isDefaultProbeMethod = method === "GET" || method === "POST";
|
|
1515
|
+
return isDefaultProbeMethod ? url : `${method} ${url}`;
|
|
1516
|
+
}
|
|
1506
1517
|
|
|
1507
1518
|
// src/discovery/openapi.ts
|
|
1508
1519
|
var import_server4 = require("next/server");
|
|
1509
|
-
function createOpenAPIHandler(registry, baseUrl, pricesKeys,
|
|
1520
|
+
function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
1510
1521
|
const normalizedBase = baseUrl.replace(/\/+$/, "");
|
|
1511
1522
|
let cached = null;
|
|
1512
1523
|
let validated = false;
|
|
@@ -1547,21 +1558,20 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, options) {
|
|
|
1547
1558
|
};
|
|
1548
1559
|
}
|
|
1549
1560
|
const discoveryMetadata = {};
|
|
1550
|
-
if (
|
|
1551
|
-
discoveryMetadata.ownershipProofs =
|
|
1552
|
-
}
|
|
1553
|
-
if (options.llmsTxtUrl) {
|
|
1554
|
-
discoveryMetadata.llmsTxtUrl = options.llmsTxtUrl;
|
|
1561
|
+
if (discovery.ownershipProofs && discovery.ownershipProofs.length > 0) {
|
|
1562
|
+
discoveryMetadata.ownershipProofs = discovery.ownershipProofs;
|
|
1555
1563
|
}
|
|
1564
|
+
const guidance = await resolveGuidance(discovery);
|
|
1556
1565
|
const openApiDocument = {
|
|
1557
1566
|
openapi: "3.1.0",
|
|
1558
1567
|
info: {
|
|
1559
|
-
title:
|
|
1560
|
-
description:
|
|
1561
|
-
version:
|
|
1562
|
-
|
|
1568
|
+
title: discovery.title,
|
|
1569
|
+
description: discovery.description,
|
|
1570
|
+
version: discovery.version,
|
|
1571
|
+
guidance,
|
|
1572
|
+
...discovery.contact && { contact: discovery.contact }
|
|
1563
1573
|
},
|
|
1564
|
-
servers: [{ url: (
|
|
1574
|
+
servers: [{ url: (discovery.serverUrl ?? normalizedBase).replace(/\/+$/, "") }],
|
|
1565
1575
|
tags: Array.from(tagSet).sort().map((name) => ({ name })),
|
|
1566
1576
|
...Object.keys(securitySchemes).length > 0 ? {
|
|
1567
1577
|
components: {
|
|
@@ -1681,6 +1691,20 @@ function buildPricingInfo(entry) {
|
|
|
1681
1691
|
return void 0;
|
|
1682
1692
|
}
|
|
1683
1693
|
|
|
1694
|
+
// src/discovery/llms-txt.ts
|
|
1695
|
+
var import_server5 = require("next/server");
|
|
1696
|
+
function createLlmsTxtHandler(discovery) {
|
|
1697
|
+
return async (_request) => {
|
|
1698
|
+
const guidance = await resolveGuidance(discovery) ?? "";
|
|
1699
|
+
return new import_server5.NextResponse(guidance, {
|
|
1700
|
+
headers: {
|
|
1701
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
1702
|
+
"Access-Control-Allow-Origin": "*"
|
|
1703
|
+
}
|
|
1704
|
+
});
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1684
1708
|
// src/index.ts
|
|
1685
1709
|
function createRouter(config) {
|
|
1686
1710
|
const registry = new RouteRegistry();
|
|
@@ -1783,19 +1807,40 @@ function createRouter(config) {
|
|
|
1783
1807
|
})();
|
|
1784
1808
|
const pricesKeys = config.prices ? Object.keys(config.prices) : void 0;
|
|
1785
1809
|
return {
|
|
1786
|
-
route(
|
|
1787
|
-
const
|
|
1810
|
+
route(keyOrDefinition) {
|
|
1811
|
+
const isDefinition = typeof keyOrDefinition !== "string";
|
|
1812
|
+
if (config.strictRoutes && !isDefinition) {
|
|
1813
|
+
throw new Error(
|
|
1814
|
+
"[router] strictRoutes=true requires route({ path }) form. Replace route('my/key') with route({ path: 'my/key' })."
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
const definition = isDefinition ? keyOrDefinition : { path: keyOrDefinition, key: keyOrDefinition };
|
|
1818
|
+
const normalizedPath = normalizePath(definition.path);
|
|
1819
|
+
const key = definition.key ?? normalizedPath;
|
|
1820
|
+
if (config.strictRoutes && definition.key && definition.key !== definition.path) {
|
|
1821
|
+
throw new Error(
|
|
1822
|
+
`[router] strictRoutes=true forbids key/path divergence for route '${definition.path}'. Remove custom \`key\` or make it equal to \`path\`.`
|
|
1823
|
+
);
|
|
1824
|
+
}
|
|
1825
|
+
let builder = new RouteBuilder(key, registry, deps);
|
|
1826
|
+
builder = builder.path(normalizedPath);
|
|
1827
|
+
if (definition.method) {
|
|
1828
|
+
builder = builder.method(definition.method);
|
|
1829
|
+
}
|
|
1788
1830
|
if (config.prices && key in config.prices) {
|
|
1789
1831
|
const options = config.protocols ? { protocols: config.protocols } : void 0;
|
|
1790
1832
|
return builder.paid(config.prices[key], options);
|
|
1791
1833
|
}
|
|
1792
1834
|
return builder;
|
|
1793
1835
|
},
|
|
1794
|
-
wellKnown(
|
|
1795
|
-
return createWellKnownHandler(registry, resolvedBaseUrl, pricesKeys,
|
|
1836
|
+
wellKnown() {
|
|
1837
|
+
return createWellKnownHandler(registry, resolvedBaseUrl, pricesKeys, config.discovery);
|
|
1796
1838
|
},
|
|
1797
|
-
openapi(
|
|
1798
|
-
return createOpenAPIHandler(registry, resolvedBaseUrl, pricesKeys,
|
|
1839
|
+
openapi() {
|
|
1840
|
+
return createOpenAPIHandler(registry, resolvedBaseUrl, pricesKeys, config.discovery);
|
|
1841
|
+
},
|
|
1842
|
+
llmsTxt() {
|
|
1843
|
+
return createLlmsTxtHandler(config.discovery);
|
|
1799
1844
|
},
|
|
1800
1845
|
monitors() {
|
|
1801
1846
|
const result = [];
|
|
@@ -1816,6 +1861,12 @@ function createRouter(config) {
|
|
|
1816
1861
|
registry
|
|
1817
1862
|
};
|
|
1818
1863
|
}
|
|
1864
|
+
function normalizePath(path) {
|
|
1865
|
+
let normalized = path.trim();
|
|
1866
|
+
normalized = normalized.replace(/^\/+/, "");
|
|
1867
|
+
normalized = normalized.replace(/^api\/+/, "");
|
|
1868
|
+
return normalized.replace(/\/+$/, "");
|
|
1869
|
+
}
|
|
1819
1870
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1820
1871
|
0 && (module.exports = {
|
|
1821
1872
|
HttpError,
|
package/dist/index.d.cts
CHANGED
|
@@ -185,6 +185,26 @@ interface X402Server {
|
|
|
185
185
|
}
|
|
186
186
|
type ProtocolType = 'x402' | 'mpp';
|
|
187
187
|
type AuthMode = 'paid' | 'siwx' | 'apiKey' | 'unprotected';
|
|
188
|
+
type RouteMethod = 'GET' | 'POST' | 'DELETE' | 'PUT' | 'PATCH';
|
|
189
|
+
interface RouteDefinition<K extends string = string> {
|
|
190
|
+
/**
|
|
191
|
+
* Public API path segment (without `/api/` prefix).
|
|
192
|
+
* Example: `flightaware/airports/id/flights/arrivals`
|
|
193
|
+
*/
|
|
194
|
+
path: K;
|
|
195
|
+
/**
|
|
196
|
+
* Internal route ID for pricing maps / analytics. Defaults to `path`.
|
|
197
|
+
*
|
|
198
|
+
* In `strictRoutes` mode, custom keys are disallowed to prevent discovery
|
|
199
|
+
* drift between internal IDs and advertised paths.
|
|
200
|
+
*/
|
|
201
|
+
key?: string;
|
|
202
|
+
/**
|
|
203
|
+
* Optional explicit method. If omitted, defaults to builder behavior
|
|
204
|
+
* (`POST`, or `GET` when `.query()` is used).
|
|
205
|
+
*/
|
|
206
|
+
method?: RouteMethod;
|
|
207
|
+
}
|
|
188
208
|
interface TierConfig {
|
|
189
209
|
price: string;
|
|
190
210
|
label?: string;
|
|
@@ -251,7 +271,7 @@ interface RouteEntry {
|
|
|
251
271
|
outputSchema?: ZodType;
|
|
252
272
|
description?: string;
|
|
253
273
|
path?: string;
|
|
254
|
-
method:
|
|
274
|
+
method: RouteMethod;
|
|
255
275
|
maxPrice?: string;
|
|
256
276
|
minPrice?: string;
|
|
257
277
|
payTo?: string | ((request: Request) => string | Promise<string>);
|
|
@@ -260,6 +280,21 @@ interface RouteEntry {
|
|
|
260
280
|
providerConfig?: ProviderConfig;
|
|
261
281
|
validateFn?: (body: unknown) => void | Promise<void>;
|
|
262
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
|
+
}
|
|
263
298
|
interface RouterConfig {
|
|
264
299
|
payeeAddress: string;
|
|
265
300
|
/**
|
|
@@ -299,6 +334,17 @@ interface RouterConfig {
|
|
|
299
334
|
* })
|
|
300
335
|
*/
|
|
301
336
|
protocols?: ProtocolType[];
|
|
337
|
+
/**
|
|
338
|
+
* Enforce explicit, path-first route definitions.
|
|
339
|
+
*
|
|
340
|
+
* When enabled:
|
|
341
|
+
* - `.route('key')` is rejected; use `.route({ path })`.
|
|
342
|
+
* - custom `key` differing from `path` is rejected.
|
|
343
|
+
*
|
|
344
|
+
* This prevents discovery/openapi drift caused by shorthand internal keys.
|
|
345
|
+
*/
|
|
346
|
+
strictRoutes?: boolean;
|
|
347
|
+
discovery: DiscoveryConfig;
|
|
302
348
|
}
|
|
303
349
|
|
|
304
350
|
declare class RouteRegistry {
|
|
@@ -311,25 +357,6 @@ declare class RouteRegistry {
|
|
|
311
357
|
validate(expectedKeys?: string[]): void;
|
|
312
358
|
}
|
|
313
359
|
|
|
314
|
-
interface WellKnownOptions {
|
|
315
|
-
description?: string;
|
|
316
|
-
instructions?: string | (() => string | Promise<string>);
|
|
317
|
-
ownershipProofs?: string[];
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
interface OpenAPIOptions {
|
|
321
|
-
title: string;
|
|
322
|
-
version: string;
|
|
323
|
-
description?: string;
|
|
324
|
-
baseUrl?: string;
|
|
325
|
-
contact?: {
|
|
326
|
-
name?: string;
|
|
327
|
-
url?: string;
|
|
328
|
-
};
|
|
329
|
-
llmsTxtUrl?: string;
|
|
330
|
-
ownershipProofs?: string[];
|
|
331
|
-
}
|
|
332
|
-
|
|
333
360
|
interface OrchestrateDeps {
|
|
334
361
|
x402Server: X402Server | null;
|
|
335
362
|
initPromise: Promise<void>;
|
|
@@ -437,9 +464,10 @@ interface MonitorEntry {
|
|
|
437
464
|
critical?: number;
|
|
438
465
|
}
|
|
439
466
|
interface ServiceRouter<TPriceKeys extends string = never> {
|
|
440
|
-
route<K extends string>(
|
|
441
|
-
wellKnown(
|
|
442
|
-
openapi(
|
|
467
|
+
route<K extends string>(keyOrDefinition: K | RouteDefinition<K>): [K] extends [TPriceKeys] ? RouteBuilder<undefined, undefined, true, false, false> : RouteBuilder<undefined, undefined, false, false, false>;
|
|
468
|
+
wellKnown(): (request: NextRequest) => Promise<NextResponse>;
|
|
469
|
+
openapi(): (request: NextRequest) => Promise<NextResponse>;
|
|
470
|
+
llmsTxt(): (request: NextRequest) => Promise<NextResponse>;
|
|
443
471
|
monitors(): MonitorEntry[];
|
|
444
472
|
registry: RouteRegistry;
|
|
445
473
|
}
|
|
@@ -447,4 +475,4 @@ declare function createRouter<const P extends Record<string, string> = Record<ne
|
|
|
447
475
|
prices?: P;
|
|
448
476
|
}): ServiceRouter<Extract<keyof P, string>>;
|
|
449
477
|
|
|
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 };
|
|
478
|
+
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
|
@@ -185,6 +185,26 @@ interface X402Server {
|
|
|
185
185
|
}
|
|
186
186
|
type ProtocolType = 'x402' | 'mpp';
|
|
187
187
|
type AuthMode = 'paid' | 'siwx' | 'apiKey' | 'unprotected';
|
|
188
|
+
type RouteMethod = 'GET' | 'POST' | 'DELETE' | 'PUT' | 'PATCH';
|
|
189
|
+
interface RouteDefinition<K extends string = string> {
|
|
190
|
+
/**
|
|
191
|
+
* Public API path segment (without `/api/` prefix).
|
|
192
|
+
* Example: `flightaware/airports/id/flights/arrivals`
|
|
193
|
+
*/
|
|
194
|
+
path: K;
|
|
195
|
+
/**
|
|
196
|
+
* Internal route ID for pricing maps / analytics. Defaults to `path`.
|
|
197
|
+
*
|
|
198
|
+
* In `strictRoutes` mode, custom keys are disallowed to prevent discovery
|
|
199
|
+
* drift between internal IDs and advertised paths.
|
|
200
|
+
*/
|
|
201
|
+
key?: string;
|
|
202
|
+
/**
|
|
203
|
+
* Optional explicit method. If omitted, defaults to builder behavior
|
|
204
|
+
* (`POST`, or `GET` when `.query()` is used).
|
|
205
|
+
*/
|
|
206
|
+
method?: RouteMethod;
|
|
207
|
+
}
|
|
188
208
|
interface TierConfig {
|
|
189
209
|
price: string;
|
|
190
210
|
label?: string;
|
|
@@ -251,7 +271,7 @@ interface RouteEntry {
|
|
|
251
271
|
outputSchema?: ZodType;
|
|
252
272
|
description?: string;
|
|
253
273
|
path?: string;
|
|
254
|
-
method:
|
|
274
|
+
method: RouteMethod;
|
|
255
275
|
maxPrice?: string;
|
|
256
276
|
minPrice?: string;
|
|
257
277
|
payTo?: string | ((request: Request) => string | Promise<string>);
|
|
@@ -260,6 +280,21 @@ interface RouteEntry {
|
|
|
260
280
|
providerConfig?: ProviderConfig;
|
|
261
281
|
validateFn?: (body: unknown) => void | Promise<void>;
|
|
262
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
|
+
}
|
|
263
298
|
interface RouterConfig {
|
|
264
299
|
payeeAddress: string;
|
|
265
300
|
/**
|
|
@@ -299,6 +334,17 @@ interface RouterConfig {
|
|
|
299
334
|
* })
|
|
300
335
|
*/
|
|
301
336
|
protocols?: ProtocolType[];
|
|
337
|
+
/**
|
|
338
|
+
* Enforce explicit, path-first route definitions.
|
|
339
|
+
*
|
|
340
|
+
* When enabled:
|
|
341
|
+
* - `.route('key')` is rejected; use `.route({ path })`.
|
|
342
|
+
* - custom `key` differing from `path` is rejected.
|
|
343
|
+
*
|
|
344
|
+
* This prevents discovery/openapi drift caused by shorthand internal keys.
|
|
345
|
+
*/
|
|
346
|
+
strictRoutes?: boolean;
|
|
347
|
+
discovery: DiscoveryConfig;
|
|
302
348
|
}
|
|
303
349
|
|
|
304
350
|
declare class RouteRegistry {
|
|
@@ -311,25 +357,6 @@ declare class RouteRegistry {
|
|
|
311
357
|
validate(expectedKeys?: string[]): void;
|
|
312
358
|
}
|
|
313
359
|
|
|
314
|
-
interface WellKnownOptions {
|
|
315
|
-
description?: string;
|
|
316
|
-
instructions?: string | (() => string | Promise<string>);
|
|
317
|
-
ownershipProofs?: string[];
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
interface OpenAPIOptions {
|
|
321
|
-
title: string;
|
|
322
|
-
version: string;
|
|
323
|
-
description?: string;
|
|
324
|
-
baseUrl?: string;
|
|
325
|
-
contact?: {
|
|
326
|
-
name?: string;
|
|
327
|
-
url?: string;
|
|
328
|
-
};
|
|
329
|
-
llmsTxtUrl?: string;
|
|
330
|
-
ownershipProofs?: string[];
|
|
331
|
-
}
|
|
332
|
-
|
|
333
360
|
interface OrchestrateDeps {
|
|
334
361
|
x402Server: X402Server | null;
|
|
335
362
|
initPromise: Promise<void>;
|
|
@@ -437,9 +464,10 @@ interface MonitorEntry {
|
|
|
437
464
|
critical?: number;
|
|
438
465
|
}
|
|
439
466
|
interface ServiceRouter<TPriceKeys extends string = never> {
|
|
440
|
-
route<K extends string>(
|
|
441
|
-
wellKnown(
|
|
442
|
-
openapi(
|
|
467
|
+
route<K extends string>(keyOrDefinition: K | RouteDefinition<K>): [K] extends [TPriceKeys] ? RouteBuilder<undefined, undefined, true, false, false> : RouteBuilder<undefined, undefined, false, false, false>;
|
|
468
|
+
wellKnown(): (request: NextRequest) => Promise<NextResponse>;
|
|
469
|
+
openapi(): (request: NextRequest) => Promise<NextResponse>;
|
|
470
|
+
llmsTxt(): (request: NextRequest) => Promise<NextResponse>;
|
|
443
471
|
monitors(): MonitorEntry[];
|
|
444
472
|
registry: RouteRegistry;
|
|
445
473
|
}
|
|
@@ -447,4 +475,4 @@ declare function createRouter<const P extends Record<string, string> = Record<ne
|
|
|
447
475
|
prices?: P;
|
|
448
476
|
}): ServiceRouter<Extract<keyof P, string>>;
|
|
449
477
|
|
|
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 };
|
|
478
|
+
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
|
@@ -1417,7 +1417,15 @@ function createRedisEntitlementStore(client, options) {
|
|
|
1417
1417
|
|
|
1418
1418
|
// src/discovery/well-known.ts
|
|
1419
1419
|
import { NextResponse as NextResponse3 } from "next/server";
|
|
1420
|
-
|
|
1420
|
+
|
|
1421
|
+
// src/discovery/utils/guidance.ts
|
|
1422
|
+
async function resolveGuidance(discovery) {
|
|
1423
|
+
if (typeof discovery.guidance === "function") return discovery.guidance();
|
|
1424
|
+
return discovery.guidance;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// src/discovery/well-known.ts
|
|
1428
|
+
function createWellKnownHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
1421
1429
|
const normalizedBase = baseUrl.replace(/\/+$/, "");
|
|
1422
1430
|
let validated = false;
|
|
1423
1431
|
return async (_request) => {
|
|
@@ -1427,17 +1435,14 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
|
|
|
1427
1435
|
}
|
|
1428
1436
|
const x402Set = /* @__PURE__ */ new Set();
|
|
1429
1437
|
const mppSet = /* @__PURE__ */ new Set();
|
|
1438
|
+
const methodHints = discovery.methodHints ?? "non-default";
|
|
1430
1439
|
for (const [key, entry] of registry.entries()) {
|
|
1431
1440
|
const url = `${normalizedBase}/api/${entry.path ?? key}`;
|
|
1432
|
-
|
|
1433
|
-
if (entry.
|
|
1434
|
-
|
|
1435
|
-
let instructions;
|
|
1436
|
-
if (typeof options.instructions === "function") {
|
|
1437
|
-
instructions = await options.instructions();
|
|
1438
|
-
} else if (typeof options.instructions === "string") {
|
|
1439
|
-
instructions = options.instructions;
|
|
1441
|
+
const resource = toDiscoveryResource(entry.method, url, methodHints);
|
|
1442
|
+
if (entry.authMode !== "unprotected") x402Set.add(resource);
|
|
1443
|
+
if (entry.protocols.includes("mpp")) mppSet.add(resource);
|
|
1440
1444
|
}
|
|
1445
|
+
const instructions = await resolveGuidance(discovery);
|
|
1441
1446
|
const body = {
|
|
1442
1447
|
version: 1,
|
|
1443
1448
|
resources: Array.from(x402Set)
|
|
@@ -1446,11 +1451,11 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
|
|
|
1446
1451
|
if (mppResources.length > 0) {
|
|
1447
1452
|
body.mppResources = mppResources;
|
|
1448
1453
|
}
|
|
1449
|
-
if (
|
|
1450
|
-
body.description =
|
|
1454
|
+
if (discovery.description) {
|
|
1455
|
+
body.description = discovery.description;
|
|
1451
1456
|
}
|
|
1452
|
-
if (
|
|
1453
|
-
body.ownershipProofs =
|
|
1457
|
+
if (discovery.ownershipProofs) {
|
|
1458
|
+
body.ownershipProofs = discovery.ownershipProofs;
|
|
1454
1459
|
}
|
|
1455
1460
|
if (instructions) {
|
|
1456
1461
|
body.instructions = instructions;
|
|
@@ -1464,10 +1469,16 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
|
|
|
1464
1469
|
});
|
|
1465
1470
|
};
|
|
1466
1471
|
}
|
|
1472
|
+
function toDiscoveryResource(method, url, mode) {
|
|
1473
|
+
if (mode === "off") return url;
|
|
1474
|
+
if (mode === "always") return `${method} ${url}`;
|
|
1475
|
+
const isDefaultProbeMethod = method === "GET" || method === "POST";
|
|
1476
|
+
return isDefaultProbeMethod ? url : `${method} ${url}`;
|
|
1477
|
+
}
|
|
1467
1478
|
|
|
1468
1479
|
// src/discovery/openapi.ts
|
|
1469
1480
|
import { NextResponse as NextResponse4 } from "next/server";
|
|
1470
|
-
function createOpenAPIHandler(registry, baseUrl, pricesKeys,
|
|
1481
|
+
function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
|
|
1471
1482
|
const normalizedBase = baseUrl.replace(/\/+$/, "");
|
|
1472
1483
|
let cached = null;
|
|
1473
1484
|
let validated = false;
|
|
@@ -1508,21 +1519,20 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, options) {
|
|
|
1508
1519
|
};
|
|
1509
1520
|
}
|
|
1510
1521
|
const discoveryMetadata = {};
|
|
1511
|
-
if (
|
|
1512
|
-
discoveryMetadata.ownershipProofs =
|
|
1513
|
-
}
|
|
1514
|
-
if (options.llmsTxtUrl) {
|
|
1515
|
-
discoveryMetadata.llmsTxtUrl = options.llmsTxtUrl;
|
|
1522
|
+
if (discovery.ownershipProofs && discovery.ownershipProofs.length > 0) {
|
|
1523
|
+
discoveryMetadata.ownershipProofs = discovery.ownershipProofs;
|
|
1516
1524
|
}
|
|
1525
|
+
const guidance = await resolveGuidance(discovery);
|
|
1517
1526
|
const openApiDocument = {
|
|
1518
1527
|
openapi: "3.1.0",
|
|
1519
1528
|
info: {
|
|
1520
|
-
title:
|
|
1521
|
-
description:
|
|
1522
|
-
version:
|
|
1523
|
-
|
|
1529
|
+
title: discovery.title,
|
|
1530
|
+
description: discovery.description,
|
|
1531
|
+
version: discovery.version,
|
|
1532
|
+
guidance,
|
|
1533
|
+
...discovery.contact && { contact: discovery.contact }
|
|
1524
1534
|
},
|
|
1525
|
-
servers: [{ url: (
|
|
1535
|
+
servers: [{ url: (discovery.serverUrl ?? normalizedBase).replace(/\/+$/, "") }],
|
|
1526
1536
|
tags: Array.from(tagSet).sort().map((name) => ({ name })),
|
|
1527
1537
|
...Object.keys(securitySchemes).length > 0 ? {
|
|
1528
1538
|
components: {
|
|
@@ -1642,6 +1652,20 @@ function buildPricingInfo(entry) {
|
|
|
1642
1652
|
return void 0;
|
|
1643
1653
|
}
|
|
1644
1654
|
|
|
1655
|
+
// src/discovery/llms-txt.ts
|
|
1656
|
+
import { NextResponse as NextResponse5 } from "next/server";
|
|
1657
|
+
function createLlmsTxtHandler(discovery) {
|
|
1658
|
+
return async (_request) => {
|
|
1659
|
+
const guidance = await resolveGuidance(discovery) ?? "";
|
|
1660
|
+
return new NextResponse5(guidance, {
|
|
1661
|
+
headers: {
|
|
1662
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
1663
|
+
"Access-Control-Allow-Origin": "*"
|
|
1664
|
+
}
|
|
1665
|
+
});
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1645
1669
|
// src/index.ts
|
|
1646
1670
|
function createRouter(config) {
|
|
1647
1671
|
const registry = new RouteRegistry();
|
|
@@ -1744,19 +1768,40 @@ function createRouter(config) {
|
|
|
1744
1768
|
})();
|
|
1745
1769
|
const pricesKeys = config.prices ? Object.keys(config.prices) : void 0;
|
|
1746
1770
|
return {
|
|
1747
|
-
route(
|
|
1748
|
-
const
|
|
1771
|
+
route(keyOrDefinition) {
|
|
1772
|
+
const isDefinition = typeof keyOrDefinition !== "string";
|
|
1773
|
+
if (config.strictRoutes && !isDefinition) {
|
|
1774
|
+
throw new Error(
|
|
1775
|
+
"[router] strictRoutes=true requires route({ path }) form. Replace route('my/key') with route({ path: 'my/key' })."
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
const definition = isDefinition ? keyOrDefinition : { path: keyOrDefinition, key: keyOrDefinition };
|
|
1779
|
+
const normalizedPath = normalizePath(definition.path);
|
|
1780
|
+
const key = definition.key ?? normalizedPath;
|
|
1781
|
+
if (config.strictRoutes && definition.key && definition.key !== definition.path) {
|
|
1782
|
+
throw new Error(
|
|
1783
|
+
`[router] strictRoutes=true forbids key/path divergence for route '${definition.path}'. Remove custom \`key\` or make it equal to \`path\`.`
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
let builder = new RouteBuilder(key, registry, deps);
|
|
1787
|
+
builder = builder.path(normalizedPath);
|
|
1788
|
+
if (definition.method) {
|
|
1789
|
+
builder = builder.method(definition.method);
|
|
1790
|
+
}
|
|
1749
1791
|
if (config.prices && key in config.prices) {
|
|
1750
1792
|
const options = config.protocols ? { protocols: config.protocols } : void 0;
|
|
1751
1793
|
return builder.paid(config.prices[key], options);
|
|
1752
1794
|
}
|
|
1753
1795
|
return builder;
|
|
1754
1796
|
},
|
|
1755
|
-
wellKnown(
|
|
1756
|
-
return createWellKnownHandler(registry, resolvedBaseUrl, pricesKeys,
|
|
1797
|
+
wellKnown() {
|
|
1798
|
+
return createWellKnownHandler(registry, resolvedBaseUrl, pricesKeys, config.discovery);
|
|
1757
1799
|
},
|
|
1758
|
-
openapi(
|
|
1759
|
-
return createOpenAPIHandler(registry, resolvedBaseUrl, pricesKeys,
|
|
1800
|
+
openapi() {
|
|
1801
|
+
return createOpenAPIHandler(registry, resolvedBaseUrl, pricesKeys, config.discovery);
|
|
1802
|
+
},
|
|
1803
|
+
llmsTxt() {
|
|
1804
|
+
return createLlmsTxtHandler(config.discovery);
|
|
1760
1805
|
},
|
|
1761
1806
|
monitors() {
|
|
1762
1807
|
const result = [];
|
|
@@ -1777,6 +1822,12 @@ function createRouter(config) {
|
|
|
1777
1822
|
registry
|
|
1778
1823
|
};
|
|
1779
1824
|
}
|
|
1825
|
+
function normalizePath(path) {
|
|
1826
|
+
let normalized = path.trim();
|
|
1827
|
+
normalized = normalized.replace(/^\/+/, "");
|
|
1828
|
+
normalized = normalized.replace(/^api\/+/, "");
|
|
1829
|
+
return normalized.replace(/\/+$/, "");
|
|
1830
|
+
}
|
|
1780
1831
|
export {
|
|
1781
1832
|
HttpError,
|
|
1782
1833
|
MemoryEntitlementStore,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentcash/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.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.
|
|
51
|
+
"mppx": "^0.3.13",
|
|
52
52
|
"next": "^15.0.0",
|
|
53
53
|
"prettier": "^3.8.1",
|
|
54
54
|
"react": "^19.0.0",
|