@agentcash/router 0.4.5 → 0.4.7
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 +245 -38
- package/dist/index.d.cts +75 -1
- package/dist/index.d.ts +75 -1
- package/dist/index.js +242 -38
- package/dist/siwx-BMlja_nt.d.cts +9 -0
- package/dist/siwx-BMlja_nt.d.ts +9 -0
- package/package.json +1 -1
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")) {
|
|
@@ -354,6 +420,12 @@ async function verifyX402Payment(server, request, routeEntry, price, payeeAddres
|
|
|
354
420
|
}
|
|
355
421
|
async function settleX402Payment(server, payload, requirements) {
|
|
356
422
|
const { encodePaymentResponseHeader } = await import("@x402/core/http");
|
|
423
|
+
const payloadKeys = typeof payload === "object" && payload !== null ? Object.keys(payload).sort().join(",") : "n/a";
|
|
424
|
+
const reqKeys = typeof requirements === "object" && requirements !== null ? Object.keys(requirements).sort().join(",") : "n/a";
|
|
425
|
+
console.info("x402 settle input", {
|
|
426
|
+
payloadKeys,
|
|
427
|
+
requirementsKeys: reqKeys
|
|
428
|
+
});
|
|
357
429
|
const result = await server.settlePayment(payload, requirements);
|
|
358
430
|
const encoded = encodePaymentResponseHeader(result);
|
|
359
431
|
return { encoded, result };
|
|
@@ -461,21 +533,47 @@ function buildMPPReceipt(reference) {
|
|
|
461
533
|
}
|
|
462
534
|
|
|
463
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
|
+
}
|
|
464
554
|
async function verifySIWX(request, _routeEntry, nonceStore) {
|
|
465
555
|
const { parseSIWxHeader, validateSIWxMessage, verifySIWxSignature } = await import("@x402/extensions/sign-in-with-x");
|
|
466
556
|
const header = request.headers.get("SIGN-IN-WITH-X");
|
|
467
|
-
if (!header)
|
|
468
|
-
|
|
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
|
+
}
|
|
469
566
|
const uri = request.url;
|
|
470
567
|
const validation = await validateSIWxMessage(payload, uri, {
|
|
471
568
|
checkNonce: (nonce) => nonceStore.check(nonce)
|
|
472
569
|
});
|
|
473
570
|
if (!validation.valid) {
|
|
474
|
-
|
|
571
|
+
const code = categorizeValidationError(validation.error);
|
|
572
|
+
return { valid: false, wallet: null, code };
|
|
475
573
|
}
|
|
476
574
|
const verified = await verifySIWxSignature(payload);
|
|
477
575
|
if (!verified?.valid) {
|
|
478
|
-
return { valid: false, wallet: null };
|
|
576
|
+
return { valid: false, wallet: null, code: "siwx_invalid_signature" };
|
|
479
577
|
}
|
|
480
578
|
return { valid: true, wallet: verified.address };
|
|
481
579
|
}
|
|
@@ -549,6 +647,15 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
549
647
|
firePluginResponse(deps, pluginCtx, meta, body2.response);
|
|
550
648
|
return body2.response;
|
|
551
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
|
+
}
|
|
552
659
|
const { response, rawResult } = await invoke(
|
|
553
660
|
request,
|
|
554
661
|
meta,
|
|
@@ -573,13 +680,20 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
573
680
|
return fail(401, "Invalid or missing API key", meta, pluginCtx);
|
|
574
681
|
}
|
|
575
682
|
account = keyResult.account;
|
|
683
|
+
firePluginHook(deps.plugin, "onAuthVerified", pluginCtx, {
|
|
684
|
+
authMode: "apiKey",
|
|
685
|
+
wallet: null,
|
|
686
|
+
route: routeEntry.key,
|
|
687
|
+
account
|
|
688
|
+
});
|
|
576
689
|
if (routeEntry.authMode === "apiKey" && !routeEntry.pricing) {
|
|
577
690
|
return handleAuth(null, account);
|
|
578
691
|
}
|
|
579
692
|
}
|
|
580
693
|
const protocol = detectProtocol(request);
|
|
581
694
|
let earlyBodyData;
|
|
582
|
-
|
|
695
|
+
const needsEarlyParse = !protocol && routeEntry.bodySchema && (typeof routeEntry.pricing === "function" || routeEntry.validateFn);
|
|
696
|
+
if (needsEarlyParse) {
|
|
583
697
|
const requestForPricing = request.clone();
|
|
584
698
|
const earlyBodyResult = await parseBody(requestForPricing, routeEntry);
|
|
585
699
|
if (!earlyBodyResult.ok) {
|
|
@@ -587,11 +701,35 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
587
701
|
return earlyBodyResult.response;
|
|
588
702
|
}
|
|
589
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
|
+
}
|
|
590
713
|
}
|
|
591
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
|
+
}
|
|
592
730
|
if (!request.headers.get("SIGN-IN-WITH-X")) {
|
|
593
731
|
const url = new URL(request.url);
|
|
594
|
-
const nonce = crypto.randomUUID();
|
|
732
|
+
const nonce = crypto.randomUUID().replace(/-/g, "");
|
|
595
733
|
const siwxInfo = {
|
|
596
734
|
domain: url.hostname,
|
|
597
735
|
uri: request.url,
|
|
@@ -600,7 +738,7 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
600
738
|
type: "eip191",
|
|
601
739
|
nonce,
|
|
602
740
|
issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
603
|
-
expirationTime: new Date(Date.now() +
|
|
741
|
+
expirationTime: new Date(Date.now() + SIWX_CHALLENGE_EXPIRY_MS).toISOString(),
|
|
604
742
|
statement: "Sign in to verify your wallet identity"
|
|
605
743
|
};
|
|
606
744
|
let siwxSchema;
|
|
@@ -620,6 +758,8 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
620
758
|
extensions: {
|
|
621
759
|
"sign-in-with-x": {
|
|
622
760
|
info: siwxInfo,
|
|
761
|
+
// supportedChains at top level required by MCP tools for chain detection
|
|
762
|
+
supportedChains: [{ chainId: deps.network, type: "eip191" }],
|
|
623
763
|
...siwxSchema ? { schema: siwxSchema } : {}
|
|
624
764
|
}
|
|
625
765
|
}
|
|
@@ -645,15 +785,21 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
645
785
|
}
|
|
646
786
|
const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
|
|
647
787
|
if (!siwx.valid) {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
meta,
|
|
652
|
-
pluginCtx
|
|
788
|
+
const response = import_server3.NextResponse.json(
|
|
789
|
+
{ error: siwx.code, message: SIWX_ERROR_MESSAGES[siwx.code] },
|
|
790
|
+
{ status: 402 }
|
|
653
791
|
);
|
|
792
|
+
firePluginResponse(deps, pluginCtx, meta, response);
|
|
793
|
+
return response;
|
|
654
794
|
}
|
|
655
|
-
|
|
656
|
-
|
|
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);
|
|
657
803
|
}
|
|
658
804
|
if (!protocol || protocol === "siwx") {
|
|
659
805
|
return await build402(request, routeEntry, deps, meta, pluginCtx, earlyBodyData);
|
|
@@ -663,6 +809,15 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
663
809
|
firePluginResponse(deps, pluginCtx, meta, body.response);
|
|
664
810
|
return body.response;
|
|
665
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
|
+
}
|
|
666
821
|
let price;
|
|
667
822
|
try {
|
|
668
823
|
price = await resolvePrice(routeEntry.pricing, body.data);
|
|
@@ -688,10 +843,12 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
688
843
|
deps.network
|
|
689
844
|
);
|
|
690
845
|
if (!verify?.valid) return await build402(request, routeEntry, deps, meta, pluginCtx);
|
|
691
|
-
|
|
846
|
+
const { payload: verifyPayload, requirements: verifyRequirements } = verify;
|
|
847
|
+
const wallet = verify.payer.toLowerCase();
|
|
848
|
+
pluginCtx.setVerifiedWallet(wallet);
|
|
692
849
|
firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
|
|
693
850
|
protocol: "x402",
|
|
694
|
-
payer:
|
|
851
|
+
payer: wallet,
|
|
695
852
|
amount: price,
|
|
696
853
|
network: deps.network
|
|
697
854
|
});
|
|
@@ -699,16 +856,25 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
699
856
|
request,
|
|
700
857
|
meta,
|
|
701
858
|
pluginCtx,
|
|
702
|
-
|
|
859
|
+
wallet,
|
|
703
860
|
account,
|
|
704
861
|
body.data
|
|
705
862
|
);
|
|
706
863
|
if (response.status < 400) {
|
|
707
864
|
try {
|
|
865
|
+
const payloadFingerprint = typeof verifyPayload === "object" && verifyPayload !== null ? {
|
|
866
|
+
keys: Object.keys(verifyPayload).sort().join(","),
|
|
867
|
+
payloadType: typeof verifyPayload
|
|
868
|
+
} : { payloadType: typeof verifyPayload };
|
|
869
|
+
console.info("Settlement attempt", {
|
|
870
|
+
route: routeEntry.key,
|
|
871
|
+
network: deps.network,
|
|
872
|
+
...payloadFingerprint
|
|
873
|
+
});
|
|
708
874
|
const settle = await settleX402Payment(
|
|
709
875
|
deps.x402Server,
|
|
710
|
-
|
|
711
|
-
|
|
876
|
+
verifyPayload,
|
|
877
|
+
verifyRequirements
|
|
712
878
|
);
|
|
713
879
|
response.headers.set("PAYMENT-RESPONSE", settle.encoded);
|
|
714
880
|
firePluginHook(deps.plugin, "onPaymentSettled", pluginCtx, {
|
|
@@ -718,6 +884,14 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
718
884
|
network: deps.network
|
|
719
885
|
});
|
|
720
886
|
} catch (err) {
|
|
887
|
+
const errObj = err;
|
|
888
|
+
console.error("Settlement failed", {
|
|
889
|
+
message: err instanceof Error ? err.message : String(err),
|
|
890
|
+
route: routeEntry.key,
|
|
891
|
+
network: deps.network,
|
|
892
|
+
facilitatorStatus: errObj.response?.status,
|
|
893
|
+
facilitatorBody: errObj.response?.data ?? errObj.response?.body
|
|
894
|
+
});
|
|
721
895
|
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
722
896
|
level: "critical",
|
|
723
897
|
message: `Settlement failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -733,7 +907,7 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
733
907
|
if (!deps.mppConfig) return await build402(request, routeEntry, deps, meta, pluginCtx);
|
|
734
908
|
const verify = await verifyMPPCredential(request, routeEntry, deps.mppConfig, price);
|
|
735
909
|
if (!verify?.valid) return await build402(request, routeEntry, deps, meta, pluginCtx);
|
|
736
|
-
const wallet = verify.payer;
|
|
910
|
+
const wallet = verify.payer.toLowerCase();
|
|
737
911
|
pluginCtx.setVerifiedWallet(wallet);
|
|
738
912
|
firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
|
|
739
913
|
protocol: "mpp",
|
|
@@ -992,6 +1166,8 @@ var RouteBuilder = class {
|
|
|
992
1166
|
_providerName;
|
|
993
1167
|
/** @internal */
|
|
994
1168
|
_providerConfig;
|
|
1169
|
+
/** @internal */
|
|
1170
|
+
_validateFn;
|
|
995
1171
|
constructor(key, registry, deps) {
|
|
996
1172
|
this._key = key;
|
|
997
1173
|
this._registry = registry;
|
|
@@ -1004,6 +1180,11 @@ var RouteBuilder = class {
|
|
|
1004
1180
|
return next;
|
|
1005
1181
|
}
|
|
1006
1182
|
paid(pricing, options) {
|
|
1183
|
+
if (this._authMode === "siwx") {
|
|
1184
|
+
throw new Error(
|
|
1185
|
+
`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.`
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1007
1188
|
const next = this.fork();
|
|
1008
1189
|
next._authMode = "paid";
|
|
1009
1190
|
next._pricing = pricing;
|
|
@@ -1033,6 +1214,11 @@ var RouteBuilder = class {
|
|
|
1033
1214
|
return next;
|
|
1034
1215
|
}
|
|
1035
1216
|
siwx() {
|
|
1217
|
+
if (this._authMode === "paid") {
|
|
1218
|
+
throw new Error(
|
|
1219
|
+
`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.`
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1036
1222
|
const next = this.fork();
|
|
1037
1223
|
next._authMode = "siwx";
|
|
1038
1224
|
next._protocols = [];
|
|
@@ -1093,7 +1279,41 @@ var RouteBuilder = class {
|
|
|
1093
1279
|
next._method = m;
|
|
1094
1280
|
return next;
|
|
1095
1281
|
}
|
|
1282
|
+
// -------------------------------------------------------------------------
|
|
1283
|
+
// Pre-payment validation
|
|
1284
|
+
// -------------------------------------------------------------------------
|
|
1285
|
+
/**
|
|
1286
|
+
* Add pre-payment validation that runs after body parsing but before the 402
|
|
1287
|
+
* challenge is shown. Use this for async business logic like "is this resource
|
|
1288
|
+
* available?" or "has this user hit their rate limit?".
|
|
1289
|
+
*
|
|
1290
|
+
* Requires `.body()` — call `.body()` before `.validate()` for type inference.
|
|
1291
|
+
*
|
|
1292
|
+
* @example
|
|
1293
|
+
* ```typescript
|
|
1294
|
+
* router
|
|
1295
|
+
* .route('domain/register')
|
|
1296
|
+
* .paid(calculatePrice)
|
|
1297
|
+
* .body(RegisterSchema) // .body() first for type inference
|
|
1298
|
+
* .validate(async (body) => {
|
|
1299
|
+
* if (await isDomainTaken(body.domain)) {
|
|
1300
|
+
* throw Object.assign(new Error('Domain taken'), { status: 409 });
|
|
1301
|
+
* }
|
|
1302
|
+
* })
|
|
1303
|
+
* .handler(async ({ body }) => { ... });
|
|
1304
|
+
* ```
|
|
1305
|
+
*/
|
|
1306
|
+
validate(fn) {
|
|
1307
|
+
const next = this.fork();
|
|
1308
|
+
next._validateFn = fn;
|
|
1309
|
+
return next;
|
|
1310
|
+
}
|
|
1096
1311
|
handler(fn) {
|
|
1312
|
+
if (this._validateFn && !this._bodySchema) {
|
|
1313
|
+
throw new Error(
|
|
1314
|
+
`route '${this._key}': .validate() requires .body() \u2014 validation runs on parsed body`
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1097
1317
|
const entry = {
|
|
1098
1318
|
key: this._key,
|
|
1099
1319
|
authMode: this._authMode,
|
|
@@ -1108,30 +1328,14 @@ var RouteBuilder = class {
|
|
|
1108
1328
|
maxPrice: this._maxPrice,
|
|
1109
1329
|
apiKeyResolver: this._apiKeyResolver,
|
|
1110
1330
|
providerName: this._providerName,
|
|
1111
|
-
providerConfig: this._providerConfig
|
|
1331
|
+
providerConfig: this._providerConfig,
|
|
1332
|
+
validateFn: this._validateFn
|
|
1112
1333
|
};
|
|
1113
1334
|
this._registry.register(entry);
|
|
1114
1335
|
return createRequestHandler(entry, fn, this._deps);
|
|
1115
1336
|
}
|
|
1116
1337
|
};
|
|
1117
1338
|
|
|
1118
|
-
// src/auth/nonce.ts
|
|
1119
|
-
var MemoryNonceStore = class {
|
|
1120
|
-
seen = /* @__PURE__ */ new Map();
|
|
1121
|
-
async check(nonce) {
|
|
1122
|
-
this.evict();
|
|
1123
|
-
if (this.seen.has(nonce)) return false;
|
|
1124
|
-
this.seen.set(nonce, Date.now() + 5 * 60 * 1e3);
|
|
1125
|
-
return true;
|
|
1126
|
-
}
|
|
1127
|
-
evict() {
|
|
1128
|
-
const now = Date.now();
|
|
1129
|
-
for (const [n, exp] of this.seen) {
|
|
1130
|
-
if (exp < now) this.seen.delete(n);
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
};
|
|
1134
|
-
|
|
1135
1339
|
// src/discovery/well-known.ts
|
|
1136
1340
|
var import_server4 = require("next/server");
|
|
1137
1341
|
function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
|
|
@@ -1364,6 +1568,9 @@ function createRouter(config) {
|
|
|
1364
1568
|
MemoryNonceStore,
|
|
1365
1569
|
RouteBuilder,
|
|
1366
1570
|
RouteRegistry,
|
|
1571
|
+
SIWX_CHALLENGE_EXPIRY_MS,
|
|
1572
|
+
SIWX_ERROR_MESSAGES,
|
|
1367
1573
|
consolePlugin,
|
|
1574
|
+
createRedisNonceStore,
|
|
1368
1575
|
createRouter
|
|
1369
1576
|
});
|
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;
|
|
@@ -171,6 +221,7 @@ interface RouteEntry {
|
|
|
171
221
|
apiKeyResolver?: (key: string) => unknown | Promise<unknown>;
|
|
172
222
|
providerName?: string;
|
|
173
223
|
providerConfig?: ProviderConfig;
|
|
224
|
+
validateFn?: (body: unknown) => void | Promise<void>;
|
|
174
225
|
}
|
|
175
226
|
interface RouterConfig {
|
|
176
227
|
payeeAddress: string;
|
|
@@ -266,6 +317,7 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, HasAuth extend
|
|
|
266
317
|
/** @internal */ _apiKeyResolver: ((key: string) => unknown | Promise<unknown>) | undefined;
|
|
267
318
|
/** @internal */ _providerName: string | undefined;
|
|
268
319
|
/** @internal */ _providerConfig: ProviderConfig | undefined;
|
|
320
|
+
/** @internal */ _validateFn: ((body: TBody) => void | Promise<void>) | undefined;
|
|
269
321
|
constructor(key: string, registry: RouteRegistry, deps: OrchestrateDeps);
|
|
270
322
|
private fork;
|
|
271
323
|
paid(pricing: string, options?: PaidOptions): RouteBuilder<TBody, TQuery, True, False, HasBody>;
|
|
@@ -290,6 +342,28 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, HasAuth extend
|
|
|
290
342
|
description(text: string): this;
|
|
291
343
|
path(p: string): this;
|
|
292
344
|
method(m: 'GET' | 'POST' | 'DELETE' | 'PUT' | 'PATCH'): this;
|
|
345
|
+
/**
|
|
346
|
+
* Add pre-payment validation that runs after body parsing but before the 402
|
|
347
|
+
* challenge is shown. Use this for async business logic like "is this resource
|
|
348
|
+
* available?" or "has this user hit their rate limit?".
|
|
349
|
+
*
|
|
350
|
+
* Requires `.body()` — call `.body()` before `.validate()` for type inference.
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* ```typescript
|
|
354
|
+
* router
|
|
355
|
+
* .route('domain/register')
|
|
356
|
+
* .paid(calculatePrice)
|
|
357
|
+
* .body(RegisterSchema) // .body() first for type inference
|
|
358
|
+
* .validate(async (body) => {
|
|
359
|
+
* if (await isDomainTaken(body.domain)) {
|
|
360
|
+
* throw Object.assign(new Error('Domain taken'), { status: 409 });
|
|
361
|
+
* }
|
|
362
|
+
* })
|
|
363
|
+
* .handler(async ({ body }) => { ... });
|
|
364
|
+
* ```
|
|
365
|
+
*/
|
|
366
|
+
validate(fn: (body: TBody) => void | Promise<void>): RouteBuilder<TBody, TQuery, HasAuth, NeedsBody, HasBody>;
|
|
293
367
|
handler(this: RouteBuilder<TBody, TQuery, True, true, false>, fn: never): never;
|
|
294
368
|
handler(this: RouteBuilder<TBody, TQuery, false, boolean, boolean>, fn: never): never;
|
|
295
369
|
handler(this: RouteBuilder<TBody, TQuery, True, False, HasBody>, fn: (ctx: HandlerContext<TBody, TQuery>) => Promise<unknown>): (request: NextRequest) => Promise<Response>;
|
|
@@ -313,4 +387,4 @@ interface ServiceRouter {
|
|
|
313
387
|
}
|
|
314
388
|
declare function createRouter(config: RouterConfig): ServiceRouter;
|
|
315
389
|
|
|
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 };
|
|
390
|
+
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 };
|