@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/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
- var mpayLoaded = false;
324
- var Challenge;
325
- var Credential;
326
- var Receipt;
327
- var tempo;
328
- async function ensureMpay() {
329
- if (mpayLoaded) return;
330
- try {
331
- const mpay = await import("mpay");
332
- Challenge = mpay.Challenge;
333
- Credential = mpay.Credential;
334
- Receipt = mpay.Receipt;
335
- const mpayServer = await import("mpay/server");
336
- tempo = mpayServer.tempo;
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
- await ensureMpay();
348
+ const standardRequest = toStandardRequest(request);
349
+ const currency = mppConfig.currency;
350
+ const recipient = mppConfig.recipient ?? "";
344
351
  const methodIntent = tempo.charge({
345
- amount: price,
346
- currency: mppConfig.currency,
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(request.url).origin,
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
- await ensureMpay();
358
- const credential = Credential.fromRequest(request);
359
- if (!credential) return null;
360
- const isValid = Challenge.verify(credential.challenge, { secretKey: mppConfig.secretKey });
361
- if (!isValid) {
362
- return { valid: false, payer: null };
363
- }
364
- const chargeConfig = {
365
- amount: price,
366
- currency: mppConfig.currency,
367
- recipient: mppConfig.recipient ?? ""
368
- };
369
- const verifyResult = await tempo.charge(chargeConfig).verify(credential);
370
- if (!verifyResult?.valid) {
371
- return { valid: false, payer: null };
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
- async function buildMPPReceipt(reference) {
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 = void 0;
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 build402(request, routeEntry, deps, meta, pluginCtx) {
714
- const response = new NextResponse2(null, { status: 402 });
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.maxPrice) {
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.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"