@agentcash/router 0.3.0 → 0.4.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/.claude/CLAUDE.md +86 -0
- package/.claude/skills/router-guide/SKILL.md +533 -0
- package/README.md +52 -4
- package/dist/index.cjs +166 -66
- package/dist/index.d.cts +12 -7
- package/dist/index.d.ts +12 -7
- package/dist/index.js +152 -52
- package/package.json +5 -7
package/dist/index.js
CHANGED
|
@@ -320,63 +320,106 @@ async function settleX402Payment(server, payload, requirements) {
|
|
|
320
320
|
}
|
|
321
321
|
|
|
322
322
|
// src/protocols/mpp.ts
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
mpayLoaded = true;
|
|
338
|
-
} catch {
|
|
339
|
-
throw new Error("mpay package is required for MPP protocol support. Install it: pnpm add mpay");
|
|
323
|
+
import { Challenge, Credential, Receipt } from "mpay";
|
|
324
|
+
import { tempo } from "mpay/server";
|
|
325
|
+
import { createClient, http } from "viem";
|
|
326
|
+
import { tempo as tempoChain } from "viem/chains";
|
|
327
|
+
function buildGetClient(rpcUrl) {
|
|
328
|
+
const url = rpcUrl ?? process.env.TEMPO_RPC_URL;
|
|
329
|
+
if (!url) return {};
|
|
330
|
+
return {
|
|
331
|
+
getClient: () => createClient({ chain: tempoChain, transport: http(url) })
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function toStandardRequest(request) {
|
|
335
|
+
if (request.constructor.name === "Request") {
|
|
336
|
+
return request;
|
|
340
337
|
}
|
|
338
|
+
return new Request(request.url, {
|
|
339
|
+
method: request.method,
|
|
340
|
+
headers: request.headers,
|
|
341
|
+
body: request.body,
|
|
342
|
+
// @ts-expect-error - Request.duplex is required for streaming bodies but not in types yet
|
|
343
|
+
duplex: "half"
|
|
344
|
+
});
|
|
341
345
|
}
|
|
346
|
+
var DEFAULT_DECIMALS = 6;
|
|
342
347
|
async function buildMPPChallenge(routeEntry, request, mppConfig, price) {
|
|
343
|
-
|
|
348
|
+
const standardRequest = toStandardRequest(request);
|
|
349
|
+
const currency = mppConfig.currency;
|
|
350
|
+
const recipient = mppConfig.recipient ?? "";
|
|
344
351
|
const methodIntent = tempo.charge({
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
recipient: mppConfig.recipient ?? ""
|
|
352
|
+
currency,
|
|
353
|
+
recipient
|
|
348
354
|
});
|
|
349
355
|
const challenge = Challenge.fromIntent(methodIntent, {
|
|
350
356
|
secretKey: mppConfig.secretKey,
|
|
351
|
-
realm: new URL(
|
|
352
|
-
request
|
|
357
|
+
realm: new URL(standardRequest.url).origin,
|
|
358
|
+
request: {
|
|
359
|
+
amount: price,
|
|
360
|
+
currency,
|
|
361
|
+
recipient,
|
|
362
|
+
decimals: DEFAULT_DECIMALS
|
|
363
|
+
}
|
|
353
364
|
});
|
|
354
365
|
return Challenge.serialize(challenge);
|
|
355
366
|
}
|
|
356
367
|
async function verifyMPPCredential(request, _routeEntry, mppConfig, price) {
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
368
|
+
const standardRequest = toStandardRequest(request);
|
|
369
|
+
const currency = mppConfig.currency;
|
|
370
|
+
const recipient = mppConfig.recipient ?? "";
|
|
371
|
+
try {
|
|
372
|
+
const authHeader = standardRequest.headers.get("Authorization");
|
|
373
|
+
if (!authHeader) {
|
|
374
|
+
console.error("[MPP] No Authorization header found");
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
const credential = Credential.fromRequest(standardRequest);
|
|
378
|
+
if (!credential?.challenge) {
|
|
379
|
+
console.error("[MPP] Invalid credential structure");
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
const isValid = Challenge.verify(credential.challenge, { secretKey: mppConfig.secretKey });
|
|
383
|
+
if (!isValid) {
|
|
384
|
+
console.error("[MPP] Challenge HMAC verification failed");
|
|
385
|
+
return { valid: false, payer: null };
|
|
386
|
+
}
|
|
387
|
+
const methodIntent = tempo.charge({
|
|
388
|
+
currency,
|
|
389
|
+
recipient,
|
|
390
|
+
...buildGetClient(mppConfig.rpcUrl)
|
|
391
|
+
});
|
|
392
|
+
const paymentRequest = {
|
|
393
|
+
amount: price,
|
|
394
|
+
currency,
|
|
395
|
+
recipient,
|
|
396
|
+
decimals: DEFAULT_DECIMALS
|
|
397
|
+
};
|
|
398
|
+
const resolvedRequest = methodIntent.request ? await methodIntent.request({ credential, request: paymentRequest }) : paymentRequest;
|
|
399
|
+
const receipt = await methodIntent.verify({
|
|
400
|
+
credential,
|
|
401
|
+
request: resolvedRequest
|
|
402
|
+
});
|
|
403
|
+
if (!receipt || receipt.status !== "success") {
|
|
404
|
+
console.error("[MPP] Tempo verification failed:", receipt);
|
|
405
|
+
return { valid: false, payer: null };
|
|
406
|
+
}
|
|
407
|
+
const payer = receipt.reference ?? "";
|
|
408
|
+
return {
|
|
409
|
+
valid: true,
|
|
410
|
+
payer,
|
|
411
|
+
txHash: receipt.reference
|
|
412
|
+
};
|
|
413
|
+
} catch (error) {
|
|
414
|
+
console.error("[MPP] Credential verification error:", {
|
|
415
|
+
message: error instanceof Error ? error.message : String(error),
|
|
416
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
417
|
+
errorType: error?.constructor?.name
|
|
418
|
+
});
|
|
419
|
+
return null;
|
|
372
420
|
}
|
|
373
|
-
return {
|
|
374
|
-
valid: true,
|
|
375
|
-
payer: verifyResult.payer
|
|
376
|
-
};
|
|
377
421
|
}
|
|
378
|
-
|
|
379
|
-
await ensureMpay();
|
|
422
|
+
function buildMPPReceipt(reference) {
|
|
380
423
|
const receipt = Receipt.from({
|
|
381
424
|
method: "tempo",
|
|
382
425
|
status: "success",
|
|
@@ -489,7 +532,7 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
489
532
|
if (routeEntry.authMode === "unprotected") {
|
|
490
533
|
return handleAuth(null, void 0);
|
|
491
534
|
}
|
|
492
|
-
let account
|
|
535
|
+
let account;
|
|
493
536
|
if (routeEntry.authMode === "apiKey" || routeEntry.apiKeyResolver) {
|
|
494
537
|
if (!routeEntry.apiKeyResolver) {
|
|
495
538
|
return fail(401, "API key resolver not configured", meta, pluginCtx);
|
|
@@ -504,6 +547,16 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
504
547
|
}
|
|
505
548
|
}
|
|
506
549
|
const protocol = detectProtocol(request);
|
|
550
|
+
let earlyBodyData;
|
|
551
|
+
if (!protocol && typeof routeEntry.pricing === "function" && routeEntry.bodySchema) {
|
|
552
|
+
const requestForPricing = request.clone();
|
|
553
|
+
const earlyBodyResult = await parseBody(requestForPricing, routeEntry);
|
|
554
|
+
if (!earlyBodyResult.ok) {
|
|
555
|
+
firePluginResponse(deps, pluginCtx, meta, earlyBodyResult.response);
|
|
556
|
+
return earlyBodyResult.response;
|
|
557
|
+
}
|
|
558
|
+
earlyBodyData = earlyBodyResult.data;
|
|
559
|
+
}
|
|
507
560
|
if (routeEntry.authMode === "siwx") {
|
|
508
561
|
if (!request.headers.get("SIGN-IN-WITH-X")) {
|
|
509
562
|
const url = new URL(request.url);
|
|
@@ -572,7 +625,7 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
572
625
|
return handleAuth(siwx.wallet, void 0);
|
|
573
626
|
}
|
|
574
627
|
if (!protocol || protocol === "siwx") {
|
|
575
|
-
return await build402(request, routeEntry, deps, meta, pluginCtx);
|
|
628
|
+
return await build402(request, routeEntry, deps, meta, pluginCtx, earlyBodyData);
|
|
576
629
|
}
|
|
577
630
|
const body = await parseBody(request, routeEntry);
|
|
578
631
|
if (!body.ok) {
|
|
@@ -710,10 +763,60 @@ function parseQuery(request, routeEntry) {
|
|
|
710
763
|
const result = routeEntry.querySchema.safeParse(params);
|
|
711
764
|
return result.success ? result.data : params;
|
|
712
765
|
}
|
|
713
|
-
async function
|
|
714
|
-
|
|
766
|
+
async function resolveDynamicPrice(bodyData, routeEntry, deps, pluginCtx, meta) {
|
|
767
|
+
try {
|
|
768
|
+
let price = await resolvePrice(routeEntry.pricing, bodyData);
|
|
769
|
+
if (routeEntry.maxPrice) {
|
|
770
|
+
const calculated = parseFloat(price);
|
|
771
|
+
const max = parseFloat(routeEntry.maxPrice);
|
|
772
|
+
if (calculated > max) {
|
|
773
|
+
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
774
|
+
level: "warn",
|
|
775
|
+
message: `Price ${price} exceeds maxPrice ${routeEntry.maxPrice}, capping`,
|
|
776
|
+
route: routeEntry.key,
|
|
777
|
+
meta: { calculated: price, maxPrice: routeEntry.maxPrice, body: bodyData }
|
|
778
|
+
});
|
|
779
|
+
price = routeEntry.maxPrice;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return { price };
|
|
783
|
+
} catch (err) {
|
|
784
|
+
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
785
|
+
level: "error",
|
|
786
|
+
message: `Pricing function failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
787
|
+
route: routeEntry.key,
|
|
788
|
+
meta: { error: err instanceof Error ? err.stack : String(err), body: bodyData }
|
|
789
|
+
});
|
|
790
|
+
if (routeEntry.maxPrice) {
|
|
791
|
+
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
792
|
+
level: "warn",
|
|
793
|
+
message: `Using maxPrice ${routeEntry.maxPrice} as fallback after pricing error`,
|
|
794
|
+
route: routeEntry.key
|
|
795
|
+
});
|
|
796
|
+
return { price: routeEntry.maxPrice };
|
|
797
|
+
} else {
|
|
798
|
+
const errorResponse = NextResponse2.json(
|
|
799
|
+
{ success: false, error: "Price calculation failed" },
|
|
800
|
+
{ status: 500 }
|
|
801
|
+
);
|
|
802
|
+
firePluginResponse(deps, pluginCtx, meta, errorResponse);
|
|
803
|
+
return { error: errorResponse };
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
async function build402(request, routeEntry, deps, meta, pluginCtx, bodyData) {
|
|
808
|
+
const response = new NextResponse2(null, {
|
|
809
|
+
status: 402,
|
|
810
|
+
headers: {
|
|
811
|
+
"Content-Type": "application/json"
|
|
812
|
+
}
|
|
813
|
+
});
|
|
715
814
|
let challengePrice;
|
|
716
|
-
if (routeEntry.
|
|
815
|
+
if (bodyData !== void 0 && typeof routeEntry.pricing === "function") {
|
|
816
|
+
const result = await resolveDynamicPrice(bodyData, routeEntry, deps, pluginCtx, meta);
|
|
817
|
+
if ("error" in result) return result.error;
|
|
818
|
+
challengePrice = result.price;
|
|
819
|
+
} else if (routeEntry.maxPrice) {
|
|
717
820
|
challengePrice = routeEntry.maxPrice;
|
|
718
821
|
} else if (routeEntry.pricing) {
|
|
719
822
|
try {
|
|
@@ -874,9 +977,6 @@ var RouteBuilder = class {
|
|
|
874
977
|
next._pricing = pricing;
|
|
875
978
|
if (options?.protocols) next._protocols = options.protocols;
|
|
876
979
|
if (options?.maxPrice) next._maxPrice = options.maxPrice;
|
|
877
|
-
if (typeof pricing === "function" && !options?.maxPrice) {
|
|
878
|
-
throw new Error(`route '${this._key}': dynamic pricing requires maxPrice option`);
|
|
879
|
-
}
|
|
880
980
|
if (typeof pricing === "object" && "tiers" in pricing) {
|
|
881
981
|
for (const [tierKey, tierConfig] of Object.entries(pricing.tiers)) {
|
|
882
982
|
if (!tierKey) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentcash/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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": {
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
"module": "./dist/index.js",
|
|
20
20
|
"types": "./dist/index.d.ts",
|
|
21
21
|
"files": [
|
|
22
|
-
"dist"
|
|
22
|
+
"dist",
|
|
23
|
+
".claude/CLAUDE.md",
|
|
24
|
+
".claude/skills"
|
|
23
25
|
],
|
|
24
26
|
"scripts": {
|
|
25
27
|
"build": "tsup",
|
|
@@ -42,11 +44,6 @@
|
|
|
42
44
|
"zod": "^4.0.0",
|
|
43
45
|
"zod-openapi": "^5.0.0"
|
|
44
46
|
},
|
|
45
|
-
"peerDependenciesMeta": {
|
|
46
|
-
"mpay": {
|
|
47
|
-
"optional": true
|
|
48
|
-
}
|
|
49
|
-
},
|
|
50
47
|
"devDependencies": {
|
|
51
48
|
"@coinbase/x402": "^2.1.0",
|
|
52
49
|
"@eslint/js": "^10.0.1",
|
|
@@ -62,6 +59,7 @@
|
|
|
62
59
|
"tsup": "^8.0.0",
|
|
63
60
|
"typescript": "^5.8.0",
|
|
64
61
|
"typescript-eslint": "^8.55.0",
|
|
62
|
+
"viem": "^2.0.0",
|
|
65
63
|
"vitest": "^3.0.0",
|
|
66
64
|
"zod": "^4.0.0",
|
|
67
65
|
"zod-openapi": "^5.0.0"
|