@agentcash/router 1.8.0 → 1.9.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
@@ -563,7 +563,8 @@ function preflight(routeEntry, handler, deps, request) {
563
563
  request,
564
564
  meta,
565
565
  pluginCtx,
566
- report: createReporter(deps.plugin, pluginCtx, routeEntry.key)
566
+ report: createReporter(deps.plugin, pluginCtx, routeEntry.key),
567
+ query: void 0
567
568
  };
568
569
  }
569
570
  function buildMeta(request, routeEntry) {
@@ -586,13 +587,19 @@ function buildMeta(request, routeEntry) {
586
587
  import { NextResponse } from "next/server";
587
588
 
588
589
  // src/pipeline/body.ts
590
+ var MalformedJsonError = class extends Error {
591
+ constructor() {
592
+ super("Invalid JSON");
593
+ this.name = "MalformedJsonError";
594
+ }
595
+ };
589
596
  async function bufferBody(request) {
590
597
  const text = await request.text();
591
- if (!text) return void 0;
598
+ if (!text.trim()) return void 0;
592
599
  try {
593
600
  return JSON.parse(text);
594
601
  } catch {
595
- return void 0;
602
+ throw new MalformedJsonError();
596
603
  }
597
604
  }
598
605
  function validateBody(parsed, schema) {
@@ -673,7 +680,18 @@ function computeQuotaLevel(remaining, warn, critical) {
673
680
  // src/pipeline/steps/parse-body.ts
674
681
  async function parseBody(ctx, request = ctx.request) {
675
682
  if (!ctx.routeEntry.bodySchema) return { ok: true, data: void 0 };
676
- const raw = await bufferBody(request);
683
+ let raw;
684
+ try {
685
+ raw = await bufferBody(request);
686
+ } catch (err) {
687
+ if (!(err instanceof MalformedJsonError)) throw err;
688
+ const response2 = NextResponse.json(
689
+ { success: false, error: "Invalid JSON", issues: [] },
690
+ { status: 400 }
691
+ );
692
+ firePluginResponse(ctx, response2);
693
+ return { ok: false, response: response2 };
694
+ }
677
695
  const result = validateBody(raw, ctx.routeEntry.bodySchema);
678
696
  if (result.success) return { ok: true, data: result.data };
679
697
  const response = NextResponse.json(
@@ -684,6 +702,22 @@ async function parseBody(ctx, request = ctx.request) {
684
702
  return { ok: false, response };
685
703
  }
686
704
 
705
+ // src/pipeline/steps/parse-query.ts
706
+ import { NextResponse as NextResponse2 } from "next/server";
707
+ function validateQuery(ctx) {
708
+ const { querySchema } = ctx.routeEntry;
709
+ if (!querySchema) return { ok: true, data: void 0 };
710
+ const params = Object.fromEntries(ctx.request.nextUrl.searchParams.entries());
711
+ const result = validateBody(params, querySchema);
712
+ if (result.success) return { ok: true, data: result.data };
713
+ const response = NextResponse2.json(
714
+ { success: false, error: result.error, issues: result.issues },
715
+ { status: 400 }
716
+ );
717
+ firePluginResponse(ctx, response);
718
+ return { ok: false, response };
719
+ }
720
+
687
721
  // src/pipeline/steps/errors.ts
688
722
  function errorStatus(error, fallback) {
689
723
  const status = error?.status;
@@ -698,9 +732,9 @@ function handlerFailureError(response) {
698
732
  }
699
733
 
700
734
  // src/pipeline/steps/fail.ts
701
- import { NextResponse as NextResponse2 } from "next/server";
735
+ import { NextResponse as NextResponse3 } from "next/server";
702
736
  function fail(ctx, status, message, requestBody) {
703
- const response = NextResponse2.json({ success: false, error: message }, { status });
737
+ const response = NextResponse3.json({ success: false, error: message }, { status });
704
738
  firePluginResponse(ctx, response, requestBody);
705
739
  return response;
706
740
  }
@@ -717,7 +751,7 @@ async function runValidate(ctx, body) {
717
751
  }
718
752
 
719
753
  // src/pipeline/flows/static/static-invoke.ts
720
- import { NextResponse as NextResponse3 } from "next/server";
754
+ import { NextResponse as NextResponse4 } from "next/server";
721
755
 
722
756
  // src/types.ts
723
757
  var HttpError = class extends Error {
@@ -728,14 +762,6 @@ var HttpError = class extends Error {
728
762
  }
729
763
  };
730
764
 
731
- // src/pipeline/steps/parse-query.ts
732
- function parseQuery(request, routeEntry) {
733
- if (!routeEntry.querySchema) return void 0;
734
- const params = Object.fromEntries(request.nextUrl.searchParams.entries());
735
- const result = routeEntry.querySchema.safeParse(params);
736
- return result.success ? result.data : params;
737
- }
738
-
739
765
  // src/pipeline/flows/static/static-invoke.ts
740
766
  function invokePaidStatic(ctx, wallet, account, body, payment) {
741
767
  return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, payment));
@@ -746,7 +772,7 @@ function invokeUnauthed(ctx, wallet, account, body) {
746
772
  function buildHandlerCtx(ctx, wallet, account, body, payment) {
747
773
  return {
748
774
  body,
749
- query: parseQuery(ctx.request, ctx.routeEntry),
775
+ query: ctx.query,
750
776
  request: ctx.request,
751
777
  requestId: ctx.meta.requestId,
752
778
  route: ctx.routeEntry.key,
@@ -775,14 +801,14 @@ async function runHandler(ctx, handlerCtx) {
775
801
  } catch (error) {
776
802
  return errorResult(error);
777
803
  }
778
- const response = rawResult instanceof Response ? rawResult : NextResponse3.json(rawResult);
804
+ const response = rawResult instanceof Response ? rawResult : NextResponse4.json(rawResult);
779
805
  return { response, rawResult };
780
806
  }
781
807
  function errorResult(error) {
782
808
  const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
783
809
  const message = error instanceof Error ? error.message : "Internal error";
784
810
  return {
785
- response: NextResponse3.json({ success: false, error: message }, { status }),
811
+ response: NextResponse4.json({ success: false, error: message }, { status }),
786
812
  rawResult: void 0,
787
813
  handlerError: error
788
814
  };
@@ -1195,9 +1221,10 @@ var DynamicPricing = class {
1195
1221
  }
1196
1222
  needsBody = true;
1197
1223
  async quote(body) {
1224
+ let priced;
1198
1225
  try {
1199
1226
  const raw = await this.opts.fn(body);
1200
- return this.cap(raw, body);
1227
+ priced = this.cap(raw, body);
1201
1228
  } catch (err) {
1202
1229
  if (err instanceof HttpError) throw err;
1203
1230
  this.alert("error", `Pricing function failed: ${msg(err)}`, {
@@ -1210,6 +1237,13 @@ var DynamicPricing = class {
1210
1237
  }
1211
1238
  throw err;
1212
1239
  }
1240
+ if (!isPositiveDecimal(priced)) {
1241
+ throw new HttpError(
1242
+ `route '${this.opts.route ?? "unknown"}': dynamic pricing returned an invalid amount '${priced}'`,
1243
+ 500
1244
+ );
1245
+ }
1246
+ return priced;
1213
1247
  }
1214
1248
  challengeQuote(body) {
1215
1249
  if (body === void 0) return Promise.resolve(this.opts.maxPrice ?? "0");
@@ -1284,14 +1318,14 @@ var TieredPricing = class {
1284
1318
  `Unknown tier '${tierKey}' for field '${field}'. Valid tiers: ${Object.keys(tiers).join(", ")}`
1285
1319
  );
1286
1320
  }
1287
- challengeQuote(body) {
1321
+ async challengeQuote(body) {
1288
1322
  if (body !== void 0) {
1289
1323
  try {
1290
- return this.quote(body);
1324
+ return await this.quote(body);
1291
1325
  } catch {
1292
1326
  }
1293
1327
  }
1294
- return Promise.resolve(this.maxTierPrice());
1328
+ return this.maxTierPrice();
1295
1329
  }
1296
1330
  describe() {
1297
1331
  return {
@@ -2025,7 +2059,7 @@ async function settleX402Payment(server, payload, requirements, amountOverride)
2025
2059
  };
2026
2060
  }
2027
2061
  function tagBareDecimalAsDollars(amount) {
2028
- if (/^\d+\.\d+$/.test(amount)) return `$${amount}`;
2062
+ if (/^\d+(?:\.\d+)?$/.test(amount)) return `$${amount}`;
2029
2063
  return amount;
2030
2064
  }
2031
2065
 
@@ -2272,8 +2306,8 @@ function getAllowedStrategies(allowed) {
2272
2306
  return allowed.map((name) => STRATEGIES[name]);
2273
2307
  }
2274
2308
 
2275
- // src/pipeline/flows/build402.ts
2276
- import { NextResponse as NextResponse4 } from "next/server";
2309
+ // src/pipeline/flows/challenge-response.ts
2310
+ import { NextResponse as NextResponse5 } from "next/server";
2277
2311
 
2278
2312
  // src/pipeline/challenge-extensions.ts
2279
2313
  init_evm();
@@ -2289,20 +2323,17 @@ async function buildChallengeExtensions(ctx) {
2289
2323
  });
2290
2324
  const inputSchema = routeEntry.bodySchema ? toJSON(routeEntry.bodySchema) : routeEntry.querySchema ? toJSON(routeEntry.querySchema) : void 0;
2291
2325
  const outputSchema = routeEntry.outputSchema ? toJSON(routeEntry.outputSchema) : void 0;
2292
- if (inputSchema) {
2293
- const config = {
2294
- method: routeEntry.method,
2295
- bodyType: routeEntry.bodySchema ? "json" : void 0,
2296
- inputSchema
2297
- };
2298
- if (routeEntry.inputExample !== void 0) {
2299
- config.input = routeEntry.inputExample;
2300
- }
2301
- if (outputSchema && routeEntry.outputExample !== void 0) {
2302
- config.output = { schema: outputSchema, example: routeEntry.outputExample };
2303
- }
2304
- extensions = declareDiscoveryExtension(config);
2326
+ const isBodyMethod = routeEntry.method === "POST" || routeEntry.method === "PUT" || routeEntry.method === "PATCH";
2327
+ const config = { method: routeEntry.method };
2328
+ if (isBodyMethod) config.bodyType = "json";
2329
+ if (inputSchema) config.inputSchema = inputSchema;
2330
+ if (routeEntry.inputExample !== void 0) {
2331
+ config.input = routeEntry.inputExample;
2332
+ }
2333
+ if (outputSchema && routeEntry.outputExample !== void 0) {
2334
+ config.output = { schema: outputSchema, example: routeEntry.outputExample };
2305
2335
  }
2336
+ extensions = declareDiscoveryExtension(config);
2306
2337
  } catch (err) {
2307
2338
  ctx.report(
2308
2339
  "warn",
@@ -2339,14 +2370,14 @@ async function buildChallengeExtensions(ctx) {
2339
2370
  return extensions;
2340
2371
  }
2341
2372
 
2342
- // src/pipeline/flows/build402.ts
2343
- async function build402(ctx, pricing, body, failure) {
2373
+ // src/pipeline/flows/challenge-response.ts
2374
+ async function buildChallengeResponse(ctx, pricing, body, failure) {
2344
2375
  let challengePrice;
2345
2376
  try {
2346
2377
  challengePrice = pricing ? await pricing.challengeQuote(body) : "0";
2347
2378
  } catch (err) {
2348
2379
  const message = errorMessage(err, "Price calculation failed");
2349
- const errorResponse = NextResponse4.json(
2380
+ const errorResponse = NextResponse5.json(
2350
2381
  { success: false, error: message },
2351
2382
  { status: errorStatus(err, 500) }
2352
2383
  );
@@ -2355,7 +2386,7 @@ async function build402(ctx, pricing, body, failure) {
2355
2386
  }
2356
2387
  const extensions = await buildChallengeExtensions(ctx);
2357
2388
  const responseBody = failure ? JSON.stringify({ error: failure.message ?? null, reason: failure.reason }) : null;
2358
- const response = new NextResponse4(responseBody, {
2389
+ const response = new NextResponse5(responseBody, {
2359
2390
  status: 402,
2360
2391
  headers: {
2361
2392
  "Content-Type": "application/json",
@@ -2382,7 +2413,7 @@ async function build402(ctx, pricing, body, failure) {
2382
2413
  const message = `${strategy.protocol} challenge build failed: ${errorMessage(err, String(err))}`;
2383
2414
  ctx.report("critical", message);
2384
2415
  if (strategy.protocol === "x402") {
2385
- const errorResponse = NextResponse4.json(
2416
+ const errorResponse = NextResponse5.json(
2386
2417
  { success: false, error: message },
2387
2418
  { status: 500 }
2388
2419
  );
@@ -2434,7 +2465,7 @@ function surrogatePriceForSkippedBody(routeEntry) {
2434
2465
  }
2435
2466
 
2436
2467
  // src/pipeline/flows/dynamic/dynamic-channel-mgmt.ts
2437
- import { NextResponse as NextResponse5 } from "next/server";
2468
+ import { NextResponse as NextResponse6 } from "next/server";
2438
2469
  async function runDynamicChannelMgmtFlow(args) {
2439
2470
  const { ctx, strategy, account, pricing, skipBody } = args;
2440
2471
  const { request, routeEntry, deps, report } = ctx;
@@ -2453,7 +2484,7 @@ async function runDynamicChannelMgmtFlow(args) {
2453
2484
  if (verifyOutcome.kind === "config") {
2454
2485
  return fail(ctx, 500, verifyOutcome.message, parsedBody);
2455
2486
  }
2456
- return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
2487
+ return buildChallengeResponse(ctx, pricing, parsedBody, verifyOutcome.failure);
2457
2488
  }
2458
2489
  ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
2459
2490
  firePaymentVerified(ctx, {
@@ -2462,7 +2493,7 @@ async function runDynamicChannelMgmtFlow(args) {
2462
2493
  amount: price,
2463
2494
  network: verifyOutcome.payment.network
2464
2495
  });
2465
- const synthetic = new NextResponse5(null, { status: 200 });
2496
+ const synthetic = new NextResponse6(null, { status: 200 });
2466
2497
  const settleScope = {
2467
2498
  wallet: verifyOutcome.wallet,
2468
2499
  account,
@@ -2524,11 +2555,11 @@ function createChargeContext(args) {
2524
2555
  }
2525
2556
 
2526
2557
  // src/pipeline/flows/dynamic/dynamic-invoke/shared.ts
2527
- import { NextResponse as NextResponse6 } from "next/server";
2558
+ import { NextResponse as NextResponse7 } from "next/server";
2528
2559
  function buildBaseHandlerCtx(ctx, wallet, account, body, payment) {
2529
2560
  return {
2530
2561
  body,
2531
- query: parseQuery(ctx.request, ctx.routeEntry),
2562
+ query: ctx.query,
2532
2563
  request: ctx.request,
2533
2564
  requestId: ctx.meta.requestId,
2534
2565
  route: ctx.routeEntry.key,
@@ -2540,14 +2571,14 @@ function buildBaseHandlerCtx(ctx, wallet, account, body, payment) {
2540
2571
  };
2541
2572
  }
2542
2573
  function toResponse(rawResult) {
2543
- return rawResult instanceof Response ? rawResult : NextResponse6.json(rawResult);
2574
+ return rawResult instanceof Response ? rawResult : NextResponse7.json(rawResult);
2544
2575
  }
2545
2576
  function errorResult2(error) {
2546
2577
  const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
2547
2578
  const message = error instanceof Error ? error.message : "Internal error";
2548
2579
  return {
2549
2580
  kind: "request",
2550
- response: NextResponse6.json({ success: false, error: message }, { status }),
2581
+ response: NextResponse7.json({ success: false, error: message }, { status }),
2551
2582
  rawResult: void 0,
2552
2583
  handlerError: error
2553
2584
  };
@@ -2750,7 +2781,7 @@ async function runDynamicPaidFlow(ctx) {
2750
2781
  if (!incomingStrategy) {
2751
2782
  const initError = protocolInitError(routeEntry, deps);
2752
2783
  if (initError) return fail(ctx, 500, initError);
2753
- return build402(ctx, pricing, earlyBody);
2784
+ return buildChallengeResponse(ctx, pricing, earlyBody);
2754
2785
  }
2755
2786
  const { skipBody, skipHandler } = resolveDynamicPreflight(incomingStrategy, request, routeEntry);
2756
2787
  if (skipHandler) {
@@ -2777,7 +2808,7 @@ async function runDynamicPaidFlow(ctx) {
2777
2808
  if (verifyOutcome.kind === "config") {
2778
2809
  return fail(ctx, 500, verifyOutcome.message, parsedBody);
2779
2810
  }
2780
- return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
2811
+ return buildChallengeResponse(ctx, pricing, parsedBody, verifyOutcome.failure);
2781
2812
  }
2782
2813
  ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
2783
2814
  firePaymentVerified(ctx, {
@@ -2921,7 +2952,7 @@ async function runStaticPaidFlow(ctx) {
2921
2952
  if (!incomingStrategy) {
2922
2953
  const initError = protocolInitError(routeEntry, deps);
2923
2954
  if (initError) return fail(ctx, 500, initError);
2924
- return build402(ctx, pricing, earlyBody);
2955
+ return buildChallengeResponse(ctx, pricing, earlyBody);
2925
2956
  }
2926
2957
  const bodyAndPrice = await resolveStaticBodyAndPrice({ ctx, pricing });
2927
2958
  if (!bodyAndPrice.ok) return bodyAndPrice.response;
@@ -2938,7 +2969,7 @@ async function runStaticPaidFlow(ctx) {
2938
2969
  if (verifyOutcome.kind === "config") {
2939
2970
  return fail(ctx, 500, verifyOutcome.message, parsedBody);
2940
2971
  }
2941
- return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
2972
+ return buildChallengeResponse(ctx, pricing, parsedBody, verifyOutcome.failure);
2942
2973
  }
2943
2974
  ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
2944
2975
  firePaymentVerified(ctx, {
@@ -2972,7 +3003,7 @@ async function runPaidFlow(ctx) {
2972
3003
  }
2973
3004
 
2974
3005
  // src/pipeline/flows/siwx-only.ts
2975
- import { NextResponse as NextResponse7 } from "next/server";
3006
+ import { NextResponse as NextResponse8 } from "next/server";
2976
3007
 
2977
3008
  // src/kv-store/client.ts
2978
3009
  var BIGINT_SUFFIX = "#__bigint";
@@ -3206,7 +3237,7 @@ async function runSiwxOnlyFlow(ctx) {
3206
3237
  }
3207
3238
  const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
3208
3239
  if (!siwx.valid) {
3209
- const response = NextResponse7.json(
3240
+ const response = NextResponse8.json(
3210
3241
  { error: siwx.code, message: SIWX_ERROR_MESSAGES[siwx.code] },
3211
3242
  { status: 402 }
3212
3243
  );
@@ -3267,7 +3298,7 @@ async function buildSiwxChallenge(ctx) {
3267
3298
  `SIWX challenge header encoding failed: ${err instanceof Error ? err.message : String(err)}`
3268
3299
  );
3269
3300
  }
3270
- const response = new NextResponse7(JSON.stringify(paymentRequired), {
3301
+ const response = new NextResponse8(JSON.stringify(paymentRequired), {
3271
3302
  status: 402,
3272
3303
  headers: { "Content-Type": "application/json", "Cache-Control": "no-store" }
3273
3304
  });
@@ -3313,6 +3344,9 @@ function createRequestHandler(routeEntry, handler, deps) {
3313
3344
  return async (request) => {
3314
3345
  await deps.initPromise;
3315
3346
  const ctx = preflight(routeEntry, handler, deps, request);
3347
+ const query = validateQuery(ctx);
3348
+ if (!query.ok) return query.response;
3349
+ ctx.query = query.data;
3316
3350
  if (routeEntry.authMode === "unprotected") return runUnprotectedFlow(ctx);
3317
3351
  if (routeEntry.authMode === "siwx") return runSiwxOnlyFlow(ctx);
3318
3352
  if (routeEntry.pricing) return runPaidFlow(ctx);
@@ -3486,11 +3520,21 @@ var RouteBuilder = class _RouteBuilder {
3486
3520
  }
3487
3521
  }
3488
3522
  }
3523
+ if (billing === "exact" && typeof pricing === "string" && !isPositiveDecimal(pricing)) {
3524
+ throw new Error(
3525
+ `route '${this.#s.key}': price '${pricing}' must be a positive decimal string`
3526
+ );
3527
+ }
3489
3528
  if (next.#s.maxPrice !== void 0 && !isPositiveDecimal(next.#s.maxPrice)) {
3490
3529
  throw new Error(
3491
3530
  `route '${this.#s.key}': maxPrice '${next.#s.maxPrice}' must be a positive decimal string`
3492
3531
  );
3493
3532
  }
3533
+ if (next.#s.minPrice !== void 0 && !isPositiveDecimal(next.#s.minPrice)) {
3534
+ throw new Error(
3535
+ `route '${this.#s.key}': minPrice '${next.#s.minPrice}' must be a positive decimal string`
3536
+ );
3537
+ }
3494
3538
  if (next.#s.tickCost !== void 0 && !isPositiveDecimal(next.#s.tickCost)) {
3495
3539
  throw new Error(
3496
3540
  `route '${this.#s.key}': tickCost '${next.#s.tickCost}' must be a positive decimal string`
@@ -3938,7 +3982,7 @@ function normalizeMeteredArg(routeKey, options) {
3938
3982
  }
3939
3983
 
3940
3984
  // src/discovery/well-known.ts
3941
- import { NextResponse as NextResponse8 } from "next/server";
3985
+ import { NextResponse as NextResponse9 } from "next/server";
3942
3986
 
3943
3987
  // src/discovery/utils/guidance.ts
3944
3988
  async function resolveGuidance(discovery) {
@@ -3982,7 +4026,7 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, discovery) {
3982
4026
  if (instructions) {
3983
4027
  body.instructions = instructions;
3984
4028
  }
3985
- return NextResponse8.json(body, {
4029
+ return NextResponse9.json(body, {
3986
4030
  headers: {
3987
4031
  "Access-Control-Allow-Origin": "*",
3988
4032
  "Access-Control-Allow-Methods": "GET",
@@ -4000,13 +4044,13 @@ function toDiscoveryResource(method, url, mode) {
4000
4044
 
4001
4045
  // src/discovery/openapi.ts
4002
4046
  init_constants();
4003
- import { NextResponse as NextResponse9 } from "next/server";
4047
+ import { NextResponse as NextResponse10 } from "next/server";
4004
4048
  function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
4005
4049
  const normalizedBase = baseUrl.replace(/\/+$/, "");
4006
4050
  let cached = null;
4007
4051
  let validated = false;
4008
4052
  return async (_request) => {
4009
- if (cached) return NextResponse9.json(cached);
4053
+ if (cached) return NextResponse10.json(cached);
4010
4054
  if (!validated && pricesKeys) {
4011
4055
  registry.validate(pricesKeys);
4012
4056
  validated = true;
@@ -4069,7 +4113,7 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
4069
4113
  paths
4070
4114
  };
4071
4115
  cached = createDocument(openApiDocument);
4072
- return NextResponse9.json(cached);
4116
+ return NextResponse10.json(cached);
4073
4117
  };
4074
4118
  }
4075
4119
  function deriveTag(routeKey) {
@@ -4205,11 +4249,11 @@ function tierExtrema(prices) {
4205
4249
  }
4206
4250
 
4207
4251
  // src/discovery/llms-txt.ts
4208
- import { NextResponse as NextResponse10 } from "next/server";
4252
+ import { NextResponse as NextResponse11 } from "next/server";
4209
4253
  function createLlmsTxtHandler(discovery) {
4210
4254
  return async (_request) => {
4211
4255
  const guidance = await resolveGuidance(discovery) ?? "";
4212
- return new NextResponse10(guidance, {
4256
+ return new NextResponse11(guidance, {
4213
4257
  headers: {
4214
4258
  "Content-Type": "text/plain; charset=utf-8",
4215
4259
  "Access-Control-Allow-Origin": "*"
@@ -4259,10 +4303,14 @@ var isPlaceholderEvm = (v) => ZERO_EVM_ADDRESS_RE.test(v);
4259
4303
  var isSolanaAddress = (v) => SOLANA_ADDRESS_RE.test(v);
4260
4304
  var isX402Network = (v) => v.startsWith("eip155:") || v.startsWith("solana:");
4261
4305
  var canonicalizeEvm = (addr) => addr.toLowerCase();
4306
+ function evmAddressFromKey(key) {
4307
+ if (!key || !isEvmPrivateKey(key)) return null;
4308
+ return privateKeyToAccount(key).address.toLowerCase();
4309
+ }
4262
4310
  function operatorAddressesCollide(opKey, fpKey) {
4263
- if (!opKey || !fpKey || !isEvmPrivateKey(opKey) || !isEvmPrivateKey(fpKey)) return null;
4264
- const op = privateKeyToAccount(opKey).address.toLowerCase();
4265
- const fp = privateKeyToAccount(fpKey).address.toLowerCase();
4311
+ const op = evmAddressFromKey(opKey);
4312
+ const fp = evmAddressFromKey(fpKey);
4313
+ if (!op || !fp) return null;
4266
4314
  return op === fp ? op : null;
4267
4315
  }
4268
4316
  function trimAll(raw) {
@@ -4424,7 +4472,7 @@ function getConfiguredX402Accepts2(config) {
4424
4472
  }
4425
4473
  ];
4426
4474
  }
4427
- function validateX402Config(config, env, options) {
4475
+ function validateX402Config(config, env) {
4428
4476
  const accepts = getConfiguredX402Accepts2(config);
4429
4477
  const issues = [];
4430
4478
  const push = (code, message) => issues.push({ code, protocol: "x402", message });
@@ -4466,23 +4514,21 @@ function validateX402Config(config, env, options) {
4466
4514
  `x402 payee '${placeholder}' is a placeholder address and cannot receive payments.`
4467
4515
  );
4468
4516
  }
4469
- if (options.requireCdpKeys !== false) {
4470
- const hasEvm = accepts.some(
4471
- (a) => typeof a.network === "string" && a.network.startsWith("eip155:")
4472
- );
4473
- if (hasEvm) {
4474
- const missing = ["CDP_API_KEY_ID", "CDP_API_KEY_SECRET"].filter((k) => !env[k]);
4475
- if (missing.length > 0) {
4476
- push(
4477
- "missing_cdp_keys",
4478
- `x402 EVM facilitator (Coinbase) requires ${missing.join(" and ")}.`
4479
- );
4480
- }
4517
+ const hasEvm = accepts.some(
4518
+ (a) => typeof a.network === "string" && a.network.startsWith("eip155:")
4519
+ );
4520
+ if (hasEvm) {
4521
+ const missing = ["CDP_API_KEY_ID", "CDP_API_KEY_SECRET"].filter((k) => !env[k]);
4522
+ if (missing.length > 0) {
4523
+ push(
4524
+ "missing_cdp_keys",
4525
+ `x402 EVM facilitator (Coinbase) requires ${missing.join(" and ")}. Create an API key at https://portal.cdp.coinbase.com and set it via env.`
4526
+ );
4481
4527
  }
4482
4528
  }
4483
4529
  return issues;
4484
4530
  }
4485
- function validateMppConfig(config) {
4531
+ function validateMppConfig(config, env) {
4486
4532
  const m = config.mpp;
4487
4533
  if (!m) {
4488
4534
  return [
@@ -4530,7 +4576,7 @@ function validateMppConfig(config) {
4530
4576
  `MPP recipient '${placeholder}' is a placeholder address and cannot receive payments.`
4531
4577
  );
4532
4578
  }
4533
- if (!m.rpcUrl) {
4579
+ if (!m.rpcUrl && !env.TEMPO_RPC_URL) {
4534
4580
  push(
4535
4581
  "missing_mpp_rpc_url",
4536
4582
  "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object."
@@ -4555,6 +4601,15 @@ function validateMppConfig(config) {
4555
4601
  `MPP operatorKey and feePayerKey resolve to the same address (${collision}). Tempo rejects fee-delegated txs with sender === feePayer, so channel close/settle would fail at runtime. Either use two distinct wallets, or omit feePayerKey to disable gas sponsorship (clients then pay their own gas).`
4556
4602
  );
4557
4603
  }
4604
+ if (m.session && recipient && isEvmAddress(recipient)) {
4605
+ const operatorAddress = evmAddressFromKey(m.operatorKey);
4606
+ if (operatorAddress && operatorAddress !== recipient.toLowerCase()) {
4607
+ push(
4608
+ "mpp_operator_recipient_mismatch",
4609
+ `MPP session operatorKey resolves to ${operatorAddress}, which must equal the recipient/payee ${recipient.toLowerCase()}. mppx's channel-close handler asserts sender === payee. Set mpp.operatorKey to the recipient\u2019s private key, or set mpp.recipient/payeeAddress to the operator address.`
4610
+ );
4611
+ }
4612
+ }
4558
4613
  return issues;
4559
4614
  }
4560
4615
  function translateZodIssues(error) {
@@ -4683,8 +4738,8 @@ function getRouterConfigIssues(config, options = {}) {
4683
4738
  message: "RouterConfig.protocols cannot be empty. Omit the field to use default ['x402'] or specify protocols explicitly."
4684
4739
  });
4685
4740
  }
4686
- if (protocols.includes("x402")) issues.push(...validateX402Config(config, env, options));
4687
- if (protocols.includes("mpp")) issues.push(...validateMppConfig(config));
4741
+ if (protocols.includes("x402")) issues.push(...validateX402Config(config, env));
4742
+ if (protocols.includes("mpp")) issues.push(...validateMppConfig(config, env));
4688
4743
  return issues;
4689
4744
  }
4690
4745
 
@@ -4769,9 +4824,6 @@ async function initMpp(config, resolvedBaseUrl, kvStore, configError) {
4769
4824
  const getClient = async () => tempoClient;
4770
4825
  const operatorAccount = config.mpp.operatorKey ? privateKeyToAccount2(config.mpp.operatorKey) : void 0;
4771
4826
  const feePayerAccount = config.mpp.feePayerKey ? privateKeyToAccount2(config.mpp.feePayerKey) : void 0;
4772
- if (config.mpp.session && operatorAccount) {
4773
- assertOperatorMatchesRecipient(config, operatorAccount.address);
4774
- }
4775
4827
  const resolvedStore = kvStore ? await createKvMppStore(kvStore) : void 0;
4776
4828
  const realm = new URL(resolvedBaseUrl).host;
4777
4829
  const mppConfig = config.mpp;
@@ -4809,15 +4861,6 @@ async function initMpp(config, resolvedBaseUrl, kvStore, configError) {
4809
4861
  return { initError: err instanceof Error ? err.message : String(err) };
4810
4862
  }
4811
4863
  }
4812
- function assertOperatorMatchesRecipient(config, operatorAddress) {
4813
- const recipient = (config.mpp?.recipient ?? config.payeeAddress)?.toLowerCase();
4814
- const opAddr = operatorAddress.toLowerCase();
4815
- if (recipient && opAddr !== recipient) {
4816
- throw new Error(
4817
- `MPP session config mismatch: operator address ${operatorAddress} must equal recipient/payee ${recipient}. mppx's channel-close handler asserts sender === payee. Set mpp.operatorKey to the private key for ${recipient}, or set mpp.recipient/payeeAddress to ${operatorAddress}.`
4818
- );
4819
- }
4820
- }
4821
4864
 
4822
4865
  // src/index.ts
4823
4866
  init_constants();
@@ -4828,10 +4871,7 @@ function createRouter(config) {
4828
4871
  const entitlementStore = kvStore ? createKvEntitlementStore(kvStore) : new MemoryEntitlementStore();
4829
4872
  const network = config.network ?? BASE_MAINNET_NETWORK;
4830
4873
  const x402Accepts = getConfiguredX402Accepts(config);
4831
- const configIssues = getRouterConfigIssues(config, {
4832
- env: process.env,
4833
- requireCdpKeys: process.env.NODE_ENV === "production"
4834
- });
4874
+ const configIssues = getRouterConfigIssues(config, { env: process.env });
4835
4875
  const baseUrlIssue = configIssues.find((issue) => issue.code === "missing_base_url");
4836
4876
  if (baseUrlIssue) throw new RouterConfigError([baseUrlIssue]);
4837
4877
  const emptyProtocolsIssue = configIssues.find((issue) => issue.code === "empty_protocols");
@@ -4844,10 +4884,7 @@ function createRouter(config) {
4844
4884
  const x402ConfigError = x402ConfigIssues.length > 0 ? formatRouterConfigIssues(x402ConfigIssues) : void 0;
4845
4885
  const mppConfigError = mppConfigIssues.length > 0 ? formatRouterConfigIssues(mppConfigIssues) : void 0;
4846
4886
  if (protocolConfigIssues.length > 0) {
4847
- for (const issue of protocolConfigIssues) console.error(`[router] ${issue.message}`);
4848
- if (process.env.NODE_ENV === "production") {
4849
- throw new RouterConfigError(protocolConfigIssues);
4850
- }
4887
+ throw new RouterConfigError(protocolConfigIssues);
4851
4888
  }
4852
4889
  const resolvedBaseUrl = config.baseUrl.replace(/\/+$/, "");
4853
4890
  if (config.plugin?.init) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentcash/router",
3
- "version": "1.8.0",
3
+ "version": "1.9.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": {
@@ -77,8 +77,8 @@
77
77
  "typecheck": "tsc --noEmit",
78
78
  "lint": "eslint src/",
79
79
  "lint:fix": "eslint src/ --fix",
80
- "format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts' '*.json' '*.mjs'",
81
- "format:check": "prettier --check 'src/**/*.ts' 'tests/**/*.ts' '*.json' '*.mjs'",
80
+ "format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts' 'examples/fortune/**/*.ts' 'examples/fortune/**/*.md' '*.json' '*.mjs'",
81
+ "format:check": "prettier --check 'src/**/*.ts' 'tests/**/*.ts' 'examples/fortune/**/*.ts' 'examples/fortune/**/*.md' '*.json' '*.mjs'",
82
82
  "test": "vitest run",
83
83
  "test:watch": "vitest",
84
84
  "knip": "knip",