@agentcash/router 0.4.6 → 0.4.8
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/.claude/CLAUDE.md +129 -0
- package/dist/client/index.cjs +94 -0
- package/dist/client/index.d.cts +86 -0
- package/dist/client/index.d.ts +86 -0
- package/dist/client/index.js +56 -0
- package/dist/index.cjs +234 -40
- package/dist/index.d.cts +78 -1
- package/dist/index.d.ts +78 -1
- package/dist/index.js +231 -40
- package/dist/siwx-BMlja_nt.d.cts +9 -0
- package/dist/siwx-BMlja_nt.d.ts +9 -0
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -76,7 +76,10 @@ __export(index_exports, {
|
|
|
76
76
|
MemoryNonceStore: () => MemoryNonceStore,
|
|
77
77
|
RouteBuilder: () => RouteBuilder,
|
|
78
78
|
RouteRegistry: () => RouteRegistry,
|
|
79
|
+
SIWX_CHALLENGE_EXPIRY_MS: () => SIWX_CHALLENGE_EXPIRY_MS,
|
|
80
|
+
SIWX_ERROR_MESSAGES: () => SIWX_ERROR_MESSAGES,
|
|
79
81
|
consolePlugin: () => consolePlugin,
|
|
82
|
+
createRedisNonceStore: () => createRedisNonceStore,
|
|
80
83
|
createRouter: () => createRouter
|
|
81
84
|
});
|
|
82
85
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -164,6 +167,10 @@ function consolePlugin() {
|
|
|
164
167
|
const ctx = createDefaultContext(meta);
|
|
165
168
|
return ctx;
|
|
166
169
|
},
|
|
170
|
+
onAuthVerified(_ctx, auth) {
|
|
171
|
+
const wallet = auth.wallet ? ` wallet=${auth.wallet}` : "";
|
|
172
|
+
console.log(`[router] AUTH ${auth.authMode} ${auth.route}${wallet}`);
|
|
173
|
+
},
|
|
167
174
|
onPaymentVerified(_ctx, payment) {
|
|
168
175
|
console.log(`[router] VERIFIED ${payment.protocol} ${payment.payer} ${payment.amount}`);
|
|
169
176
|
},
|
|
@@ -193,6 +200,65 @@ function consolePlugin() {
|
|
|
193
200
|
};
|
|
194
201
|
}
|
|
195
202
|
|
|
203
|
+
// src/auth/nonce.ts
|
|
204
|
+
var SIWX_CHALLENGE_EXPIRY_MS = 5 * 60 * 1e3;
|
|
205
|
+
var MemoryNonceStore = class {
|
|
206
|
+
seen = /* @__PURE__ */ new Map();
|
|
207
|
+
async check(nonce) {
|
|
208
|
+
this.evict();
|
|
209
|
+
if (this.seen.has(nonce)) return false;
|
|
210
|
+
this.seen.set(nonce, Date.now() + SIWX_CHALLENGE_EXPIRY_MS);
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
evict() {
|
|
214
|
+
const now = Date.now();
|
|
215
|
+
for (const [n, exp] of this.seen) {
|
|
216
|
+
if (exp < now) this.seen.delete(n);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
function detectRedisClientType(client) {
|
|
221
|
+
if (!client || typeof client !== "object") {
|
|
222
|
+
throw new Error(
|
|
223
|
+
"createRedisNonceStore requires a Redis client. Supported: @upstash/redis, ioredis. Pass your Redis client instance as the first argument."
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if ("options" in client && "status" in client) {
|
|
227
|
+
return "ioredis";
|
|
228
|
+
}
|
|
229
|
+
const constructor = client.constructor?.name;
|
|
230
|
+
if (constructor === "Redis" && "url" in client) {
|
|
231
|
+
return "upstash";
|
|
232
|
+
}
|
|
233
|
+
if (typeof client.set === "function") {
|
|
234
|
+
return "upstash";
|
|
235
|
+
}
|
|
236
|
+
throw new Error(
|
|
237
|
+
"Unrecognized Redis client. Supported: @upstash/redis, ioredis. If using a different client, implement NonceStore interface directly."
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
function createRedisNonceStore(client, opts) {
|
|
241
|
+
const prefix = opts?.prefix ?? "siwx:nonce:";
|
|
242
|
+
const ttlSeconds = Math.ceil((opts?.ttlMs ?? SIWX_CHALLENGE_EXPIRY_MS) / 1e3);
|
|
243
|
+
const clientType = detectRedisClientType(client);
|
|
244
|
+
return {
|
|
245
|
+
async check(nonce) {
|
|
246
|
+
const key = `${prefix}${nonce}`;
|
|
247
|
+
if (clientType === "upstash") {
|
|
248
|
+
const redis = client;
|
|
249
|
+
const result = await redis.set(key, "1", { ex: ttlSeconds, nx: true });
|
|
250
|
+
return result !== null;
|
|
251
|
+
}
|
|
252
|
+
if (clientType === "ioredis") {
|
|
253
|
+
const redis = client;
|
|
254
|
+
const result = await redis.set(key, "1", "EX", ttlSeconds, "NX");
|
|
255
|
+
return result === "OK";
|
|
256
|
+
}
|
|
257
|
+
throw new Error("Unknown Redis client type");
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
196
262
|
// src/protocols/detect.ts
|
|
197
263
|
function detectProtocol(request) {
|
|
198
264
|
if (request.headers.get("PAYMENT-SIGNATURE") || request.headers.get("X-PAYMENT")) {
|
|
@@ -368,6 +434,7 @@ async function settleX402Payment(server, payload, requirements) {
|
|
|
368
434
|
// src/protocols/mpp.ts
|
|
369
435
|
var import_mppx = require("mppx");
|
|
370
436
|
var import_server2 = require("mppx/server");
|
|
437
|
+
var import_tempo = require("mppx/tempo");
|
|
371
438
|
var import_viem = require("viem");
|
|
372
439
|
var import_chains = require("viem/chains");
|
|
373
440
|
function buildGetClient(rpcUrl) {
|
|
@@ -391,8 +458,7 @@ async function buildMPPChallenge(routeEntry, request, mppConfig, price) {
|
|
|
391
458
|
const standardRequest = toStandardRequest(request);
|
|
392
459
|
const currency = mppConfig.currency;
|
|
393
460
|
const recipient = mppConfig.recipient ?? "";
|
|
394
|
-
const
|
|
395
|
-
const challenge = import_mppx.Challenge.fromIntent(methodIntent, {
|
|
461
|
+
const challenge = import_mppx.Challenge.fromMethod(import_tempo.Methods.charge, {
|
|
396
462
|
secretKey: mppConfig.secretKey,
|
|
397
463
|
realm: new URL(standardRequest.url).origin,
|
|
398
464
|
request: {
|
|
@@ -467,21 +533,47 @@ function buildMPPReceipt(reference) {
|
|
|
467
533
|
}
|
|
468
534
|
|
|
469
535
|
// src/auth/siwx.ts
|
|
536
|
+
var SIWX_ERROR_MESSAGES = {
|
|
537
|
+
siwx_missing_header: "Missing SIGN-IN-WITH-X header",
|
|
538
|
+
siwx_malformed: "Malformed SIWX payload",
|
|
539
|
+
siwx_expired: "SIWX message expired \u2014 request a new challenge",
|
|
540
|
+
siwx_nonce_used: "Nonce already used \u2014 request a new challenge",
|
|
541
|
+
siwx_invalid_signature: "Invalid signature \u2014 wallet mismatch or corrupted proof"
|
|
542
|
+
};
|
|
543
|
+
function categorizeValidationError(error) {
|
|
544
|
+
if (!error) return "siwx_malformed";
|
|
545
|
+
const err = error.toLowerCase();
|
|
546
|
+
if (err.includes("expired") || err.includes("message too old")) {
|
|
547
|
+
return "siwx_expired";
|
|
548
|
+
}
|
|
549
|
+
if (err.includes("nonce validation failed")) {
|
|
550
|
+
return "siwx_nonce_used";
|
|
551
|
+
}
|
|
552
|
+
return "siwx_malformed";
|
|
553
|
+
}
|
|
470
554
|
async function verifySIWX(request, _routeEntry, nonceStore) {
|
|
471
555
|
const { parseSIWxHeader, validateSIWxMessage, verifySIWxSignature } = await import("@x402/extensions/sign-in-with-x");
|
|
472
556
|
const header = request.headers.get("SIGN-IN-WITH-X");
|
|
473
|
-
if (!header)
|
|
474
|
-
|
|
557
|
+
if (!header) {
|
|
558
|
+
return { valid: false, wallet: null, code: "siwx_missing_header" };
|
|
559
|
+
}
|
|
560
|
+
let payload;
|
|
561
|
+
try {
|
|
562
|
+
payload = parseSIWxHeader(header);
|
|
563
|
+
} catch {
|
|
564
|
+
return { valid: false, wallet: null, code: "siwx_malformed" };
|
|
565
|
+
}
|
|
475
566
|
const uri = request.url;
|
|
476
567
|
const validation = await validateSIWxMessage(payload, uri, {
|
|
477
568
|
checkNonce: (nonce) => nonceStore.check(nonce)
|
|
478
569
|
});
|
|
479
570
|
if (!validation.valid) {
|
|
480
|
-
|
|
571
|
+
const code = categorizeValidationError(validation.error);
|
|
572
|
+
return { valid: false, wallet: null, code };
|
|
481
573
|
}
|
|
482
574
|
const verified = await verifySIWxSignature(payload);
|
|
483
575
|
if (!verified?.valid) {
|
|
484
|
-
return { valid: false, wallet: null };
|
|
576
|
+
return { valid: false, wallet: null, code: "siwx_invalid_signature" };
|
|
485
577
|
}
|
|
486
578
|
return { valid: true, wallet: verified.address };
|
|
487
579
|
}
|
|
@@ -555,6 +647,15 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
555
647
|
firePluginResponse(deps, pluginCtx, meta, body2.response);
|
|
556
648
|
return body2.response;
|
|
557
649
|
}
|
|
650
|
+
if (routeEntry.validateFn) {
|
|
651
|
+
try {
|
|
652
|
+
await routeEntry.validateFn(body2.data);
|
|
653
|
+
} catch (err) {
|
|
654
|
+
const status = err.status ?? 400;
|
|
655
|
+
const message = err instanceof Error ? err.message : "Validation failed";
|
|
656
|
+
return fail(status, message, meta, pluginCtx);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
558
659
|
const { response, rawResult } = await invoke(
|
|
559
660
|
request,
|
|
560
661
|
meta,
|
|
@@ -579,13 +680,20 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
579
680
|
return fail(401, "Invalid or missing API key", meta, pluginCtx);
|
|
580
681
|
}
|
|
581
682
|
account = keyResult.account;
|
|
683
|
+
firePluginHook(deps.plugin, "onAuthVerified", pluginCtx, {
|
|
684
|
+
authMode: "apiKey",
|
|
685
|
+
wallet: null,
|
|
686
|
+
route: routeEntry.key,
|
|
687
|
+
account
|
|
688
|
+
});
|
|
582
689
|
if (routeEntry.authMode === "apiKey" && !routeEntry.pricing) {
|
|
583
690
|
return handleAuth(null, account);
|
|
584
691
|
}
|
|
585
692
|
}
|
|
586
693
|
const protocol = detectProtocol(request);
|
|
587
694
|
let earlyBodyData;
|
|
588
|
-
|
|
695
|
+
const needsEarlyParse = !protocol && routeEntry.bodySchema && (typeof routeEntry.pricing === "function" || routeEntry.validateFn);
|
|
696
|
+
if (needsEarlyParse) {
|
|
589
697
|
const requestForPricing = request.clone();
|
|
590
698
|
const earlyBodyResult = await parseBody(requestForPricing, routeEntry);
|
|
591
699
|
if (!earlyBodyResult.ok) {
|
|
@@ -593,11 +701,35 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
593
701
|
return earlyBodyResult.response;
|
|
594
702
|
}
|
|
595
703
|
earlyBodyData = earlyBodyResult.data;
|
|
704
|
+
if (routeEntry.validateFn) {
|
|
705
|
+
try {
|
|
706
|
+
await routeEntry.validateFn(earlyBodyData);
|
|
707
|
+
} catch (err) {
|
|
708
|
+
const status = err.status ?? 400;
|
|
709
|
+
const message = err instanceof Error ? err.message : "Validation failed";
|
|
710
|
+
return fail(status, message, meta, pluginCtx);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
596
713
|
}
|
|
597
714
|
if (routeEntry.authMode === "siwx") {
|
|
715
|
+
if (routeEntry.validateFn && routeEntry.bodySchema && !request.headers.get("SIGN-IN-WITH-X")) {
|
|
716
|
+
const requestForValidation = request.clone();
|
|
717
|
+
const earlyBodyResult = await parseBody(requestForValidation, routeEntry);
|
|
718
|
+
if (!earlyBodyResult.ok) {
|
|
719
|
+
firePluginResponse(deps, pluginCtx, meta, earlyBodyResult.response);
|
|
720
|
+
return earlyBodyResult.response;
|
|
721
|
+
}
|
|
722
|
+
try {
|
|
723
|
+
await routeEntry.validateFn(earlyBodyResult.data);
|
|
724
|
+
} catch (err) {
|
|
725
|
+
const status = err.status ?? 400;
|
|
726
|
+
const message = err instanceof Error ? err.message : "Validation failed";
|
|
727
|
+
return fail(status, message, meta, pluginCtx);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
598
730
|
if (!request.headers.get("SIGN-IN-WITH-X")) {
|
|
599
731
|
const url = new URL(request.url);
|
|
600
|
-
const nonce = crypto.randomUUID();
|
|
732
|
+
const nonce = crypto.randomUUID().replace(/-/g, "");
|
|
601
733
|
const siwxInfo = {
|
|
602
734
|
domain: url.hostname,
|
|
603
735
|
uri: request.url,
|
|
@@ -606,7 +738,7 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
606
738
|
type: "eip191",
|
|
607
739
|
nonce,
|
|
608
740
|
issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
609
|
-
expirationTime: new Date(Date.now() +
|
|
741
|
+
expirationTime: new Date(Date.now() + SIWX_CHALLENGE_EXPIRY_MS).toISOString(),
|
|
610
742
|
statement: "Sign in to verify your wallet identity"
|
|
611
743
|
};
|
|
612
744
|
let siwxSchema;
|
|
@@ -626,6 +758,8 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
626
758
|
extensions: {
|
|
627
759
|
"sign-in-with-x": {
|
|
628
760
|
info: siwxInfo,
|
|
761
|
+
// supportedChains at top level required by MCP tools for chain detection
|
|
762
|
+
supportedChains: [{ chainId: deps.network, type: "eip191" }],
|
|
629
763
|
...siwxSchema ? { schema: siwxSchema } : {}
|
|
630
764
|
}
|
|
631
765
|
}
|
|
@@ -651,15 +785,21 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
651
785
|
}
|
|
652
786
|
const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
|
|
653
787
|
if (!siwx.valid) {
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
meta,
|
|
658
|
-
pluginCtx
|
|
788
|
+
const response = import_server3.NextResponse.json(
|
|
789
|
+
{ error: siwx.code, message: SIWX_ERROR_MESSAGES[siwx.code] },
|
|
790
|
+
{ status: 402 }
|
|
659
791
|
);
|
|
792
|
+
firePluginResponse(deps, pluginCtx, meta, response);
|
|
793
|
+
return response;
|
|
660
794
|
}
|
|
661
|
-
|
|
662
|
-
|
|
795
|
+
const wallet = siwx.wallet.toLowerCase();
|
|
796
|
+
pluginCtx.setVerifiedWallet(wallet);
|
|
797
|
+
firePluginHook(deps.plugin, "onAuthVerified", pluginCtx, {
|
|
798
|
+
authMode: "siwx",
|
|
799
|
+
wallet,
|
|
800
|
+
route: routeEntry.key
|
|
801
|
+
});
|
|
802
|
+
return handleAuth(wallet, void 0);
|
|
663
803
|
}
|
|
664
804
|
if (!protocol || protocol === "siwx") {
|
|
665
805
|
return await build402(request, routeEntry, deps, meta, pluginCtx, earlyBodyData);
|
|
@@ -669,6 +809,15 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
669
809
|
firePluginResponse(deps, pluginCtx, meta, body.response);
|
|
670
810
|
return body.response;
|
|
671
811
|
}
|
|
812
|
+
if (routeEntry.validateFn) {
|
|
813
|
+
try {
|
|
814
|
+
await routeEntry.validateFn(body.data);
|
|
815
|
+
} catch (err) {
|
|
816
|
+
const status = err.status ?? 400;
|
|
817
|
+
const message = err instanceof Error ? err.message : "Validation failed";
|
|
818
|
+
return fail(status, message, meta, pluginCtx);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
672
821
|
let price;
|
|
673
822
|
try {
|
|
674
823
|
price = await resolvePrice(routeEntry.pricing, body.data);
|
|
@@ -695,10 +844,11 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
695
844
|
);
|
|
696
845
|
if (!verify?.valid) return await build402(request, routeEntry, deps, meta, pluginCtx);
|
|
697
846
|
const { payload: verifyPayload, requirements: verifyRequirements } = verify;
|
|
698
|
-
|
|
847
|
+
const wallet = verify.payer.toLowerCase();
|
|
848
|
+
pluginCtx.setVerifiedWallet(wallet);
|
|
699
849
|
firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
|
|
700
850
|
protocol: "x402",
|
|
701
|
-
payer:
|
|
851
|
+
payer: wallet,
|
|
702
852
|
amount: price,
|
|
703
853
|
network: deps.network
|
|
704
854
|
});
|
|
@@ -706,7 +856,7 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
706
856
|
request,
|
|
707
857
|
meta,
|
|
708
858
|
pluginCtx,
|
|
709
|
-
|
|
859
|
+
wallet,
|
|
710
860
|
account,
|
|
711
861
|
body.data
|
|
712
862
|
);
|
|
@@ -757,7 +907,7 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
757
907
|
if (!deps.mppConfig) return await build402(request, routeEntry, deps, meta, pluginCtx);
|
|
758
908
|
const verify = await verifyMPPCredential(request, routeEntry, deps.mppConfig, price);
|
|
759
909
|
if (!verify?.valid) return await build402(request, routeEntry, deps, meta, pluginCtx);
|
|
760
|
-
const wallet = verify.payer;
|
|
910
|
+
const wallet = verify.payer.toLowerCase();
|
|
761
911
|
pluginCtx.setVerifiedWallet(wallet);
|
|
762
912
|
firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
|
|
763
913
|
protocol: "mpp",
|
|
@@ -999,6 +1149,8 @@ var RouteBuilder = class {
|
|
|
999
1149
|
/** @internal */
|
|
1000
1150
|
_maxPrice;
|
|
1001
1151
|
/** @internal */
|
|
1152
|
+
_minPrice;
|
|
1153
|
+
/** @internal */
|
|
1002
1154
|
_bodySchema;
|
|
1003
1155
|
/** @internal */
|
|
1004
1156
|
_querySchema;
|
|
@@ -1016,6 +1168,8 @@ var RouteBuilder = class {
|
|
|
1016
1168
|
_providerName;
|
|
1017
1169
|
/** @internal */
|
|
1018
1170
|
_providerConfig;
|
|
1171
|
+
/** @internal */
|
|
1172
|
+
_validateFn;
|
|
1019
1173
|
constructor(key, registry, deps) {
|
|
1020
1174
|
this._key = key;
|
|
1021
1175
|
this._registry = registry;
|
|
@@ -1028,11 +1182,17 @@ var RouteBuilder = class {
|
|
|
1028
1182
|
return next;
|
|
1029
1183
|
}
|
|
1030
1184
|
paid(pricing, options) {
|
|
1185
|
+
if (this._authMode === "siwx") {
|
|
1186
|
+
throw new Error(
|
|
1187
|
+
`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.`
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1031
1190
|
const next = this.fork();
|
|
1032
1191
|
next._authMode = "paid";
|
|
1033
1192
|
next._pricing = pricing;
|
|
1034
1193
|
if (options?.protocols) next._protocols = options.protocols;
|
|
1035
1194
|
if (options?.maxPrice) next._maxPrice = options.maxPrice;
|
|
1195
|
+
if (options?.minPrice) next._minPrice = options.minPrice;
|
|
1036
1196
|
if (typeof pricing === "object" && "tiers" in pricing) {
|
|
1037
1197
|
for (const [tierKey, tierConfig] of Object.entries(pricing.tiers)) {
|
|
1038
1198
|
if (!tierKey) {
|
|
@@ -1057,6 +1217,11 @@ var RouteBuilder = class {
|
|
|
1057
1217
|
return next;
|
|
1058
1218
|
}
|
|
1059
1219
|
siwx() {
|
|
1220
|
+
if (this._authMode === "paid") {
|
|
1221
|
+
throw new Error(
|
|
1222
|
+
`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.`
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1060
1225
|
const next = this.fork();
|
|
1061
1226
|
next._authMode = "siwx";
|
|
1062
1227
|
next._protocols = [];
|
|
@@ -1117,7 +1282,41 @@ var RouteBuilder = class {
|
|
|
1117
1282
|
next._method = m;
|
|
1118
1283
|
return next;
|
|
1119
1284
|
}
|
|
1285
|
+
// -------------------------------------------------------------------------
|
|
1286
|
+
// Pre-payment validation
|
|
1287
|
+
// -------------------------------------------------------------------------
|
|
1288
|
+
/**
|
|
1289
|
+
* Add pre-payment validation that runs after body parsing but before the 402
|
|
1290
|
+
* challenge is shown. Use this for async business logic like "is this resource
|
|
1291
|
+
* available?" or "has this user hit their rate limit?".
|
|
1292
|
+
*
|
|
1293
|
+
* Requires `.body()` — call `.body()` before `.validate()` for type inference.
|
|
1294
|
+
*
|
|
1295
|
+
* @example
|
|
1296
|
+
* ```typescript
|
|
1297
|
+
* router
|
|
1298
|
+
* .route('domain/register')
|
|
1299
|
+
* .paid(calculatePrice)
|
|
1300
|
+
* .body(RegisterSchema) // .body() first for type inference
|
|
1301
|
+
* .validate(async (body) => {
|
|
1302
|
+
* if (await isDomainTaken(body.domain)) {
|
|
1303
|
+
* throw Object.assign(new Error('Domain taken'), { status: 409 });
|
|
1304
|
+
* }
|
|
1305
|
+
* })
|
|
1306
|
+
* .handler(async ({ body }) => { ... });
|
|
1307
|
+
* ```
|
|
1308
|
+
*/
|
|
1309
|
+
validate(fn) {
|
|
1310
|
+
const next = this.fork();
|
|
1311
|
+
next._validateFn = fn;
|
|
1312
|
+
return next;
|
|
1313
|
+
}
|
|
1120
1314
|
handler(fn) {
|
|
1315
|
+
if (this._validateFn && !this._bodySchema) {
|
|
1316
|
+
throw new Error(
|
|
1317
|
+
`route '${this._key}': .validate() requires .body() \u2014 validation runs on parsed body`
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1121
1320
|
const entry = {
|
|
1122
1321
|
key: this._key,
|
|
1123
1322
|
authMode: this._authMode,
|
|
@@ -1130,32 +1329,17 @@ var RouteBuilder = class {
|
|
|
1130
1329
|
path: this._path,
|
|
1131
1330
|
method: this._method,
|
|
1132
1331
|
maxPrice: this._maxPrice,
|
|
1332
|
+
minPrice: this._minPrice,
|
|
1133
1333
|
apiKeyResolver: this._apiKeyResolver,
|
|
1134
1334
|
providerName: this._providerName,
|
|
1135
|
-
providerConfig: this._providerConfig
|
|
1335
|
+
providerConfig: this._providerConfig,
|
|
1336
|
+
validateFn: this._validateFn
|
|
1136
1337
|
};
|
|
1137
1338
|
this._registry.register(entry);
|
|
1138
1339
|
return createRequestHandler(entry, fn, this._deps);
|
|
1139
1340
|
}
|
|
1140
1341
|
};
|
|
1141
1342
|
|
|
1142
|
-
// src/auth/nonce.ts
|
|
1143
|
-
var MemoryNonceStore = class {
|
|
1144
|
-
seen = /* @__PURE__ */ new Map();
|
|
1145
|
-
async check(nonce) {
|
|
1146
|
-
this.evict();
|
|
1147
|
-
if (this.seen.has(nonce)) return false;
|
|
1148
|
-
this.seen.set(nonce, Date.now() + 5 * 60 * 1e3);
|
|
1149
|
-
return true;
|
|
1150
|
-
}
|
|
1151
|
-
evict() {
|
|
1152
|
-
const now = Date.now();
|
|
1153
|
-
for (const [n, exp] of this.seen) {
|
|
1154
|
-
if (exp < now) this.seen.delete(n);
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
};
|
|
1158
|
-
|
|
1159
1343
|
// src/discovery/well-known.ts
|
|
1160
1344
|
var import_server4 = require("next/server");
|
|
1161
1345
|
function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
|
|
@@ -1248,9 +1432,16 @@ function buildOperation(routeKey, entry, tag) {
|
|
|
1248
1432
|
const protocols = entry.protocols.length > 0 ? entry.protocols : void 0;
|
|
1249
1433
|
let price;
|
|
1250
1434
|
if (typeof entry.pricing === "string") {
|
|
1251
|
-
price =
|
|
1435
|
+
price = entry.pricing;
|
|
1436
|
+
} else if (typeof entry.pricing === "object" && "tiers" in entry.pricing) {
|
|
1437
|
+
const tierPrices = Object.values(entry.pricing.tiers).map((t) => parseFloat(t.price));
|
|
1438
|
+
const min = Math.min(...tierPrices);
|
|
1439
|
+
const max = Math.max(...tierPrices);
|
|
1440
|
+
price = min === max ? String(min) : `${min}-${max}`;
|
|
1441
|
+
} else if (entry.minPrice && entry.maxPrice) {
|
|
1442
|
+
price = `${entry.minPrice}-${entry.maxPrice}`;
|
|
1252
1443
|
} else if (entry.maxPrice) {
|
|
1253
|
-
price =
|
|
1444
|
+
price = entry.maxPrice;
|
|
1254
1445
|
}
|
|
1255
1446
|
const operation = {
|
|
1256
1447
|
operationId: routeKey.replace(/\//g, "_"),
|
|
@@ -1388,6 +1579,9 @@ function createRouter(config) {
|
|
|
1388
1579
|
MemoryNonceStore,
|
|
1389
1580
|
RouteBuilder,
|
|
1390
1581
|
RouteRegistry,
|
|
1582
|
+
SIWX_CHALLENGE_EXPIRY_MS,
|
|
1583
|
+
SIWX_ERROR_MESSAGES,
|
|
1391
1584
|
consolePlugin,
|
|
1585
|
+
createRedisNonceStore,
|
|
1392
1586
|
createRouter
|
|
1393
1587
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,15 +1,53 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { ZodType } from 'zod';
|
|
3
3
|
import { PaymentRequirements, PaymentRequired, SettleResponse } from '@x402/core/types';
|
|
4
|
+
export { S as SIWX_ERROR_MESSAGES, a as SiwxErrorCode } from './siwx-BMlja_nt.cjs';
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* SIWX challenge expiry in milliseconds.
|
|
8
|
+
* Currently not configurable per-route — this is a known limitation.
|
|
9
|
+
* Future versions may add `siwx: { expiryMs }` to RouterConfig.
|
|
10
|
+
*/
|
|
11
|
+
declare const SIWX_CHALLENGE_EXPIRY_MS: number;
|
|
5
12
|
interface NonceStore {
|
|
6
13
|
check(nonce: string): Promise<boolean>;
|
|
7
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* In-memory nonce store for development and testing.
|
|
17
|
+
* NOT suitable for production serverless environments (Vercel, etc.)
|
|
18
|
+
* where each function invocation gets fresh memory.
|
|
19
|
+
*
|
|
20
|
+
* For production, use `createRedisNonceStore()` with Upstash or ioredis.
|
|
21
|
+
*/
|
|
8
22
|
declare class MemoryNonceStore implements NonceStore {
|
|
9
23
|
private seen;
|
|
10
24
|
check(nonce: string): Promise<boolean>;
|
|
11
25
|
private evict;
|
|
12
26
|
}
|
|
27
|
+
interface RedisNonceStoreOptions {
|
|
28
|
+
/** Key prefix for nonce storage. Default: 'siwx:nonce:' */
|
|
29
|
+
prefix?: string;
|
|
30
|
+
/** TTL in milliseconds. Default: SIWX_CHALLENGE_EXPIRY_MS (5 minutes) */
|
|
31
|
+
ttlMs?: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Create a Redis-backed nonce store for production SIWX replay protection.
|
|
35
|
+
* Auto-detects client type (Upstash or ioredis) and uses appropriate API.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* // Upstash (Vercel)
|
|
40
|
+
* import { Redis } from '@upstash/redis';
|
|
41
|
+
* const redis = new Redis({ url: process.env.UPSTASH_URL, token: process.env.UPSTASH_TOKEN });
|
|
42
|
+
* const nonceStore = createRedisNonceStore(redis);
|
|
43
|
+
*
|
|
44
|
+
* // ioredis
|
|
45
|
+
* import Redis from 'ioredis';
|
|
46
|
+
* const redis = new Redis(process.env.REDIS_URL);
|
|
47
|
+
* const nonceStore = createRedisNonceStore(redis);
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
declare function createRedisNonceStore(client: unknown, opts?: RedisNonceStoreOptions): NonceStore;
|
|
13
51
|
|
|
14
52
|
interface RequestMeta {
|
|
15
53
|
requestId: string;
|
|
@@ -57,11 +95,23 @@ interface ErrorEvent {
|
|
|
57
95
|
message: string;
|
|
58
96
|
settled: boolean;
|
|
59
97
|
}
|
|
98
|
+
interface AuthEvent {
|
|
99
|
+
/** Authentication mode that was verified */
|
|
100
|
+
authMode: 'siwx' | 'apiKey';
|
|
101
|
+
/** Verified wallet address (lowercase) */
|
|
102
|
+
wallet: string | null;
|
|
103
|
+
/** Route key */
|
|
104
|
+
route: string;
|
|
105
|
+
/** Account data from API key resolver (for apiKey auth) */
|
|
106
|
+
account?: unknown;
|
|
107
|
+
}
|
|
60
108
|
interface RouterPlugin {
|
|
61
109
|
init?(config: {
|
|
62
110
|
origin?: string;
|
|
63
111
|
}): void | Promise<void>;
|
|
64
112
|
onRequest?(meta: RequestMeta): PluginContext;
|
|
113
|
+
/** Fired after successful SIWX or API key verification, before handler */
|
|
114
|
+
onAuthVerified?(ctx: PluginContext, event: AuthEvent): void;
|
|
65
115
|
onPaymentVerified?(ctx: PluginContext, payment: PaymentEvent): void;
|
|
66
116
|
onPaymentSettled?(ctx: PluginContext, settlement: SettlementEvent): void;
|
|
67
117
|
onResponse?(ctx: PluginContext, response: ResponseMeta): void;
|
|
@@ -120,6 +170,7 @@ type PricingConfig<TBody = unknown> = string | ((body: TBody) => string | Promis
|
|
|
120
170
|
interface PaidOptions {
|
|
121
171
|
protocols?: ProtocolType[];
|
|
122
172
|
maxPrice?: string;
|
|
173
|
+
minPrice?: string;
|
|
123
174
|
}
|
|
124
175
|
interface HandlerContext<TBody = undefined, TQuery = undefined> {
|
|
125
176
|
body: TBody;
|
|
@@ -168,9 +219,11 @@ interface RouteEntry {
|
|
|
168
219
|
path?: string;
|
|
169
220
|
method: 'GET' | 'POST' | 'DELETE' | 'PUT' | 'PATCH';
|
|
170
221
|
maxPrice?: string;
|
|
222
|
+
minPrice?: string;
|
|
171
223
|
apiKeyResolver?: (key: string) => unknown | Promise<unknown>;
|
|
172
224
|
providerName?: string;
|
|
173
225
|
providerConfig?: ProviderConfig;
|
|
226
|
+
validateFn?: (body: unknown) => void | Promise<void>;
|
|
174
227
|
}
|
|
175
228
|
interface RouterConfig {
|
|
176
229
|
payeeAddress: string;
|
|
@@ -257,6 +310,7 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, HasAuth extend
|
|
|
257
310
|
/** @internal */ _pricing: PricingConfig | undefined;
|
|
258
311
|
/** @internal */ _protocols: ProtocolType[];
|
|
259
312
|
/** @internal */ _maxPrice: string | undefined;
|
|
313
|
+
/** @internal */ _minPrice: string | undefined;
|
|
260
314
|
/** @internal */ _bodySchema: ZodType | undefined;
|
|
261
315
|
/** @internal */ _querySchema: ZodType | undefined;
|
|
262
316
|
/** @internal */ _outputSchema: ZodType | undefined;
|
|
@@ -266,6 +320,7 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, HasAuth extend
|
|
|
266
320
|
/** @internal */ _apiKeyResolver: ((key: string) => unknown | Promise<unknown>) | undefined;
|
|
267
321
|
/** @internal */ _providerName: string | undefined;
|
|
268
322
|
/** @internal */ _providerConfig: ProviderConfig | undefined;
|
|
323
|
+
/** @internal */ _validateFn: ((body: TBody) => void | Promise<void>) | undefined;
|
|
269
324
|
constructor(key: string, registry: RouteRegistry, deps: OrchestrateDeps);
|
|
270
325
|
private fork;
|
|
271
326
|
paid(pricing: string, options?: PaidOptions): RouteBuilder<TBody, TQuery, True, False, HasBody>;
|
|
@@ -290,6 +345,28 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, HasAuth extend
|
|
|
290
345
|
description(text: string): this;
|
|
291
346
|
path(p: string): this;
|
|
292
347
|
method(m: 'GET' | 'POST' | 'DELETE' | 'PUT' | 'PATCH'): this;
|
|
348
|
+
/**
|
|
349
|
+
* Add pre-payment validation that runs after body parsing but before the 402
|
|
350
|
+
* challenge is shown. Use this for async business logic like "is this resource
|
|
351
|
+
* available?" or "has this user hit their rate limit?".
|
|
352
|
+
*
|
|
353
|
+
* Requires `.body()` — call `.body()` before `.validate()` for type inference.
|
|
354
|
+
*
|
|
355
|
+
* @example
|
|
356
|
+
* ```typescript
|
|
357
|
+
* router
|
|
358
|
+
* .route('domain/register')
|
|
359
|
+
* .paid(calculatePrice)
|
|
360
|
+
* .body(RegisterSchema) // .body() first for type inference
|
|
361
|
+
* .validate(async (body) => {
|
|
362
|
+
* if (await isDomainTaken(body.domain)) {
|
|
363
|
+
* throw Object.assign(new Error('Domain taken'), { status: 409 });
|
|
364
|
+
* }
|
|
365
|
+
* })
|
|
366
|
+
* .handler(async ({ body }) => { ... });
|
|
367
|
+
* ```
|
|
368
|
+
*/
|
|
369
|
+
validate(fn: (body: TBody) => void | Promise<void>): RouteBuilder<TBody, TQuery, HasAuth, NeedsBody, HasBody>;
|
|
293
370
|
handler(this: RouteBuilder<TBody, TQuery, True, true, false>, fn: never): never;
|
|
294
371
|
handler(this: RouteBuilder<TBody, TQuery, false, boolean, boolean>, fn: never): never;
|
|
295
372
|
handler(this: RouteBuilder<TBody, TQuery, True, False, HasBody>, fn: (ctx: HandlerContext<TBody, TQuery>) => Promise<unknown>): (request: NextRequest) => Promise<Response>;
|
|
@@ -313,4 +390,4 @@ interface ServiceRouter {
|
|
|
313
390
|
}
|
|
314
391
|
declare function createRouter(config: RouterConfig): ServiceRouter;
|
|
315
392
|
|
|
316
|
-
export { type AlertEvent, type AlertFn, type AlertLevel, 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 RequestMeta, type ResponseMeta, RouteBuilder, type RouteEntry, RouteRegistry, type RouterConfig, type RouterPlugin, type ServiceRouter, type SettlementEvent, type TierConfig, type X402Server, consolePlugin, createRouter };
|
|
393
|
+
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 };
|