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