@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 +36 -5
- package/dist/index.cjs +302 -51
- package/dist/index.d.cts +76 -4
- package/dist/index.d.ts +76 -4
- package/dist/index.js +300 -51
- 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,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({
|
|
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
|
-
|
|
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
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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)
|
|
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 === "
|
|
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}':
|
|
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
|
-
|
|
1337
|
-
if (entry.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
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
|
-
...
|
|
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 (
|
|
1622
|
+
if (paymentRequired && (pricingInfo || protocols)) {
|
|
1448
1623
|
operation["x-payment-info"] = {
|
|
1449
|
-
...
|
|
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
|
|
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(
|
|
1570
|
-
const
|
|
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
|
});
|