@agentcash/router 1.5.2 → 1.7.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
@@ -9,14 +9,18 @@ var __export = (target, all) => {
9
9
  };
10
10
 
11
11
  // src/constants.ts
12
- var BASE_NETWORK, SOLANA_MAINNET_NETWORK, TEMPO_USDC_CURRENCY, ZERO_EVM_ADDRESS;
12
+ var BASE_MAINNET_NETWORK, SOLANA_MAINNET_NETWORK, TEMPO_USDC_ADDRESS, TEMPO_USDC_DECIMALS, BASE_USDC_ADDRESS, BASE_USDC_DECIMALS, ZERO_EVM_ADDRESS, DEFAULT_SOLANA_FACILITATOR_URL;
13
13
  var init_constants = __esm({
14
14
  "src/constants.ts"() {
15
15
  "use strict";
16
- BASE_NETWORK = "eip155:8453";
16
+ BASE_MAINNET_NETWORK = "eip155:8453";
17
17
  SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
18
- TEMPO_USDC_CURRENCY = "0x20c000000000000000000000b9537d11c60e8b50";
18
+ TEMPO_USDC_ADDRESS = "0x20c000000000000000000000b9537d11c60e8b50";
19
+ TEMPO_USDC_DECIMALS = 6;
20
+ BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
21
+ BASE_USDC_DECIMALS = 6;
19
22
  ZERO_EVM_ADDRESS = "0x0000000000000000000000000000000000000000";
23
+ DEFAULT_SOLANA_FACILITATOR_URL = "https://facilitator.corbits.dev";
20
24
  }
21
25
  });
22
26
 
@@ -33,7 +37,7 @@ function getConfiguredX402Accepts(config) {
33
37
  return [
34
38
  {
35
39
  scheme: "exact",
36
- network: config.network ?? BASE_NETWORK,
40
+ network: config.network ?? BASE_MAINNET_NETWORK,
37
41
  payTo: config.payeeAddress
38
42
  }
39
43
  ];
@@ -83,6 +87,18 @@ function buildEvmExactOptions(accepts, price) {
83
87
  payTo
84
88
  }));
85
89
  }
90
+ function buildEvmUptoOptions(accepts, price) {
91
+ return accepts.filter(
92
+ (accept) => accept.scheme === "upto" && isEvmNetwork(accept.network)
93
+ ).map((accept) => ({
94
+ scheme: "upto",
95
+ network: accept.network,
96
+ payTo: accept.payTo,
97
+ price,
98
+ ...accept.maxTimeoutSeconds !== void 0 ? { maxTimeoutSeconds: accept.maxTimeoutSeconds } : {},
99
+ ...accept.extra ? { extra: accept.extra } : {}
100
+ }));
101
+ }
86
102
  var init_evm = __esm({
87
103
  "src/protocols/x402/evm.ts"() {
88
104
  "use strict";
@@ -201,7 +217,10 @@ async function getAcceptsHeadersForFacilitator(facilitator) {
201
217
  return {};
202
218
  }
203
219
  function resolveX402FacilitatorTarget(config, network, defaultEvmFacilitator) {
204
- return (isSolanaNetwork(network) ? config.x402?.facilitators?.solana : void 0) ?? (isEvmNetwork(network) ? config.x402?.facilitators?.evm : void 0) ?? (isSolanaNetwork(network) ? DEFAULT_SOLANA_FACILITATOR_URL : defaultEvmFacilitator);
220
+ if (isSolanaNetwork(network)) {
221
+ return config.x402?.facilitators?.solana ?? DEFAULT_SOLANA_FACILITATOR_URL;
222
+ }
223
+ return defaultEvmFacilitator;
205
224
  }
206
225
  function normalizeFacilitatorTarget(target) {
207
226
  return typeof target === "string" ? { url: target } : target;
@@ -214,19 +233,18 @@ function getNetworkFamily(network) {
214
233
  function sameFacilitatorConfig(a, b) {
215
234
  return a.url === b.url && a.createAuthHeaders === b.createAuthHeaders && a.createAcceptsHeaders === b.createAcceptsHeaders;
216
235
  }
217
- var DEFAULT_SOLANA_FACILITATOR_URL;
218
236
  var init_facilitators = __esm({
219
237
  "src/protocols/x402/facilitators.ts"() {
220
238
  "use strict";
221
239
  init_evm();
222
240
  init_solana();
223
- DEFAULT_SOLANA_FACILITATOR_URL = "https://facilitator.corbits.dev";
241
+ init_constants();
224
242
  }
225
243
  });
226
244
 
227
- // src/server.ts
228
- var server_exports = {};
229
- __export(server_exports, {
245
+ // src/init/x402-server.ts
246
+ var x402_server_exports = {};
247
+ __export(x402_server_exports, {
230
248
  createX402Server: () => createX402Server
231
249
  });
232
250
  async function createX402Server(config) {
@@ -267,44 +285,44 @@ async function createX402Server(config) {
267
285
  facilitatorsByNetwork
268
286
  };
269
287
  }
270
- function cachedClient(inner, kinds) {
288
+ function createFacilitatorClients(facilitatorsByNetwork, HTTPFacilitatorClient) {
289
+ const groups = getResolvedX402FacilitatorGroups(facilitatorsByNetwork);
290
+ return groups.map((group) => {
291
+ const inner = new HTTPFacilitatorClient(group.config);
292
+ const kinds = buildSupportedKinds(group);
293
+ return hardcodedSupportedClient(inner, kinds);
294
+ });
295
+ }
296
+ function hardcodedSupportedClient(inner, kinds) {
271
297
  return {
272
298
  verify: inner.verify.bind(inner),
273
299
  settle: inner.settle.bind(inner),
274
- getSupported: async () => ({
275
- kinds,
276
- extensions: [],
277
- signers: {}
278
- })
300
+ getSupported: async () => ({ kinds, extensions: [], signers: {} })
279
301
  };
280
302
  }
281
- function createFacilitatorClients(facilitatorsByNetwork, HTTPFacilitatorClient) {
282
- const groups = getResolvedX402FacilitatorGroups(facilitatorsByNetwork);
283
- return groups.map((group) => {
284
- const inner = new HTTPFacilitatorClient(group.config);
285
- const kinds = group.networks.flatMap((network) => {
286
- const exactKind = {
287
- x402Version: 2,
288
- scheme: "exact",
289
- network,
290
- ...group.family === "solana" ? {
291
- extra: {
292
- features: {
293
- xSettlementAccountSupported: true
294
- }
303
+ function buildSupportedKinds(group) {
304
+ return group.networks.flatMap((network) => {
305
+ const exactKind = {
306
+ x402Version: 2,
307
+ scheme: "exact",
308
+ network,
309
+ ...group.family === "solana" ? {
310
+ extra: {
311
+ features: {
312
+ xSettlementAccountSupported: true
295
313
  }
296
- } : {}
297
- };
298
- if (group.family === "evm") {
299
- return [exactKind, { x402Version: 2, scheme: "upto", network }];
300
- }
301
- return [exactKind];
302
- });
303
- return cachedClient(inner, kinds);
314
+ }
315
+ } : {}
316
+ };
317
+ const uptoKind = { x402Version: 2, scheme: "upto", network };
318
+ if (group.family === "evm") {
319
+ return [exactKind, uptoKind];
320
+ }
321
+ return [exactKind, uptoKind];
304
322
  });
305
323
  }
306
- var init_server = __esm({
307
- "src/server.ts"() {
324
+ var init_x402_server = __esm({
325
+ "src/init/x402-server.ts"() {
308
326
  "use strict";
309
327
  init_evm();
310
328
  init_solana();
@@ -313,65 +331,9 @@ var init_server = __esm({
313
331
  }
314
332
  });
315
333
 
316
- // src/upstash-rest.ts
317
- var upstash_rest_exports = {};
318
- __export(upstash_rest_exports, {
319
- createUpstashRest: () => createUpstashRest
320
- });
321
- function createUpstashRest(url, token) {
322
- const base = url.replace(/\/+$/, "");
323
- const headers = { Authorization: `Bearer ${token}` };
324
- async function get(key) {
325
- const res = await fetch(`${base}/get/${key}`, { headers });
326
- if (!res.ok) throw new Error(`[upstash-rest] GET ${key}: ${res.status}`);
327
- const { result } = await res.json();
328
- return result ?? null;
329
- }
330
- async function set(key, value) {
331
- const res = await fetch(`${base}`, {
332
- method: "POST",
333
- headers: { ...headers, "Content-Type": "application/json" },
334
- body: JSON.stringify(["SET", key, JSON.stringify(value)])
335
- });
336
- if (!res.ok) throw new Error(`[upstash-rest] SET ${key}: ${res.status}`);
337
- return await res.json();
338
- }
339
- async function del(key) {
340
- const res = await fetch(`${base}`, {
341
- method: "POST",
342
- headers: { ...headers, "Content-Type": "application/json" },
343
- body: JSON.stringify(["DEL", key])
344
- });
345
- if (!res.ok) throw new Error(`[upstash-rest] DEL ${key}: ${res.status}`);
346
- return await res.json();
347
- }
348
- return {
349
- get,
350
- set,
351
- del,
352
- async update(key, fn) {
353
- const current = await get(key);
354
- const change = fn(current);
355
- if (change.op === "set") await set(key, change.value);
356
- if (change.op === "delete") await del(key);
357
- return change.result;
358
- }
359
- };
360
- }
361
- var init_upstash_rest = __esm({
362
- "src/upstash-rest.ts"() {
363
- "use strict";
364
- }
365
- });
366
-
367
334
  // src/registry.ts
368
335
  var RouteRegistry = class {
369
336
  routes = /* @__PURE__ */ new Map();
370
- // Internal map key includes the HTTP method so that POST and DELETE on the
371
- // same path coexist. Within the same path+method, last-write-wins is still
372
- // intentional — Next.js module loading order is non-deterministic during
373
- // build and discovery stubs may register the same route in either order.
374
- // Prior art: ElysiaJS uses the same pattern (silent overwrite in router.history).
375
337
  mapKey(entry) {
376
338
  return `${entry.key}:${entry.method}`;
377
339
  }
@@ -384,8 +346,6 @@ var RouteRegistry = class {
384
346
  }
385
347
  this.routes.set(k, entry);
386
348
  }
387
- // Accepts either a compound key ("site/domain:DELETE") or a path-only key
388
- // ("site/domain") — path-only returns the first registered method for that path.
389
349
  get(key) {
390
350
  const direct = this.routes.get(key);
391
351
  if (direct) return direct;
@@ -417,24 +377,17 @@ var RouteRegistry = class {
417
377
 
418
378
  // src/headers.ts
419
379
  var HEADERS = {
420
- // ---- Standard HTTP ----
421
380
  AUTHORIZATION: "Authorization",
422
381
  WWW_AUTHENTICATE: "WWW-Authenticate",
423
- // ---- Auth ----
424
382
  API_KEY: "X-API-Key",
425
- // ---- Request meta (used by plugin/observability) ----
426
383
  WALLET_ADDRESS: "X-Wallet-Address",
427
384
  CLIENT_ID: "X-Client-ID",
428
385
  SESSION_ID: "X-Session-ID",
429
- // ---- SIWX ----
430
386
  SIWX: "SIGN-IN-WITH-X",
431
- // ---- x402 (payment) ----
432
387
  X402_PAYMENT_SIGNATURE: "PAYMENT-SIGNATURE",
433
- /** Legacy x402 payment header — accepted alongside PAYMENT-SIGNATURE. */
434
388
  X402_PAYMENT_LEGACY: "X-PAYMENT",
435
389
  X402_PAYMENT_REQUIRED: "PAYMENT-REQUIRED",
436
390
  X402_PAYMENT_RESPONSE: "PAYMENT-RESPONSE",
437
- // ---- MPP (payment) ----
438
391
  MPP_PAYMENT_RECEIPT: "Payment-Receipt"
439
392
  };
440
393
  var AUTH_SCHEME = {
@@ -442,7 +395,7 @@ var AUTH_SCHEME = {
442
395
  MPP_PAYMENT: "Payment "
443
396
  };
444
397
 
445
- // src/plugin.ts
398
+ // src/plugin/index.ts
446
399
  function createDefaultContext(meta) {
447
400
  const ctx = {
448
401
  requestId: meta.requestId,
@@ -478,50 +431,32 @@ function firePluginHook(plugin, method, ...args) {
478
431
  return void 0;
479
432
  }
480
433
  }
481
- function consolePlugin() {
482
- return {
483
- onRequest(meta) {
484
- const ctx = createDefaultContext(meta);
485
- return ctx;
486
- },
487
- onAuthVerified(_ctx, auth) {
488
- const wallet = auth.wallet ? ` wallet=${auth.wallet}` : "";
489
- console.log(`[router] AUTH ${auth.authMode} ${auth.route}${wallet}`);
490
- },
491
- onPaymentVerified(_ctx, payment) {
492
- console.log(`[router] VERIFIED ${payment.protocol} ${payment.payer} ${payment.amount}`);
493
- },
494
- onPaymentSettled(_ctx, settlement) {
495
- console.log(`[router] SETTLED ${settlement.protocol} tx=${settlement.transaction}`);
496
- },
497
- onResponse(ctx, response) {
498
- const wallet = ctx.verifiedWallet ? ` wallet=${ctx.verifiedWallet}` : "";
499
- console.log(
500
- `[router] ${ctx.route} \u2192 ${response.statusCode} (${response.duration}ms)${wallet}`
501
- );
502
- },
503
- onError(_ctx, error) {
504
- console.error(`[router] ERROR ${error.status}: ${error.message}`);
505
- },
506
- onAlert(_ctx, alert) {
507
- const logFn = alert.level === "critical" || alert.level === "error" ? console.error : alert.level === "warn" ? console.warn : console.log;
508
- logFn(
509
- `[router] ${alert.level.toUpperCase()} ${alert.route}: ${alert.message}`,
510
- alert.meta ?? ""
511
- );
512
- },
513
- onProviderQuota(_ctx, event) {
514
- const logFn = event.level === "critical" ? console.error : event.level === "warn" ? console.warn : console.log;
515
- logFn(`[router] QUOTA ${event.level.toUpperCase()} ${event.provider}: ${event.message}`);
516
- }
434
+
435
+ // src/plugin/reporter.ts
436
+ function createReporter(plugin, pluginCtx, route) {
437
+ return (level, message, meta) => {
438
+ firePluginHook(plugin, "onAlert", pluginCtx, {
439
+ level,
440
+ message,
441
+ route,
442
+ ...meta ? { meta } : {}
443
+ });
517
444
  };
518
445
  }
519
446
 
520
- // src/pipeline/context/preflight.ts
447
+ // src/pipeline/steps/preflight.ts
521
448
  function preflight(routeEntry, handler, deps, request) {
522
449
  const meta = buildMeta(request, routeEntry);
523
450
  const pluginCtx = firePluginHook(deps.plugin, "onRequest", meta) ?? createDefaultContext(meta);
524
- return { routeEntry, handler, deps, request, meta, pluginCtx };
451
+ return {
452
+ routeEntry,
453
+ handler,
454
+ deps,
455
+ request,
456
+ meta,
457
+ pluginCtx,
458
+ report: createReporter(deps.plugin, pluginCtx, routeEntry.key)
459
+ };
525
460
  }
526
461
  function buildMeta(request, routeEntry) {
527
462
  return {
@@ -539,10 +474,10 @@ function buildMeta(request, routeEntry) {
539
474
  };
540
475
  }
541
476
 
542
- // src/pipeline/context/parse-body.ts
477
+ // src/pipeline/steps/parse-body.ts
543
478
  import { NextResponse } from "next/server";
544
479
 
545
- // src/body.ts
480
+ // src/pipeline/body.ts
546
481
  async function bufferBody(request) {
547
482
  const text = await request.text();
548
483
  if (!text) return void 0;
@@ -566,46 +501,19 @@ function validateBody(parsed, schema) {
566
501
  };
567
502
  }
568
503
 
569
- // src/pipeline/context/parse-body.ts
570
- async function parseBody(request, routeEntry) {
571
- if (!routeEntry.bodySchema) return { ok: true, data: void 0 };
572
- const raw = await bufferBody(request);
573
- const result = validateBody(raw, routeEntry.bodySchema);
574
- if (result.success) return { ok: true, data: result.data };
575
- return {
576
- ok: false,
577
- response: NextResponse.json(
578
- { success: false, error: result.error, issues: result.issues },
579
- { status: 400 }
580
- )
581
- };
582
- }
583
-
584
- // src/pipeline/context/parse-query.ts
585
- function parseQuery(request, routeEntry) {
586
- if (!routeEntry.querySchema) return void 0;
587
- const params = Object.fromEntries(request.nextUrl.searchParams.entries());
588
- const result = routeEntry.querySchema.safeParse(params);
589
- return result.success ? result.data : params;
590
- }
591
-
592
- // src/pipeline/context/errors.ts
593
- function errorStatus(error, fallback) {
594
- const status = error?.status;
595
- return typeof status === "number" ? status : fallback;
504
+ // src/plugin/events.ts
505
+ function fireAuthVerified(ctx, event) {
506
+ firePluginHook(ctx.deps.plugin, "onAuthVerified", ctx.pluginCtx, {
507
+ ...event,
508
+ route: ctx.routeEntry.key
509
+ });
596
510
  }
597
- function errorMessage(error, fallback) {
598
- return error instanceof Error ? error.message : fallback;
511
+ function firePaymentVerified(ctx, event) {
512
+ firePluginHook(ctx.deps.plugin, "onPaymentVerified", ctx.pluginCtx, event);
599
513
  }
600
- function handlerFailureError(response) {
601
- const message = response.statusText || `Handler returned HTTP ${response.status}`;
602
- return Object.assign(new Error(message), { status: response.status });
514
+ function firePaymentSettled(ctx, event) {
515
+ firePluginHook(ctx.deps.plugin, "onPaymentSettled", ctx.pluginCtx, event);
603
516
  }
604
-
605
- // src/pipeline/context/fail.ts
606
- import { NextResponse as NextResponse2 } from "next/server";
607
-
608
- // src/pipeline/context/fire-plugin-response.ts
609
517
  function firePluginResponse(ctx, response, requestBody, responseBody) {
610
518
  firePluginHook(ctx.deps.plugin, "onResponse", ctx.pluginCtx, {
611
519
  statusCode: response.status,
@@ -624,15 +532,72 @@ function firePluginResponse(ctx, response, requestBody, responseBody) {
624
532
  });
625
533
  }
626
534
  }
535
+ function fireProviderQuota(ctx, response, handlerResult) {
536
+ const { providerName, providerConfig } = ctx.routeEntry;
537
+ if (!providerName || !providerConfig?.extractQuota) return;
538
+ if (response.status >= 400) return;
539
+ try {
540
+ const quota = providerConfig.extractQuota(handlerResult, response.headers);
541
+ if (!quota) return;
542
+ const level = computeQuotaLevel(quota.remaining, providerConfig.warn, providerConfig.critical);
543
+ const overage = providerConfig.overage ?? "same-rate";
544
+ const event = {
545
+ provider: providerName,
546
+ route: ctx.routeEntry.key,
547
+ remaining: quota.remaining,
548
+ limit: quota.limit,
549
+ spend: quota.spend,
550
+ level,
551
+ overage,
552
+ message: quota.remaining !== null ? `${providerName}: ${quota.remaining}${quota.limit ? `/${quota.limit}` : ""} remaining` : `${providerName}: quota info unavailable`
553
+ };
554
+ firePluginHook(ctx.deps.plugin, "onProviderQuota", ctx.pluginCtx, event);
555
+ } catch {
556
+ }
557
+ }
558
+ function computeQuotaLevel(remaining, warn, critical) {
559
+ if (remaining === null) return "healthy";
560
+ if (critical !== void 0 && remaining <= critical) return "critical";
561
+ if (warn !== void 0 && remaining <= warn) return "warn";
562
+ return "healthy";
563
+ }
564
+
565
+ // src/pipeline/steps/parse-body.ts
566
+ async function parseBody(ctx, request = ctx.request) {
567
+ if (!ctx.routeEntry.bodySchema) return { ok: true, data: void 0 };
568
+ const raw = await bufferBody(request);
569
+ const result = validateBody(raw, ctx.routeEntry.bodySchema);
570
+ if (result.success) return { ok: true, data: result.data };
571
+ const response = NextResponse.json(
572
+ { success: false, error: result.error, issues: result.issues },
573
+ { status: 400 }
574
+ );
575
+ firePluginResponse(ctx, response);
576
+ return { ok: false, response };
577
+ }
578
+
579
+ // src/pipeline/steps/errors.ts
580
+ function errorStatus(error, fallback) {
581
+ const status = error?.status;
582
+ return typeof status === "number" ? status : fallback;
583
+ }
584
+ function errorMessage(error, fallback) {
585
+ return error instanceof Error ? error.message : fallback;
586
+ }
587
+ function handlerFailureError(response) {
588
+ const message = response.statusText || `Handler returned HTTP ${response.status}`;
589
+ return Object.assign(new Error(message), { status: response.status });
590
+ }
627
591
 
628
- // src/pipeline/context/fail.ts
592
+ // src/pipeline/steps/fail.ts
593
+ import { NextResponse as NextResponse2 } from "next/server";
629
594
  function fail(ctx, status, message, requestBody) {
630
595
  const response = NextResponse2.json({ success: false, error: message }, { status });
631
596
  firePluginResponse(ctx, response, requestBody);
632
597
  return response;
633
598
  }
634
599
 
635
- // src/pipeline/context/run-validate.ts
600
+ // src/pipeline/steps/run-validate.ts
636
601
  async function runValidate(ctx, body) {
637
602
  if (!ctx.routeEntry.validateFn) return null;
638
603
  try {
@@ -643,7 +608,7 @@ async function runValidate(ctx, body) {
643
608
  }
644
609
  }
645
610
 
646
- // src/handler.ts
611
+ // src/pipeline/flows/static/static-invoke.ts
647
612
  import { NextResponse as NextResponse3 } from "next/server";
648
613
 
649
614
  // src/types.ts
@@ -655,23 +620,23 @@ var HttpError = class extends Error {
655
620
  }
656
621
  };
657
622
 
658
- // src/handler.ts
659
- async function safeCallHandler(handler, ctx, options = {}) {
660
- try {
661
- const result = await handler(ctx);
662
- if (result instanceof Response) return result;
663
- return NextResponse3.json(result);
664
- } catch (error) {
665
- options.onError?.(error);
666
- const status = error instanceof HttpError ? error.status : typeof error.status === "number" ? error.status : 500;
667
- const message = error instanceof Error ? error.message : "Internal error";
668
- return NextResponse3.json({ success: false, error: message }, { status });
669
- }
623
+ // src/pipeline/steps/parse-query.ts
624
+ function parseQuery(request, routeEntry) {
625
+ if (!routeEntry.querySchema) return void 0;
626
+ const params = Object.fromEntries(request.nextUrl.searchParams.entries());
627
+ const result = routeEntry.querySchema.safeParse(params);
628
+ return result.success ? result.data : params;
670
629
  }
671
630
 
672
- // src/pipeline/context/invoke.ts
673
- async function invoke(ctx, wallet, account, body, payment) {
674
- const handlerCtx = {
631
+ // src/pipeline/flows/static/static-invoke.ts
632
+ function invokePaidStatic(ctx, wallet, account, body, payment) {
633
+ return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, payment));
634
+ }
635
+ function invokeUnauthed(ctx, wallet, account, body) {
636
+ return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, null));
637
+ }
638
+ function buildHandlerCtx(ctx, wallet, account, body, payment) {
639
+ return {
675
640
  body,
676
641
  query: parseQuery(ctx.request, ctx.routeEntry),
677
642
  request: ctx.request,
@@ -680,85 +645,71 @@ async function invoke(ctx, wallet, account, body, payment) {
680
645
  wallet,
681
646
  payment,
682
647
  account,
683
- alert(level, message, alertMeta) {
684
- firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
685
- level,
686
- message,
687
- route: ctx.routeEntry.key,
688
- meta: alertMeta
689
- });
690
- },
648
+ alert: ctx.report,
691
649
  setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
692
650
  };
693
- let rawResult;
694
- let handlerError;
695
- const response = await safeCallHandler(
696
- async (c) => {
697
- rawResult = await ctx.handler(c);
698
- return rawResult;
699
- },
700
- handlerCtx,
701
- {
702
- onError(error) {
703
- handlerError = error;
704
- }
705
- }
706
- );
707
- return { response, rawResult, handlerError };
708
651
  }
709
-
710
- // src/pipeline/context/fire-provider-quota.ts
711
- function fireProviderQuota(ctx, response, handlerResult) {
712
- const { providerName, providerConfig } = ctx.routeEntry;
713
- if (!providerName || !providerConfig?.extractQuota) return;
714
- if (response.status >= 400) return;
652
+ async function runHandler(ctx, handlerCtx) {
653
+ let returned;
715
654
  try {
716
- const quota = providerConfig.extractQuota(handlerResult, response.headers);
717
- if (!quota) return;
718
- const level = computeQuotaLevel(quota.remaining, providerConfig.warn, providerConfig.critical);
719
- const overage = providerConfig.overage ?? "same-rate";
720
- const event = {
721
- provider: providerName,
722
- route: ctx.routeEntry.key,
723
- remaining: quota.remaining,
724
- limit: quota.limit,
725
- spend: quota.spend,
726
- level,
727
- overage,
728
- message: quota.remaining !== null ? `${providerName}: ${quota.remaining}${quota.limit ? `/${quota.limit}` : ""} remaining` : `${providerName}: quota info unavailable`
729
- };
730
- firePluginHook(ctx.deps.plugin, "onProviderQuota", ctx.pluginCtx, event);
731
- } catch {
655
+ returned = ctx.handler(handlerCtx);
656
+ } catch (error) {
657
+ return errorResult(error);
658
+ }
659
+ if (isAsyncIterable(returned) && !isThenable(returned)) {
660
+ return errorResult(
661
+ new HttpError(
662
+ `route '${ctx.routeEntry.key}': streaming handlers require .paid({ dynamic: true })`,
663
+ 500
664
+ )
665
+ );
666
+ }
667
+ let rawResult;
668
+ try {
669
+ rawResult = await returned;
670
+ } catch (error) {
671
+ return errorResult(error);
732
672
  }
673
+ const response = rawResult instanceof Response ? rawResult : NextResponse3.json(rawResult);
674
+ return { response, rawResult };
733
675
  }
734
- function computeQuotaLevel(remaining, warn, critical) {
735
- if (remaining === null) return "healthy";
736
- if (critical !== void 0 && remaining <= critical) return "critical";
737
- if (warn !== void 0 && remaining <= warn) return "warn";
738
- return "healthy";
676
+ function errorResult(error) {
677
+ const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
678
+ const message = error instanceof Error ? error.message : "Internal error";
679
+ return {
680
+ response: NextResponse3.json({ success: false, error: message }, { status }),
681
+ rawResult: void 0,
682
+ handlerError: error
683
+ };
684
+ }
685
+ function isAsyncIterable(value) {
686
+ return value != null && typeof value === "object" && Symbol.asyncIterator in value;
687
+ }
688
+ function isThenable(value) {
689
+ return value != null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
739
690
  }
740
691
 
741
- // src/pipeline/context/finalize.ts
692
+ // src/pipeline/steps/finalize/response.ts
742
693
  function finalize(ctx, response, rawResult, requestBody) {
743
694
  fireProviderQuota(ctx, response, rawResult);
744
695
  firePluginResponse(ctx, response, requestBody, rawResult);
745
696
  return response;
746
697
  }
747
698
 
748
- // src/pipeline/context/run-handler-only.ts
749
- async function runHandlerOnly(ctx, wallet, account) {
750
- const body = await parseBody(ctx.request, ctx.routeEntry);
751
- if (!body.ok) {
752
- firePluginResponse(ctx, body.response);
753
- return body.response;
699
+ // src/pipeline/steps/grant-entitlement.ts
700
+ async function grantEntitlementIfSiwx(ctx, wallet) {
701
+ if (!ctx.routeEntry.siwxEnabled) return;
702
+ try {
703
+ await ctx.deps.entitlementStore.grant(ctx.routeEntry.key, wallet);
704
+ } catch (error) {
705
+ ctx.report(
706
+ "warn",
707
+ `Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`
708
+ );
754
709
  }
755
- const validateErr = await runValidate(ctx, body.data);
756
- if (validateErr) return validateErr;
757
- const result = await invoke(ctx, wallet, account, body.data, null);
758
- return finalize(ctx, result.response, result.rawResult, body.data);
759
710
  }
760
711
 
761
- // src/pipeline/context/settlement-context.ts
712
+ // src/pipeline/steps/settlement-context.ts
762
713
  function settlementContext(ctx, scope) {
763
714
  return {
764
715
  route: ctx.routeEntry.key,
@@ -772,24 +723,7 @@ function settlementContext(ctx, scope) {
772
723
  };
773
724
  }
774
725
 
775
- // src/pipeline/context/run-before-settle.ts
776
- async function runBeforeSettle(ctx, scope) {
777
- const hook = ctx.routeEntry.settlement?.beforeSettle;
778
- if (!hook) return null;
779
- try {
780
- await hook(settlementContext(ctx, scope));
781
- return null;
782
- } catch (error) {
783
- return fail(
784
- ctx,
785
- errorStatus(error, 500),
786
- errorMessage(error, "Pre-settlement validation failed"),
787
- scope.body
788
- );
789
- }
790
- }
791
-
792
- // src/pipeline/context/run-settlement-error.ts
726
+ // src/pipeline/steps/run-settlement-error.ts
793
727
  async function runSettlementError(ctx, scope, error, phase) {
794
728
  const hook = ctx.routeEntry.settlement?.onSettlementError;
795
729
  if (!hook) return;
@@ -797,16 +731,11 @@ async function runSettlementError(ctx, scope, error, phase) {
797
731
  await hook({ ...settlementContext(ctx, scope), error, phase });
798
732
  } catch (hookError) {
799
733
  const message = errorMessage(hookError, "Settlement error hook failed");
800
- console.error(`[router] ${ctx.routeEntry.key}: onSettlementError failed: ${message}`);
801
- firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
802
- level: "error",
803
- message: `Settlement error hook failed: ${message}`,
804
- route: ctx.routeEntry.key
805
- });
734
+ ctx.report("error", `Settlement error hook failed: ${message}`);
806
735
  }
807
736
  }
808
737
 
809
- // src/pipeline/context/run-after-settle.ts
738
+ // src/pipeline/steps/run-after-settle.ts
810
739
  async function runAfterSettle(ctx, scope) {
811
740
  const hook = ctx.routeEntry.settlement?.afterSettle;
812
741
  if (!hook) return;
@@ -814,81 +743,145 @@ async function runAfterSettle(ctx, scope) {
814
743
  await hook(settlementContext(ctx, scope));
815
744
  } catch (error) {
816
745
  const message = errorMessage(error, "Post-settlement hook failed");
817
- console.error(`[router] ${ctx.routeEntry.key}: afterSettle failed: ${message}`);
818
- firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
819
- level: "error",
820
- message: `Post-settlement hook failed: ${message}`,
821
- route: ctx.routeEntry.key
822
- });
746
+ ctx.report("error", `Post-settlement hook failed: ${message}`);
823
747
  await runSettlementError(ctx, scope, error, "afterSettle");
824
748
  }
825
749
  }
826
750
 
827
- // src/pipeline/context/run-settled-handler-error.ts
828
- async function runSettledHandlerError(ctx, scope, error = scope.handlerError ?? handlerFailureError(scope.response)) {
829
- const hook = ctx.routeEntry.settlement?.onSettledHandlerError;
830
- if (!hook) return;
831
- try {
832
- await hook({ ...settlementContext(ctx, scope), error });
833
- } catch (hookError) {
834
- const message = errorMessage(hookError, "Settled handler error hook failed");
835
- console.error(`[router] ${ctx.routeEntry.key}: onSettledHandlerError failed: ${message}`);
836
- firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
837
- level: "error",
838
- message: `Settled handler error hook failed: ${message}`,
839
- route: ctx.routeEntry.key
840
- });
841
- }
842
- }
843
-
844
- // src/pipeline/context/grant-entitlement.ts
845
- async function grantEntitlementIfSiwx(ctx, wallet) {
846
- if (!ctx.routeEntry.siwxEnabled) return;
847
- try {
848
- await ctx.deps.entitlementStore.grant(ctx.routeEntry.key, wallet);
849
- } catch (error) {
850
- firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
851
- level: "warn",
852
- message: `Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`,
853
- route: ctx.routeEntry.key
854
- });
855
- }
751
+ // src/pipeline/steps/finalize/epilogue.ts
752
+ async function runPostSettleEpilogue(args) {
753
+ const { ctx, strategy, wallet, settle, afterSettleScope, rawResult, body } = args;
754
+ await grantEntitlementIfSiwx(ctx, wallet);
755
+ firePaymentSettled(ctx, {
756
+ protocol: strategy.protocol,
757
+ payer: wallet,
758
+ transaction: settle.settledPayment.transaction ?? "",
759
+ network: settle.settledPayment.network
760
+ });
761
+ await runAfterSettle(ctx, afterSettleScope);
762
+ return finalize(ctx, settle.response, rawResult, body);
856
763
  }
857
764
 
858
- // src/pipeline/context/settle-and-finalize.ts
859
- async function settleAndFinalize(args) {
860
- const { ctx, strategy, verifyOutcome, scope, rawResult, body, onSettleError } = args;
861
- const { request, routeEntry, deps } = ctx;
765
+ // src/pipeline/steps/finalize/request.ts
766
+ async function settleAndFinalizeRequest(args) {
767
+ const { ctx, strategy, verifyOutcome, scope, rawResult, body, billedAmount, onSettleError } = args;
768
+ const { request, routeEntry, deps, report } = ctx;
862
769
  const settle = await strategy.settle({
863
770
  request,
864
771
  response: scope.response,
865
772
  payment: verifyOutcome.payment,
866
773
  token: verifyOutcome.token,
867
774
  routeEntry,
868
- deps
775
+ deps,
776
+ billedAmount,
777
+ report
869
778
  });
870
779
  if (!settle.ok) {
871
780
  if (onSettleError) await onSettleError(settle.error, settle.failMessage);
872
781
  return fail(ctx, settle.failStatus ?? 500, settle.failMessage, body);
873
782
  }
874
- await grantEntitlementIfSiwx(ctx, verifyOutcome.wallet);
875
- firePluginHook(deps.plugin, "onPaymentSettled", ctx.pluginCtx, {
876
- protocol: strategy.protocol,
877
- payer: verifyOutcome.wallet,
878
- transaction: settle.settledPayment.transaction ?? "",
879
- network: settle.settledPayment.network
880
- });
881
- await runAfterSettle(ctx, {
882
- ...scope,
883
- payment: settle.settledPayment,
884
- response: settle.response
783
+ return runPostSettleEpilogue({
784
+ ctx,
785
+ strategy,
786
+ wallet: verifyOutcome.wallet,
787
+ settle,
788
+ afterSettleScope: {
789
+ ...scope,
790
+ payment: settle.settledPayment,
791
+ response: settle.response
792
+ },
793
+ rawResult,
794
+ body
885
795
  });
886
- return finalize(ctx, settle.response, rawResult, body);
796
+ }
797
+
798
+ // src/pipeline/steps/finalize/stream.ts
799
+ async function settleAndFinalizeStream(args) {
800
+ const { ctx, strategy, verifyOutcome, source, account, body, bindChannelCharge } = args;
801
+ const { request, routeEntry, deps, report } = ctx;
802
+ if (!strategy.settleStream) {
803
+ return fail(ctx, 500, `${strategy.protocol} does not support streaming handlers`, body);
804
+ }
805
+ const settle = await strategy.settleStream({
806
+ request,
807
+ source,
808
+ payment: verifyOutcome.payment,
809
+ token: verifyOutcome.token,
810
+ routeEntry,
811
+ deps,
812
+ bindChannelCharge,
813
+ report
814
+ });
815
+ if (!settle.ok) {
816
+ return fail(ctx, settle.failStatus ?? 500, settle.failMessage, body);
817
+ }
818
+ return runPostSettleEpilogue({
819
+ ctx,
820
+ strategy,
821
+ wallet: verifyOutcome.wallet,
822
+ settle,
823
+ afterSettleScope: {
824
+ wallet: verifyOutcome.wallet,
825
+ account,
826
+ body,
827
+ payment: settle.settledPayment,
828
+ response: settle.response,
829
+ rawResult: void 0
830
+ },
831
+ rawResult: void 0,
832
+ body
833
+ });
834
+ }
835
+
836
+ // src/pipeline/steps/run-handler-only.ts
837
+ async function runHandlerOnly(ctx, wallet, account) {
838
+ const body = await parseBody(ctx);
839
+ if (!body.ok) return body.response;
840
+ const validateErr = await runValidate(ctx, body.data);
841
+ if (validateErr) return validateErr;
842
+ const result = await invokeUnauthed(ctx, wallet, account, body.data);
843
+ return finalize(ctx, result.response, result.rawResult, body.data);
844
+ }
845
+
846
+ // src/pipeline/steps/run-before-settle.ts
847
+ async function runBeforeSettle(ctx, scope) {
848
+ const hook = ctx.routeEntry.settlement?.beforeSettle;
849
+ if (!hook) return null;
850
+ try {
851
+ await hook(settlementContext(ctx, scope));
852
+ return null;
853
+ } catch (error) {
854
+ return fail(
855
+ ctx,
856
+ errorStatus(error, 500),
857
+ errorMessage(error, "Pre-settlement validation failed"),
858
+ scope.body
859
+ );
860
+ }
861
+ }
862
+
863
+ // src/pipeline/steps/run-settled-handler-error.ts
864
+ async function runSettledHandlerError(ctx, scope, error = scope.handlerError ?? handlerFailureError(scope.response)) {
865
+ const hook = ctx.routeEntry.settlement?.onSettledHandlerError;
866
+ if (!hook) return;
867
+ try {
868
+ await hook({ ...settlementContext(ctx, scope), error });
869
+ } catch (hookError) {
870
+ const message = errorMessage(hookError, "Settled handler error hook failed");
871
+ ctx.report("error", `Settled handler error hook failed: ${message}`);
872
+ }
887
873
  }
888
874
 
889
875
  // src/auth/normalize-wallet.ts
890
876
  function normalizeWalletAddress(address) {
891
- return /^0x/i.test(address) ? address.toLowerCase() : address;
877
+ const isEvm = /^0x/i.test(address);
878
+ return isEvm ? normalizeEvmWalletAddress(address) : normalizeSolanaWalletAddress(address);
879
+ }
880
+ function normalizeEvmWalletAddress(address) {
881
+ return address.toLowerCase();
882
+ }
883
+ function normalizeSolanaWalletAddress(address) {
884
+ return address;
892
885
  }
893
886
 
894
887
  // src/auth/siwx.ts
@@ -941,7 +934,7 @@ async function buildSIWXExtension() {
941
934
  return declareSIWxExtension();
942
935
  }
943
936
 
944
- // src/pipeline/context/try-siwx-fast-path.ts
937
+ // src/pipeline/steps/try-siwx-fast-path.ts
945
938
  async function trySiwxFastPath(ctx, account) {
946
939
  const { request, routeEntry, deps } = ctx;
947
940
  if (!routeEntry.siwxEnabled) return null;
@@ -953,35 +946,29 @@ async function trySiwxFastPath(ctx, account) {
953
946
  ctx.pluginCtx.setVerifiedWallet(wallet);
954
947
  const entitled = await deps.entitlementStore.has(routeEntry.key, wallet);
955
948
  if (!entitled) return null;
956
- firePluginHook(deps.plugin, "onAuthVerified", ctx.pluginCtx, {
957
- authMode: "siwx",
958
- wallet,
959
- route: routeEntry.key
960
- });
949
+ fireAuthVerified(ctx, { authMode: "siwx", wallet });
961
950
  return runHandlerOnly(ctx, wallet, account);
962
951
  }
963
952
 
964
- // src/pipeline/context/should-parse-body-early.ts
953
+ // src/pipeline/steps/should-parse-body-early.ts
965
954
  function shouldParseBodyEarly(incomingStrategy, routeEntry, pricing) {
966
955
  if (incomingStrategy) return false;
967
956
  if (!routeEntry.bodySchema) return false;
968
957
  return (pricing?.needsBody ?? false) || !!routeEntry.validateFn;
969
958
  }
970
959
 
971
- // src/pipeline/context/protocol-init-error.ts
972
- function protocolInitError(routeEntry, deps) {
973
- if (!routeEntry.pricing) return null;
974
- const errors = [];
975
- for (const protocol of routeEntry.protocols) {
976
- if (protocol === "x402" && deps.x402InitError) {
977
- errors.push(`x402: ${deps.x402InitError}`);
978
- }
979
- if (protocol === "mpp" && deps.mppInitError) {
980
- errors.push(`mpp: ${deps.mppInitError}`);
981
- }
982
- }
983
- if (errors.length === 0) return null;
984
- return `Payment protocol initialization failed. ${errors.join("; ")}`;
960
+ // src/pipeline/steps/resolve-early-body.ts
961
+ async function resolveEarlyBody(args) {
962
+ const { ctx, pricing, incomingStrategy } = args;
963
+ if (!shouldParseBodyEarly(incomingStrategy, ctx.routeEntry, pricing)) {
964
+ return { ok: true, earlyBody: void 0 };
965
+ }
966
+ const earlyClone = ctx.request.clone();
967
+ const earlyResult = await parseBody(ctx, earlyClone);
968
+ if (!earlyResult.ok) return { ok: false, response: earlyResult.response };
969
+ const validateErr = await runValidate(ctx, earlyResult.data);
970
+ if (validateErr) return { ok: false, response: validateErr };
971
+ return { ok: true, earlyBody: earlyResult.data };
985
972
  }
986
973
 
987
974
  // src/auth/api-key.ts
@@ -1002,6 +989,34 @@ function extractBearerToken(header) {
1002
989
  return null;
1003
990
  }
1004
991
 
992
+ // src/pipeline/steps/run-api-key-gate.ts
993
+ async function runApiKeyGate(ctx) {
994
+ const { request, routeEntry } = ctx;
995
+ if (!routeEntry.apiKeyResolver) return { ok: true, account: void 0 };
996
+ const apiKeyResult = await verifyApiKey(request, routeEntry.apiKeyResolver);
997
+ if (!apiKeyResult.valid) {
998
+ return { ok: false, response: fail(ctx, 401, "Invalid or missing API key") };
999
+ }
1000
+ fireAuthVerified(ctx, { authMode: "apiKey", wallet: null, account: apiKeyResult.account });
1001
+ return { ok: true, account: apiKeyResult.account };
1002
+ }
1003
+
1004
+ // src/pipeline/steps/protocol-init-error.ts
1005
+ function protocolInitError(routeEntry, deps) {
1006
+ if (!routeEntry.pricing) return null;
1007
+ const errors = [];
1008
+ for (const protocol of routeEntry.protocols) {
1009
+ if (protocol === "x402" && deps.x402InitError) {
1010
+ errors.push(`x402: ${deps.x402InitError}`);
1011
+ }
1012
+ if (protocol === "mpp" && deps.mppInitError) {
1013
+ errors.push(`mpp: ${deps.mppInitError}`);
1014
+ }
1015
+ }
1016
+ if (errors.length === 0) return null;
1017
+ return `Payment protocol initialization failed. ${errors.join("; ")}`;
1018
+ }
1019
+
1005
1020
  // src/pipeline/flows/api-key-only.ts
1006
1021
  async function runApiKeyOnlyFlow(ctx) {
1007
1022
  if (!ctx.routeEntry.apiKeyResolver) {
@@ -1009,12 +1024,7 @@ async function runApiKeyOnlyFlow(ctx) {
1009
1024
  }
1010
1025
  const result = await verifyApiKey(ctx.request, ctx.routeEntry.apiKeyResolver);
1011
1026
  if (!result.valid) return fail(ctx, 401, "Invalid or missing API key");
1012
- firePluginHook(ctx.deps.plugin, "onAuthVerified", ctx.pluginCtx, {
1013
- authMode: "apiKey",
1014
- wallet: null,
1015
- route: ctx.routeEntry.key,
1016
- account: result.account
1017
- });
1027
+ fireAuthVerified(ctx, { authMode: "apiKey", wallet: null, account: result.account });
1018
1028
  return runHandlerOnly(ctx, null, result.account);
1019
1029
  }
1020
1030
 
@@ -1171,13 +1181,22 @@ function selectPricing(raw, deps = {}) {
1171
1181
  // src/protocols/mpp/credential.ts
1172
1182
  import { Credential } from "mppx";
1173
1183
  import { getAddress, isAddress } from "viem";
1184
+ var SESSION_ACTIONS = /* @__PURE__ */ new Set(["open", "topUp", "voucher", "close"]);
1174
1185
  function readMppCredential(request) {
1175
1186
  const credential = Credential.fromRequest(request);
1176
1187
  if (!credential) return null;
1177
1188
  const wallet = walletFromDid(credential.source ?? "");
1178
- const rawType = credential.payload?.type;
1189
+ const payload = credential.payload;
1190
+ const rawType = payload?.type;
1179
1191
  const payloadType = rawType === "transaction" ? "transaction" : rawType === "hash" ? "hash" : "unknown";
1180
- return { credential, wallet, payloadType };
1192
+ const rawAction = payload?.action;
1193
+ const sessionAction = typeof rawAction === "string" && SESSION_ACTIONS.has(rawAction) ? rawAction : void 0;
1194
+ return {
1195
+ credential,
1196
+ wallet,
1197
+ payloadType,
1198
+ ...sessionAction ? { sessionAction } : {}
1199
+ };
1181
1200
  }
1182
1201
  function walletFromDid(rawSource) {
1183
1202
  const parts = rawSource.split(":");
@@ -1185,6 +1204,168 @@ function walletFromDid(rawSource) {
1185
1204
  return normalizeWalletAddress(isAddress(last) ? getAddress(last) : rawSource);
1186
1205
  }
1187
1206
 
1207
+ // src/protocols/mpp/session-mode.ts
1208
+ async function verifySessionMode(args, info) {
1209
+ const { request, deps, price, routeEntry } = args;
1210
+ if (!deps.mppx?.sessionRequest || !deps.mppx?.sessionStream || !deps.mppSessionConfig) {
1211
+ return {
1212
+ ok: false,
1213
+ kind: "config",
1214
+ message: "MPP sessions not configured on this server (set RouterConfig.mpp.session)"
1215
+ };
1216
+ }
1217
+ const tickCost = routeEntry.tickCost;
1218
+ const unitType = routeEntry.unitType;
1219
+ const streaming = routeEntry.streaming === true;
1220
+ const middleware = streaming ? deps.mppx.sessionStream : deps.mppx.sessionRequest;
1221
+ const middlewareRequest = isChannelOnlyAction(info, request) ? new Request(request.url, { method: request.method, headers: request.headers }) : request;
1222
+ let result;
1223
+ try {
1224
+ result = await middleware({
1225
+ amount: tickCost,
1226
+ unitType,
1227
+ suggestedDeposit: price,
1228
+ ...streaming ? { meta: { streaming: "true" } } : {}
1229
+ })(middlewareRequest);
1230
+ } catch (err) {
1231
+ const message = err instanceof Error ? err.message : String(err);
1232
+ return {
1233
+ ok: false,
1234
+ kind: "config",
1235
+ message: `MPP session verify failed: ${message}`
1236
+ };
1237
+ }
1238
+ if (result.status === 402) {
1239
+ const failure = await readMppxProblemDetails(result.challenge);
1240
+ return { ok: false, kind: "invalid", failure };
1241
+ }
1242
+ const mppRecipient = deps.mppRecipient ?? deps.payeeAddress;
1243
+ const payment = {
1244
+ protocol: "mpp",
1245
+ status: "verified",
1246
+ payer: info.wallet,
1247
+ amount: price,
1248
+ network: "tempo:4217",
1249
+ ...mppRecipient ? { recipient: mppRecipient } : {}
1250
+ };
1251
+ const token = {
1252
+ mode: "session",
1253
+ streaming,
1254
+ sessionResult: result,
1255
+ info,
1256
+ tickCost
1257
+ };
1258
+ return {
1259
+ ok: true,
1260
+ wallet: info.wallet,
1261
+ payment,
1262
+ token,
1263
+ alreadySettled: false
1264
+ };
1265
+ }
1266
+ async function settleSessionMode(args) {
1267
+ const { request, response, payment, token, billedAmount } = args;
1268
+ const sessionToken = token;
1269
+ if (isChannelOnlyAction(sessionToken.info, request)) {
1270
+ const wrapped2 = sessionToken.sessionResult.withReceipt(
1271
+ new Response(null, { status: 200 })
1272
+ );
1273
+ return {
1274
+ ok: true,
1275
+ response: wrapped2,
1276
+ settledPayment: { ...payment, status: "settled", amount: billedAmount }
1277
+ };
1278
+ }
1279
+ if (sessionToken.streaming) {
1280
+ return {
1281
+ ok: false,
1282
+ error: new Error("streaming session content settled via request path"),
1283
+ failMessage: "streaming session content settled via request path",
1284
+ failStatus: 500
1285
+ };
1286
+ }
1287
+ const wrapped = sessionToken.sessionResult.withReceipt(
1288
+ response
1289
+ );
1290
+ wrapped.headers.set("Cache-Control", "private");
1291
+ const receiptHeader = wrapped.headers.get(HEADERS.MPP_PAYMENT_RECEIPT) ?? void 0;
1292
+ const settledPayment = {
1293
+ ...payment,
1294
+ status: "settled",
1295
+ amount: billedAmount,
1296
+ ...receiptHeader ? { receipt: receiptHeader } : {}
1297
+ };
1298
+ return { ok: true, response: wrapped, settledPayment };
1299
+ }
1300
+ async function buildSessionChallenge(args) {
1301
+ const { request, deps, suggestedDeposit, routeEntry, report } = args;
1302
+ if (!deps.mppSessionConfig) return {};
1303
+ const streaming = routeEntry.streaming === true;
1304
+ const middleware = streaming ? deps.mppx?.sessionStream : deps.mppx?.sessionRequest;
1305
+ if (!middleware) return {};
1306
+ const tickCost = routeEntry.tickCost;
1307
+ const unitType = routeEntry.unitType;
1308
+ try {
1309
+ const result = await middleware({
1310
+ amount: tickCost,
1311
+ unitType,
1312
+ suggestedDeposit,
1313
+ ...streaming ? { meta: { streaming: "true" } } : {}
1314
+ })(request);
1315
+ if (result.status === 402) {
1316
+ const wwwAuth = result.challenge.headers.get(HEADERS.WWW_AUTHENTICATE);
1317
+ if (wwwAuth) return { headers: { [HEADERS.WWW_AUTHENTICATE]: wwwAuth } };
1318
+ }
1319
+ } catch (err) {
1320
+ report(
1321
+ "warn",
1322
+ `MPP session challenge build failed: ${err instanceof Error ? err.message : String(err)}`
1323
+ );
1324
+ throw err;
1325
+ }
1326
+ return {};
1327
+ }
1328
+ function isChannelOnlyAction(info, request) {
1329
+ const action = info.sessionAction;
1330
+ if (!action) return false;
1331
+ if (action === "close" || action === "topUp") return true;
1332
+ if ((action === "open" || action === "voucher") && !hasRequestBody(request)) return true;
1333
+ return false;
1334
+ }
1335
+ async function readMppxProblemDetails(challenge) {
1336
+ let body;
1337
+ try {
1338
+ body = await challenge.clone().text();
1339
+ } catch {
1340
+ return { reason: "mpp_session_invalid" };
1341
+ }
1342
+ if (!body) return { reason: "mpp_session_invalid" };
1343
+ let parsed;
1344
+ try {
1345
+ parsed = JSON.parse(body);
1346
+ } catch {
1347
+ return { reason: "mpp_session_invalid", message: body.slice(0, 500) };
1348
+ }
1349
+ if (!parsed || typeof parsed !== "object") {
1350
+ return { reason: "mpp_session_invalid" };
1351
+ }
1352
+ const details = parsed;
1353
+ const typeUri = typeof details.type === "string" ? details.type : void 0;
1354
+ const slug = typeUri ? typeUri.split("/").pop() : void 0;
1355
+ const reason = slug ? slug.replace(/-/g, "_") : "mpp_session_invalid";
1356
+ const message = typeof details.detail === "string" && details.detail.length > 0 ? details.detail : typeof details.title === "string" ? details.title : void 0;
1357
+ return message ? { reason, message } : { reason };
1358
+ }
1359
+ function hasRequestBody(request) {
1360
+ const cl = request.headers.get("content-length");
1361
+ if (cl !== null) {
1362
+ const n = Number.parseInt(cl.trim(), 10);
1363
+ return Number.isFinite(n) && n > 0;
1364
+ }
1365
+ if (request.headers.get("transfer-encoding") !== null) return true;
1366
+ return false;
1367
+ }
1368
+
1188
1369
  // src/protocols/mpp/transaction-mode.ts
1189
1370
  import { Transaction as TempoTransaction } from "viem/tempo";
1190
1371
  import { call as viemCall } from "viem/actions";
@@ -1212,7 +1393,7 @@ async function readChallengeReason(challenge) {
1212
1393
 
1213
1394
  // src/protocols/mpp/transaction-mode.ts
1214
1395
  async function verifyTxMode(args, info) {
1215
- const { deps, price, routeEntry } = args;
1396
+ const { deps, price, report } = args;
1216
1397
  if (!deps.tempoClient) {
1217
1398
  return {
1218
1399
  ok: false,
@@ -1230,7 +1411,7 @@ async function verifyTxMode(args, info) {
1230
1411
  });
1231
1412
  } catch (err) {
1232
1413
  const message = err instanceof Error ? err.message : String(err);
1233
- console.warn(`[router] ${routeEntry.key}: MPP simulation failed \u2014 ${message}`);
1414
+ report("warn", `MPP simulation failed: ${message}`);
1234
1415
  return { ok: false, kind: "invalid" };
1235
1416
  }
1236
1417
  const mppRecipient = deps.mppRecipient ?? deps.payeeAddress;
@@ -1251,7 +1432,7 @@ async function verifyTxMode(args, info) {
1251
1432
  };
1252
1433
  }
1253
1434
  async function settleTxMode(args) {
1254
- const { request, response, payment, deps, routeEntry } = args;
1435
+ const { request, response, payment, deps, report } = args;
1255
1436
  if (!deps.mppx) {
1256
1437
  return {
1257
1438
  ok: false,
@@ -1265,7 +1446,7 @@ async function settleTxMode(args) {
1265
1446
  result = await deps.mppx.charge({ amount: payment.amount })(request);
1266
1447
  } catch (err) {
1267
1448
  const message = err instanceof Error ? err.message : String(err);
1268
- console.error(`[router] ${routeEntry.key}: MPP broadcast failed after handler: ${message}`);
1449
+ report("error", `MPP broadcast failed after handler: ${message}`);
1269
1450
  return {
1270
1451
  ok: false,
1271
1452
  error: err,
@@ -1282,7 +1463,7 @@ async function settleTxMode(args) {
1282
1463
  mppResult: result,
1283
1464
  challenge: result.challenge
1284
1465
  });
1285
- console.error(`[router] ${routeEntry.key}: MPP payment failed after handler \u2014 ${detail}`);
1466
+ report("error", `MPP payment failed after handler: ${detail}`);
1286
1467
  return {
1287
1468
  ok: false,
1288
1469
  error: settlementError,
@@ -1305,10 +1486,10 @@ async function settleTxMode(args) {
1305
1486
 
1306
1487
  // src/protocols/mpp/hash-mode.ts
1307
1488
  async function verifyHashMode(args, info) {
1308
- const { deps, price, routeEntry, request } = args;
1489
+ const { deps, price, request, report } = args;
1309
1490
  if (!deps.mppx) {
1310
1491
  const reason = deps.mppInitError ? `MPP initialization failed: ${deps.mppInitError}` : "MPP not initialized \u2014 ensure mppx is installed and mpp config (secretKey, currency, recipient) is correct";
1311
- console.error(`[router] ${routeEntry.key}: ${reason}`);
1492
+ report("error", reason);
1312
1493
  return { ok: false, kind: "config", message: reason };
1313
1494
  }
1314
1495
  let chargeResult;
@@ -1316,13 +1497,13 @@ async function verifyHashMode(args, info) {
1316
1497
  chargeResult = await deps.mppx.charge({ amount: price })(request);
1317
1498
  } catch (err) {
1318
1499
  const message = err instanceof Error ? err.message : String(err);
1319
- console.error(`[router] ${routeEntry.key}: MPP charge failed: ${message}`);
1500
+ report("error", `MPP charge failed: ${message}`);
1320
1501
  return { ok: false, kind: "config", message: `MPP payment processing failed: ${message}` };
1321
1502
  }
1322
1503
  if (chargeResult.status === 402) {
1323
1504
  const reason = await readChallengeReason(chargeResult.challenge);
1324
1505
  const detail = reason || "credential may be invalid, or check TEMPO_RPC_URL configuration";
1325
- console.warn(`[router] ${routeEntry.key}: MPP credential rejected \u2014 ${detail}`);
1506
+ report("warn", `MPP credential rejected: ${detail}`);
1326
1507
  return { ok: false, kind: "invalid" };
1327
1508
  }
1328
1509
  const receiptHeader = chargeResult.withReceipt(new Response()).headers.get(
@@ -1367,9 +1548,20 @@ var mppStrategy = {
1367
1548
  const auth = request.headers.get(HEADERS.AUTHORIZATION);
1368
1549
  return Boolean(auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT));
1369
1550
  },
1551
+ preflight(request, _routeEntry) {
1552
+ const info = readMppCredential(request);
1553
+ if (!info?.sessionAction) return null;
1554
+ if (!isChannelOnlyAction(info, request)) return null;
1555
+ return { skipBody: true, skipHandler: true };
1556
+ },
1370
1557
  async verify(args) {
1371
1558
  const info = readMppCredential(args.request);
1372
1559
  if (!info) return { ok: false, kind: "invalid" };
1560
+ if (args.routeEntry.dynamicPrice) {
1561
+ if (!info.sessionAction) return { ok: false, kind: "invalid" };
1562
+ return verifySessionMode(args, info);
1563
+ }
1564
+ if (info.sessionAction) return { ok: false, kind: "invalid" };
1373
1565
  if (info.payloadType === "transaction" && args.deps.tempoClient) {
1374
1566
  return verifyTxMode(args, info);
1375
1567
  }
@@ -1377,28 +1569,88 @@ var mppStrategy = {
1377
1569
  },
1378
1570
  async settle(args) {
1379
1571
  const token = args.token;
1572
+ if (token.mode === "session") return settleSessionMode(args);
1380
1573
  if (token.mode === "transaction") return settleTxMode(args);
1381
1574
  return settleHashMode(args);
1382
1575
  },
1576
+ async settleStream(args) {
1577
+ const token = args.token;
1578
+ if (token.mode !== "session" || !token.streaming) {
1579
+ return {
1580
+ ok: false,
1581
+ error: new Error("streaming requires a streaming-mode MPP session credential"),
1582
+ failMessage: "streaming requires a streaming-mode MPP session credential",
1583
+ failStatus: 400
1584
+ };
1585
+ }
1586
+ const sessionToken = token;
1587
+ const sseResult = sessionToken.sessionResult;
1588
+ const { bindChannelCharge, source: handlerStream } = args;
1589
+ async function* forwardHandlerStreamWithChannelDebit(channel) {
1590
+ bindChannelCharge(channel.charge);
1591
+ try {
1592
+ for await (const chunk of handlerStream) {
1593
+ yield typeof chunk === "string" ? chunk : JSON.stringify(chunk);
1594
+ }
1595
+ } finally {
1596
+ bindChannelCharge(null);
1597
+ }
1598
+ }
1599
+ const sse = sseResult.withReceipt(forwardHandlerStreamWithChannelDebit);
1600
+ sse.headers.set("Cache-Control", "private");
1601
+ const settledPayment = {
1602
+ ...args.payment,
1603
+ status: "settled",
1604
+ amount: args.payment.amount
1605
+ };
1606
+ return { ok: true, response: sse, settledPayment };
1607
+ },
1383
1608
  async buildChallenge(args) {
1384
1609
  if (!args.deps.mppx) return {};
1385
- try {
1386
- const result = await args.deps.mppx.charge({ amount: args.price })(args.request);
1387
- if (result.status === 402) {
1388
- const wwwAuth = result.challenge.headers.get(HEADERS.WWW_AUTHENTICATE);
1389
- if (wwwAuth) return { headers: { [HEADERS.WWW_AUTHENTICATE]: wwwAuth } };
1390
- }
1391
- } catch (err) {
1392
- console.warn(
1393
- `[router] MPP challenge build failed: ${err instanceof Error ? err.message : String(err)}`
1394
- );
1395
- throw err;
1610
+ const sessionsConfigured = args.deps.mppSessionConfig && (args.deps.mppx.sessionRequest || args.deps.mppx.sessionStream);
1611
+ if (args.routeEntry.dynamicPrice && sessionsConfigured) {
1612
+ const tickCost = args.routeEntry.tickCost;
1613
+ const computedDeposit = tickCost !== void 0 ? multiplyDecimal(tickCost, args.deps.mppSessionConfig.depositMultiplier) : void 0;
1614
+ const suggestedDeposit = args.routeEntry.maxPrice ?? computedDeposit ?? args.price;
1615
+ return buildSessionChallenge({
1616
+ ...args,
1617
+ suggestedDeposit
1618
+ });
1396
1619
  }
1397
- return {};
1620
+ return buildChargeChallenge(args);
1398
1621
  }
1399
1622
  };
1623
+ function multiplyDecimal(decimal, factor) {
1624
+ if (!Number.isFinite(factor) || factor <= 0) return decimal;
1625
+ const [whole, fraction = ""] = decimal.split(".");
1626
+ const scaled = (BigInt(whole + fraction) * BigInt(factor)).toString();
1627
+ const decimals = fraction.length;
1628
+ if (decimals === 0) return scaled;
1629
+ const padded = scaled.padStart(decimals + 1, "0");
1630
+ const intPart = padded.slice(0, padded.length - decimals);
1631
+ const fracPart = padded.slice(padded.length - decimals).replace(/0+$/, "");
1632
+ return fracPart ? `${intPart}.${fracPart}` : intPart;
1633
+ }
1634
+ async function buildChargeChallenge(args) {
1635
+ if (!args.deps.mppx) return {};
1636
+ try {
1637
+ const result = await args.deps.mppx.charge({ amount: args.price })(args.request);
1638
+ if (result.status === 402) {
1639
+ const wwwAuth = result.challenge.headers.get(HEADERS.WWW_AUTHENTICATE);
1640
+ if (wwwAuth) return { headers: { [HEADERS.WWW_AUTHENTICATE]: wwwAuth } };
1641
+ }
1642
+ } catch (err) {
1643
+ args.report(
1644
+ "warn",
1645
+ `MPP challenge build failed: ${err instanceof Error ? err.message : String(err)}`
1646
+ );
1647
+ throw err;
1648
+ }
1649
+ return {};
1650
+ }
1400
1651
 
1401
1652
  // src/protocols/x402/strategy.ts
1653
+ import { PERMIT2_ADDRESS } from "@x402/evm";
1402
1654
  init_accepts();
1403
1655
 
1404
1656
  // src/protocols/x402/challenge.ts
@@ -1408,20 +1660,27 @@ init_solana();
1408
1660
  // src/protocols/x402/requirements.ts
1409
1661
  init_evm();
1410
1662
  init_solana();
1411
- async function buildExpectedRequirements(server, request, price, accepts) {
1412
- const exactRequirements = await buildExactRequirements(server, request, price, accepts);
1663
+ async function buildExpectedRequirements(server, request, price, accepts, report) {
1664
+ const sdkRequirements = await buildSdkHandledRequirements(
1665
+ server,
1666
+ request,
1667
+ price,
1668
+ accepts,
1669
+ report
1670
+ );
1413
1671
  const customRequirements = buildCustomRequirements(price, accepts);
1414
- return [...exactRequirements, ...customRequirements];
1672
+ return [...sdkRequirements, ...customRequirements];
1415
1673
  }
1416
- async function buildExactRequirements(server, request, price, accepts) {
1417
- const exactGroups = [
1674
+ async function buildSdkHandledRequirements(server, request, price, accepts, report) {
1675
+ const groups = [
1418
1676
  buildEvmExactOptions(accepts, price),
1677
+ buildEvmUptoOptions(accepts, price),
1419
1678
  buildSolanaExactOptions(accepts, price)
1420
1679
  ].filter((options) => options.length > 0);
1421
- if (exactGroups.length === 0) return [];
1680
+ if (groups.length === 0) return [];
1422
1681
  const requirements = [];
1423
1682
  const failures = [];
1424
- for (const options of exactGroups) {
1683
+ for (const options of groups) {
1425
1684
  try {
1426
1685
  requirements.push(
1427
1686
  ...await server.buildPaymentRequirementsFromOptions(options, { request })
@@ -1429,21 +1688,31 @@ async function buildExactRequirements(server, request, price, accepts) {
1429
1688
  } catch (error) {
1430
1689
  const err = error instanceof Error ? error : new Error(String(error));
1431
1690
  failures.push(err);
1432
- if (exactGroups.length === 1) {
1691
+ if (groups.length === 1) {
1433
1692
  throw err;
1434
1693
  }
1435
- console.warn(
1436
- `[router] Failed to build x402 exact requirements for ${options[0]?.network}: ${err.message}`
1694
+ report?.(
1695
+ "warn",
1696
+ `Failed to build x402 ${options[0]?.scheme} requirements for ${options[0]?.network}: ${err.message}`
1437
1697
  );
1438
1698
  }
1439
1699
  }
1440
1700
  if (requirements.length > 0) {
1441
1701
  return requirements;
1442
1702
  }
1443
- throw failures[0] ?? new Error("Failed to build x402 exact requirements");
1703
+ throw failures[0] ?? new Error("Failed to build x402 SDK-handled requirements");
1444
1704
  }
1445
1705
  function buildCustomRequirements(price, accepts) {
1446
- return accepts.filter((accept) => accept.scheme !== "exact").map((accept) => buildCustomRequirement(price, accept));
1706
+ return accepts.filter((accept) => !isSdkHandled(accept)).map((accept) => buildCustomRequirement(price, accept));
1707
+ }
1708
+ function isSdkHandled(accept) {
1709
+ if (isEvmNetwork(accept.network)) {
1710
+ return accept.scheme === "exact" || accept.scheme === "upto";
1711
+ }
1712
+ if (isSolanaRequirement({ network: accept.network })) {
1713
+ return accept.scheme === "exact";
1714
+ }
1715
+ return false;
1447
1716
  }
1448
1717
  function buildCustomRequirement(price, accept) {
1449
1718
  if (!accept.asset) {
@@ -1477,7 +1746,7 @@ function decimalToAtomicUnits(amount, decimals) {
1477
1746
 
1478
1747
  // src/protocols/x402/challenge.ts
1479
1748
  async function buildX402Challenge(opts) {
1480
- const { server, routeEntry, request, price, accepts, facilitatorsByNetwork, extensions } = opts;
1749
+ const { server, routeEntry, request, price, accepts, facilitatorsByNetwork, extensions, report } = opts;
1481
1750
  const { encodePaymentRequiredHeader } = await import("@x402/core/http");
1482
1751
  const resource = buildChallengeResource(request, routeEntry);
1483
1752
  const requirements = await buildChallengeRequirements(
@@ -1486,7 +1755,8 @@ async function buildX402Challenge(opts) {
1486
1755
  price,
1487
1756
  accepts,
1488
1757
  resource,
1489
- facilitatorsByNetwork
1758
+ facilitatorsByNetwork,
1759
+ report
1490
1760
  );
1491
1761
  const paymentRequired = await server.createPaymentRequiredResponse(
1492
1762
  requirements,
@@ -1497,13 +1767,13 @@ async function buildX402Challenge(opts) {
1497
1767
  const encoded = encodePaymentRequiredHeader(paymentRequired);
1498
1768
  return { encoded, requirements };
1499
1769
  }
1500
- async function buildChallengeRequirements(server, request, price, accepts, resource, facilitatorsByNetwork) {
1501
- const requirements = await buildExpectedRequirements(server, request, price, accepts);
1770
+ async function buildChallengeRequirements(server, request, price, accepts, resource, facilitatorsByNetwork, report) {
1771
+ const requirements = await buildExpectedRequirements(server, request, price, accepts, report);
1502
1772
  if (!needsFacilitatorEnrichment(accepts)) return requirements;
1503
- return enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork);
1773
+ return enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork, report);
1504
1774
  }
1505
1775
  function needsFacilitatorEnrichment(accepts) {
1506
- return accepts.some((accept) => accept.scheme !== "exact") || hasSolanaAccepts(accepts);
1776
+ return hasSolanaAccepts(accepts);
1507
1777
  }
1508
1778
  async function enrichGroup(group, resource) {
1509
1779
  const accepted = await enrichRequirementsWithFacilitatorAccepts(
@@ -1518,7 +1788,7 @@ async function enrichGroup(group, resource) {
1518
1788
  }
1519
1789
  return accepted;
1520
1790
  }
1521
- async function enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork) {
1791
+ async function enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork, report) {
1522
1792
  const groups = collectEnrichmentGroups(requirements, facilitatorsByNetwork);
1523
1793
  if (groups.length === 0) return requirements;
1524
1794
  const results = await Promise.all(
@@ -1528,8 +1798,9 @@ async function enrichChallengeRequirements(requirements, resource, facilitatorsB
1528
1798
  } catch (err) {
1529
1799
  const label = group.facilitator.url ?? group.facilitator.network;
1530
1800
  const reason = err instanceof Error ? err.message : String(err);
1531
- console.warn(
1532
- `[router] ${label} /accepts failed, dropping ${group.items.length} requirement(s): ${reason}`
1801
+ report?.(
1802
+ "warn",
1803
+ `${label} /accepts failed, dropping ${group.items.length} requirement(s): ${reason}`
1533
1804
  );
1534
1805
  return { success: false, group };
1535
1806
  }
@@ -1582,7 +1853,7 @@ function getRequiredFacilitator(requirement, facilitatorsByNetwork) {
1582
1853
  return facilitator;
1583
1854
  }
1584
1855
  function requiresFacilitatorEnrichment(requirement) {
1585
- return requirement.scheme !== "exact" || isSolanaRequirement(requirement);
1856
+ return isSolanaRequirement(requirement);
1586
1857
  }
1587
1858
  function buildChallengeResource(request, routeEntry) {
1588
1859
  return {
@@ -1594,33 +1865,68 @@ function buildChallengeResource(request, routeEntry) {
1594
1865
  }
1595
1866
 
1596
1867
  // src/protocols/x402/settle.ts
1597
- async function settleX402Payment(server, payload, requirements) {
1868
+ async function settleX402Payment(server, payload, requirements, amountOverride) {
1598
1869
  const { encodePaymentResponseHeader } = await import("@x402/core/http");
1870
+ if (amountOverride?.amount !== void 0) {
1871
+ const upstreamTaggedAmount = tagBareDecimalAsDollars(amountOverride.amount);
1872
+ const result2 = await server.settlePayment(payload, requirements, void 0, void 0, {
1873
+ amount: upstreamTaggedAmount
1874
+ });
1875
+ return {
1876
+ encoded: encodePaymentResponseHeader(result2),
1877
+ result: result2
1878
+ };
1879
+ }
1599
1880
  const result = await server.settlePayment(payload, requirements);
1600
- const encoded = encodePaymentResponseHeader(result);
1601
- return { encoded, result };
1881
+ return {
1882
+ encoded: encodePaymentResponseHeader(result),
1883
+ result
1884
+ };
1885
+ }
1886
+ function tagBareDecimalAsDollars(amount) {
1887
+ if (/^\d+\.\d+$/.test(amount)) return `$${amount}`;
1888
+ return amount;
1602
1889
  }
1603
1890
 
1604
1891
  // src/protocols/x402/verify.ts
1892
+ import { VerifyError } from "@x402/core/types";
1605
1893
  async function verifyX402Payment(opts) {
1606
- const { server, request, price, accepts } = opts;
1894
+ const { server, request, price, accepts, report } = opts;
1607
1895
  const payload = await readPaymentPayload(request);
1608
1896
  if (!payload) return null;
1609
- const requirements = await buildExpectedRequirements(server, request, price, accepts);
1897
+ const requirements = await buildExpectedRequirements(server, request, price, accepts, report);
1610
1898
  const matching = findVerifiableRequirements(server, requirements, payload);
1899
+ const accepted = payload.x402Version === 2 ? payload.accepted : void 0;
1611
1900
  if (!matching) {
1612
- return invalidPaymentVerification();
1901
+ return invalidPaymentVerification({
1902
+ reason: "requirements_mismatch",
1903
+ message: "Signed payment requirements did not match any server-built requirement",
1904
+ ...accepted ? { accepted } : {}
1905
+ });
1613
1906
  }
1614
1907
  let verify;
1615
1908
  try {
1616
1909
  verify = await server.verifyPayment(payload, matching);
1617
1910
  } catch (err) {
1618
- const sc = err.statusCode;
1619
- if (sc && sc >= 400 && sc < 500) return invalidPaymentVerification();
1911
+ if (err instanceof VerifyError && err.statusCode >= 400 && err.statusCode < 500) {
1912
+ return invalidPaymentVerification({
1913
+ reason: err.invalidReason ?? "verify_error",
1914
+ ...err.invalidMessage ? { message: err.invalidMessage } : {},
1915
+ ...err.payer ? { payer: err.payer } : {},
1916
+ ...accepted ? { accepted } : {}
1917
+ });
1918
+ }
1620
1919
  throw err;
1621
1920
  }
1622
- if (!verify.isValid) return invalidPaymentVerification();
1623
- if (typeof verify.payer !== "string" || verify.payer.length === 0) {
1921
+ if (!verify.isValid) {
1922
+ return invalidPaymentVerification({
1923
+ reason: verify.invalidReason ?? "unknown",
1924
+ ...verify.invalidMessage ? { message: verify.invalidMessage } : {},
1925
+ ...verify.payer ? { payer: verify.payer } : {},
1926
+ ...accepted ? { accepted } : {}
1927
+ });
1928
+ }
1929
+ if (!verify.payer) {
1624
1930
  throw new Error("x402 verification succeeded without a payer address");
1625
1931
  }
1626
1932
  return {
@@ -1652,11 +1958,36 @@ async function readPaymentPayload(request) {
1652
1958
  const { decodePaymentSignatureHeader } = await import("@x402/core/http");
1653
1959
  return decodePaymentSignatureHeader(paymentHeader);
1654
1960
  }
1655
- function invalidPaymentVerification() {
1656
- return { valid: false, payload: null, requirements: null, payer: null };
1961
+ function invalidPaymentVerification(failure) {
1962
+ return {
1963
+ valid: false,
1964
+ payload: null,
1965
+ requirements: null,
1966
+ payer: null,
1967
+ ...failure ? { failure } : {}
1968
+ };
1657
1969
  }
1658
1970
 
1659
1971
  // src/protocols/x402/strategy.ts
1972
+ function formatVerifyFailureMessage(failure) {
1973
+ if (failure.reason === "permit2_allowance_required") {
1974
+ const wallet = failure.payer ?? "<the payer wallet>";
1975
+ const asset = failure.accepted?.asset ?? "<the asset>";
1976
+ const amount = failure.accepted?.amount ?? "<the required amount>";
1977
+ const network = failure.accepted?.network ?? "<the payment network>";
1978
+ return [
1979
+ `Payment rejected: In order for Upto to charge, the wallet ${wallet} MUST approve Permit2 to spend ${asset} on ${network}.`,
1980
+ `Required call (one-time, on-chain): ${asset}.approve(${PERMIT2_ADDRESS}, MAX_UINT256) from ${wallet}.`,
1981
+ `Permit2 contract address: ${PERMIT2_ADDRESS}.`,
1982
+ `Minimum allowance for this request: ${amount} (smallest units of ${asset}); use MAX_UINT256 to avoid re-approving on every future call.`,
1983
+ `Alternative without an on-chain transaction: the merchant can adopt the EIP-2612 gas-sponsoring extension (https://docs.x402.org/extensions/eip2612-gas-sponsoring).`
1984
+ ].join(" ");
1985
+ }
1986
+ if (failure.message) {
1987
+ return `Payment rejected (${failure.reason}): ${failure.message}`;
1988
+ }
1989
+ return `Payment rejected: ${failure.reason}`;
1990
+ }
1660
1991
  var x402Strategy = {
1661
1992
  protocol: "x402",
1662
1993
  detects(request) {
@@ -1664,129 +1995,123 @@ var x402Strategy = {
1664
1995
  request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)
1665
1996
  );
1666
1997
  },
1667
- async verify(args) {
1668
- const { request, body, price, routeEntry, deps } = args;
1669
- if (!deps.x402Server) {
1670
- const reason = deps.x402InitError ? `x402 facilitator initialization failed: ${deps.x402InitError}` : "x402 server not initialized \u2014 ensure @x402/core, @x402/evm, and @coinbase/x402 are installed";
1671
- console.error(`[router] ${routeEntry.key}: ${reason}`);
1672
- return { ok: false, kind: "config", message: reason };
1673
- }
1674
- const accepts = await resolveX402Accepts(
1675
- request,
1676
- routeEntry,
1677
- deps.x402Accepts,
1678
- deps.payeeAddress,
1679
- body
1680
- );
1681
- const verifyResult = await verifyX402Payment({
1682
- server: deps.x402Server,
1683
- request,
1684
- price,
1685
- accepts
1686
- });
1687
- if (!verifyResult?.valid) return { ok: false, kind: "invalid" };
1688
- const wallet = normalizeWalletAddress(verifyResult.payer);
1689
- const matchedNetwork = getRequirementNetwork(verifyResult.requirements, deps.network);
1690
- const matchedRecipient = getRequirementRecipient(verifyResult.requirements);
1691
- const payment = {
1692
- protocol: "x402",
1693
- status: "verified",
1694
- payer: wallet,
1695
- amount: price,
1696
- network: matchedNetwork,
1697
- ...matchedRecipient ? { recipient: matchedRecipient } : {}
1698
- };
1699
- return {
1700
- ok: true,
1701
- wallet,
1702
- payment,
1703
- token: {
1704
- payload: verifyResult.payload,
1705
- requirements: verifyResult.requirements
1706
- }
1707
- };
1708
- },
1709
- async settle(args) {
1710
- const { response, payment, token, deps } = args;
1711
- const x402Token = token;
1712
- try {
1713
- const settle = await settleX402Payment(
1714
- deps.x402Server,
1715
- x402Token.payload,
1716
- x402Token.requirements
1717
- );
1718
- if (!settle.result?.success) {
1719
- const reason = settle.result?.errorReason || "x402 settlement returned success=false";
1720
- const error = new Error(reason);
1721
- error.errorReason = reason;
1722
- throw error;
1723
- }
1724
- response.headers.set(HEADERS.X402_PAYMENT_RESPONSE, settle.encoded);
1725
- response.headers.set("Cache-Control", "private");
1726
- const transaction = String(settle.result?.transaction ?? "");
1727
- const settledPayment = {
1728
- ...payment,
1729
- status: "settled",
1730
- ...transaction ? { transaction } : {}
1998
+ verify: (args) => verifyX402(args),
1999
+ settle: (args) => settleX402(args),
2000
+ buildChallenge: (args) => buildX402ChallengeContribution(args)
2001
+ };
2002
+ async function verifyX402(args) {
2003
+ const { request, body, price, routeEntry, deps, report } = args;
2004
+ if (!deps.x402Server) {
2005
+ const reason = deps.x402InitError ? `x402 facilitator initialization failed: ${deps.x402InitError}` : "x402 server not initialized \u2014 ensure @x402/core, @x402/evm, and @coinbase/x402 are installed";
2006
+ report("error", reason);
2007
+ return { ok: false, kind: "config", message: reason };
2008
+ }
2009
+ const accepts = await resolveX402Accepts(
2010
+ request,
2011
+ routeEntry,
2012
+ deps.x402Accepts,
2013
+ deps.payeeAddress,
2014
+ body
2015
+ );
2016
+ const verifyResult = await verifyX402Payment({
2017
+ server: deps.x402Server,
2018
+ request,
2019
+ price,
2020
+ accepts,
2021
+ report
2022
+ });
2023
+ if (!verifyResult?.valid) {
2024
+ const failure = verifyResult?.failure;
2025
+ if (failure) {
2026
+ return {
2027
+ ok: false,
2028
+ kind: "invalid",
2029
+ failure: {
2030
+ reason: failure.reason,
2031
+ message: formatVerifyFailureMessage(failure)
2032
+ }
1731
2033
  };
1732
- return { ok: true, response, settledPayment };
1733
- } catch (err) {
1734
- const errObj = err;
1735
- console.error("Settlement failed", {
1736
- message: err instanceof Error ? err.message : String(err),
1737
- route: args.routeEntry.key,
1738
- network: payment.network,
1739
- errorReason: errObj.errorReason,
1740
- facilitatorStatus: errObj.response?.status,
1741
- facilitatorBody: errObj.response?.data ?? errObj.response?.body
1742
- });
1743
- return { ok: false, error: err, failMessage: "Settlement failed" };
1744
2034
  }
1745
- },
1746
- async buildChallenge(args) {
1747
- const { request, routeEntry, body, price, extensions, deps } = args;
1748
- if (!deps.x402Server) return {};
1749
- const accepts = await resolveX402Accepts(
1750
- request,
1751
- routeEntry,
1752
- deps.x402Accepts,
1753
- deps.payeeAddress,
1754
- body
1755
- );
1756
- const { encoded } = await buildX402Challenge({
1757
- server: deps.x402Server,
1758
- routeEntry,
1759
- request,
1760
- price,
1761
- accepts,
1762
- facilitatorsByNetwork: deps.x402FacilitatorsByNetwork,
1763
- extensions
1764
- });
1765
- return { headers: { [HEADERS.X402_PAYMENT_REQUIRED]: encoded } };
2035
+ return { ok: false, kind: "invalid" };
1766
2036
  }
1767
- };
1768
- function getRequirementNetwork(requirements, fallback) {
1769
- const network = requirements?.network;
1770
- return typeof network === "string" ? network : fallback;
1771
- }
1772
- function getRequirementRecipient(requirements) {
1773
- const payTo = requirements?.payTo;
1774
- return typeof payTo === "string" ? payTo : void 0;
1775
- }
1776
-
1777
- // src/protocols/detect.ts
1778
- function detectProtocol(request) {
1779
- if (request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)) {
1780
- return "x402";
1781
- }
1782
- const auth = request.headers.get(HEADERS.AUTHORIZATION);
1783
- if (auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT)) {
1784
- return "mpp";
1785
- }
1786
- if (request.headers.get(HEADERS.SIWX)) {
1787
- return "siwx";
2037
+ const wallet = normalizeWalletAddress(verifyResult.payer);
2038
+ const { network, payTo } = verifyResult.requirements;
2039
+ const payment = {
2040
+ protocol: "x402",
2041
+ status: "verified",
2042
+ payer: wallet,
2043
+ amount: price,
2044
+ network,
2045
+ ...payTo ? { recipient: payTo } : {}
2046
+ };
2047
+ return {
2048
+ ok: true,
2049
+ wallet,
2050
+ payment,
2051
+ token: {
2052
+ payload: verifyResult.payload,
2053
+ requirements: verifyResult.requirements
2054
+ }
2055
+ };
2056
+ }
2057
+ async function settleX402(args) {
2058
+ const { response, payment, token, deps, routeEntry, billedAmount, report } = args;
2059
+ const { payload, requirements } = token;
2060
+ const override = routeEntry.dynamicPrice ? { amount: billedAmount } : void 0;
2061
+ try {
2062
+ const settle = await settleX402Payment(deps.x402Server, payload, requirements, override);
2063
+ if (!settle.result?.success) {
2064
+ throw Object.assign(
2065
+ new Error(settle.result?.errorReason ?? "x402 settlement returned success=false"),
2066
+ { errorReason: settle.result?.errorReason }
2067
+ );
2068
+ }
2069
+ response.headers.set(HEADERS.X402_PAYMENT_RESPONSE, settle.encoded);
2070
+ response.headers.set("Cache-Control", "private");
2071
+ const transaction = String(settle.result.transaction ?? "");
2072
+ const settledPayment = {
2073
+ ...payment,
2074
+ status: "settled",
2075
+ amount: billedAmount,
2076
+ ...transaction ? { transaction } : {}
2077
+ };
2078
+ return { ok: true, response, settledPayment };
2079
+ } catch (err) {
2080
+ reportSettleFailure(report, err, payment.network);
2081
+ return { ok: false, error: err, failMessage: "Settlement failed" };
1788
2082
  }
1789
- return null;
2083
+ }
2084
+ async function buildX402ChallengeContribution(args) {
2085
+ const { request, routeEntry, body, price, extensions, deps, report } = args;
2086
+ if (!deps.x402Server) return {};
2087
+ const accepts = await resolveX402Accepts(
2088
+ request,
2089
+ routeEntry,
2090
+ deps.x402Accepts,
2091
+ deps.payeeAddress,
2092
+ body
2093
+ );
2094
+ const { encoded } = await buildX402Challenge({
2095
+ server: deps.x402Server,
2096
+ routeEntry,
2097
+ request,
2098
+ price,
2099
+ accepts,
2100
+ facilitatorsByNetwork: deps.x402FacilitatorsByNetwork,
2101
+ extensions,
2102
+ report
2103
+ });
2104
+ return { headers: { [HEADERS.X402_PAYMENT_REQUIRED]: encoded } };
2105
+ }
2106
+ function reportSettleFailure(report, err, network) {
2107
+ const facilitator = err ?? {};
2108
+ report("error", "Settlement failed", {
2109
+ error: err instanceof Error ? err.message : String(err),
2110
+ network,
2111
+ errorReason: facilitator.errorReason,
2112
+ facilitatorStatus: facilitator.response?.status,
2113
+ facilitatorBody: facilitator.response?.data ?? facilitator.response?.body
2114
+ });
1790
2115
  }
1791
2116
 
1792
2117
  // src/protocols/index.ts
@@ -1805,9 +2130,59 @@ function getAllowedStrategies(allowed) {
1805
2130
  return allowed.map((name) => STRATEGIES[name]);
1806
2131
  }
1807
2132
 
1808
- // src/pipeline/challenge.ts
2133
+ // src/pipeline/flows/build402.ts
1809
2134
  import { NextResponse as NextResponse4 } from "next/server";
1810
- async function build402(ctx, pricing, body) {
2135
+
2136
+ // src/pipeline/challenge-extensions.ts
2137
+ async function buildChallengeExtensions(ctx) {
2138
+ const { routeEntry } = ctx;
2139
+ let extensions;
2140
+ try {
2141
+ const { z: z2 } = await import("zod");
2142
+ const { declareDiscoveryExtension } = await import("@x402/extensions/bazaar");
2143
+ const toJSON = (schema) => z2.toJSONSchema(schema, {
2144
+ target: "draft-2020-12",
2145
+ unrepresentable: "any"
2146
+ });
2147
+ const inputSchema = routeEntry.bodySchema ? toJSON(routeEntry.bodySchema) : routeEntry.querySchema ? toJSON(routeEntry.querySchema) : void 0;
2148
+ const outputSchema = routeEntry.outputSchema ? toJSON(routeEntry.outputSchema) : void 0;
2149
+ if (inputSchema) {
2150
+ const config = {
2151
+ method: routeEntry.method,
2152
+ bodyType: routeEntry.bodySchema ? "json" : void 0,
2153
+ inputSchema
2154
+ };
2155
+ if (routeEntry.inputExample !== void 0) {
2156
+ config.input = routeEntry.inputExample;
2157
+ }
2158
+ if (outputSchema && routeEntry.outputExample !== void 0) {
2159
+ config.output = { schema: outputSchema, example: routeEntry.outputExample };
2160
+ }
2161
+ extensions = declareDiscoveryExtension(config);
2162
+ }
2163
+ } catch (err) {
2164
+ ctx.report(
2165
+ "warn",
2166
+ `Bazaar schema generation failed: ${err instanceof Error ? err.message : String(err)}`
2167
+ );
2168
+ }
2169
+ if (routeEntry.siwxEnabled) {
2170
+ try {
2171
+ const siwxExtension = await buildSIWXExtension();
2172
+ if (siwxExtension && typeof siwxExtension === "object" && !Array.isArray(siwxExtension)) {
2173
+ extensions = {
2174
+ ...extensions ?? {},
2175
+ ...siwxExtension
2176
+ };
2177
+ }
2178
+ } catch {
2179
+ }
2180
+ }
2181
+ return extensions;
2182
+ }
2183
+
2184
+ // src/pipeline/flows/build402.ts
2185
+ async function build402(ctx, pricing, body, failure) {
1811
2186
  let challengePrice;
1812
2187
  try {
1813
2188
  challengePrice = pricing ? await pricing.challengeQuote(body) : "0";
@@ -1821,7 +2196,8 @@ async function build402(ctx, pricing, body) {
1821
2196
  return errorResponse;
1822
2197
  }
1823
2198
  const extensions = await buildChallengeExtensions(ctx);
1824
- const response = new NextResponse4(null, {
2199
+ const responseBody = failure ? JSON.stringify({ error: failure.message ?? null, reason: failure.reason }) : null;
2200
+ const response = new NextResponse4(responseBody, {
1825
2201
  status: 402,
1826
2202
  headers: {
1827
2203
  "Content-Type": "application/json",
@@ -1836,7 +2212,8 @@ async function build402(ctx, pricing, body) {
1836
2212
  body,
1837
2213
  price: challengePrice,
1838
2214
  extensions,
1839
- deps: ctx.deps
2215
+ deps: ctx.deps,
2216
+ report: ctx.report
1840
2217
  });
1841
2218
  if (contribution.headers) {
1842
2219
  for (const [name, value] of Object.entries(contribution.headers)) {
@@ -1845,11 +2222,7 @@ async function build402(ctx, pricing, body) {
1845
2222
  }
1846
2223
  } catch (err) {
1847
2224
  const message = `${strategy.protocol} challenge build failed: ${errorMessage(err, String(err))}`;
1848
- firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
1849
- level: "critical",
1850
- message,
1851
- route: ctx.routeEntry.key
1852
- });
2225
+ ctx.report("critical", message);
1853
2226
  if (strategy.protocol === "x402") {
1854
2227
  const errorResponse = NextResponse4.json(
1855
2228
  { success: false, error: message },
@@ -1863,97 +2236,470 @@ async function build402(ctx, pricing, body) {
1863
2236
  firePluginResponse(ctx, response);
1864
2237
  return response;
1865
2238
  }
1866
- async function buildChallengeExtensions(ctx) {
1867
- const { routeEntry } = ctx;
1868
- let extensions;
2239
+
2240
+ // src/pipeline/flows/dynamic/dynamic-body-and-price.ts
2241
+ async function resolveDynamicBodyAndPrice(args) {
2242
+ const { ctx, pricing, skipBody } = args;
2243
+ if (skipBody) {
2244
+ return {
2245
+ ok: true,
2246
+ parsedBody: void 0,
2247
+ price: surrogatePriceForSkippedBody(ctx.routeEntry)
2248
+ };
2249
+ }
2250
+ const body = await parseBody(ctx);
2251
+ if (!body.ok) return { ok: false, response: body.response };
2252
+ const validateErr = await runValidate(ctx, body.data);
2253
+ if (validateErr) {
2254
+ return { ok: false, response: validateErr };
2255
+ }
2256
+ if (!pricing) {
2257
+ return { ok: false, response: fail(ctx, 500, "Pricing not configured", body.data) };
2258
+ }
1869
2259
  try {
1870
- const { z } = await import("zod");
1871
- const { declareDiscoveryExtension } = await import("@x402/extensions/bazaar");
1872
- const toJSON = (schema) => z.toJSONSchema(schema, {
1873
- target: "draft-2020-12",
1874
- unrepresentable: "any"
1875
- });
1876
- const inputSchema = routeEntry.bodySchema ? toJSON(routeEntry.bodySchema) : routeEntry.querySchema ? toJSON(routeEntry.querySchema) : void 0;
1877
- const outputSchema = routeEntry.outputSchema ? toJSON(routeEntry.outputSchema) : void 0;
1878
- if (inputSchema) {
1879
- const config = {
1880
- method: routeEntry.method,
1881
- bodyType: routeEntry.bodySchema ? "json" : void 0,
1882
- inputSchema
1883
- };
1884
- if (routeEntry.inputExample !== void 0) {
1885
- config.input = routeEntry.inputExample;
1886
- }
1887
- if (outputSchema && routeEntry.outputExample !== void 0) {
1888
- config.output = { schema: outputSchema, example: routeEntry.outputExample };
1889
- }
1890
- extensions = declareDiscoveryExtension(config);
1891
- }
2260
+ const price = await pricing.quote(body.data);
2261
+ return { ok: true, parsedBody: body.data, price };
1892
2262
  } catch (err) {
1893
- firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
1894
- level: "warn",
1895
- message: `Bazaar schema generation failed: ${err instanceof Error ? err.message : String(err)}`,
1896
- route: routeEntry.key
1897
- });
2263
+ return {
2264
+ ok: false,
2265
+ response: fail(
2266
+ ctx,
2267
+ errorStatus(err, 500),
2268
+ errorMessage(err, "Price calculation failed"),
2269
+ body.data
2270
+ )
2271
+ };
1898
2272
  }
1899
- if (routeEntry.siwxEnabled) {
1900
- try {
1901
- const siwxExtension = await buildSIWXExtension();
1902
- if (siwxExtension && typeof siwxExtension === "object" && !Array.isArray(siwxExtension)) {
1903
- extensions = {
1904
- ...extensions ?? {},
1905
- ...siwxExtension
1906
- };
1907
- }
1908
- } catch {
2273
+ }
2274
+ function surrogatePriceForSkippedBody(routeEntry) {
2275
+ return routeEntry.maxPrice ?? routeEntry.minPrice ?? "0";
2276
+ }
2277
+
2278
+ // src/pipeline/flows/dynamic/dynamic-channel-mgmt.ts
2279
+ import { NextResponse as NextResponse5 } from "next/server";
2280
+ async function runDynamicChannelMgmtFlow(args) {
2281
+ const { ctx, strategy, account, pricing, skipBody } = args;
2282
+ const { request, routeEntry, deps, report } = ctx;
2283
+ const bodyAndPrice = await resolveDynamicBodyAndPrice({ ctx, pricing, skipBody });
2284
+ if (!bodyAndPrice.ok) return bodyAndPrice.response;
2285
+ const { parsedBody, price } = bodyAndPrice;
2286
+ const verifyOutcome = await strategy.verify({
2287
+ request,
2288
+ body: parsedBody,
2289
+ price,
2290
+ routeEntry,
2291
+ deps,
2292
+ report
2293
+ });
2294
+ if (verifyOutcome.ok === false) {
2295
+ if (verifyOutcome.kind === "config") {
2296
+ return fail(ctx, 500, verifyOutcome.message, parsedBody);
1909
2297
  }
2298
+ return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
1910
2299
  }
1911
- return extensions;
2300
+ ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
2301
+ firePaymentVerified(ctx, {
2302
+ protocol: strategy.protocol,
2303
+ payer: verifyOutcome.wallet,
2304
+ amount: price,
2305
+ network: verifyOutcome.payment.network
2306
+ });
2307
+ const synthetic = new NextResponse5(null, { status: 200 });
2308
+ const settleScope = {
2309
+ wallet: verifyOutcome.wallet,
2310
+ account,
2311
+ body: parsedBody,
2312
+ payment: verifyOutcome.payment,
2313
+ response: synthetic,
2314
+ rawResult: void 0
2315
+ };
2316
+ const beforeErr = await runBeforeSettle(ctx, settleScope);
2317
+ if (beforeErr) return beforeErr;
2318
+ return settleAndFinalizeRequest({
2319
+ ctx,
2320
+ strategy,
2321
+ verifyOutcome,
2322
+ scope: settleScope,
2323
+ rawResult: void 0,
2324
+ body: parsedBody,
2325
+ billedAmount: "0",
2326
+ onSettleError: async (error, failMessage) => {
2327
+ await runSettlementError(ctx, settleScope, error, "settle");
2328
+ report("critical", `${strategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`);
2329
+ }
2330
+ });
1912
2331
  }
1913
2332
 
1914
- // src/pipeline/flows/paid.ts
1915
- async function runPaidFlow(ctx) {
1916
- const { request, routeEntry, deps } = ctx;
1917
- let account = void 0;
1918
- if (routeEntry.apiKeyResolver) {
1919
- const apiKeyResult = await verifyApiKey(request, routeEntry.apiKeyResolver);
1920
- if (!apiKeyResult.valid) return fail(ctx, 401, "Invalid or missing API key");
1921
- account = apiKeyResult.account;
1922
- firePluginHook(deps.plugin, "onAuthVerified", ctx.pluginCtx, {
1923
- authMode: "apiKey",
1924
- wallet: null,
1925
- route: routeEntry.key,
1926
- account
2333
+ // src/pipeline/flows/dynamic/dynamic-invoke.ts
2334
+ import { NextResponse as NextResponse6 } from "next/server";
2335
+
2336
+ // src/pricing/atomic.ts
2337
+ var USDC_DECIMALS = 6;
2338
+ function decimalToAtomic(amount) {
2339
+ const m = /^(\d+)(?:\.(\d+))?$/.exec(amount.trim());
2340
+ if (!m) {
2341
+ throw Object.assign(new Error(`'${amount}' is not a valid decimal-dollar string`), {
2342
+ status: 400
1927
2343
  });
1928
2344
  }
1929
- const alertFn = (level, message, meta) => {
1930
- firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
1931
- level,
1932
- message,
1933
- route: routeEntry.key,
1934
- meta
2345
+ const whole = m[1];
2346
+ const fraction = (m[2] ?? "").slice(0, USDC_DECIMALS).padEnd(USDC_DECIMALS, "0");
2347
+ return BigInt(`${whole}${fraction}`.replace(/^0+(?=\d)/, "") || "0");
2348
+ }
2349
+ function atomicToDecimal(atomic) {
2350
+ const whole = atomic / 10n ** BigInt(USDC_DECIMALS);
2351
+ const fraction = atomic % 10n ** BigInt(USDC_DECIMALS);
2352
+ if (fraction === 0n) return whole.toString();
2353
+ const fractionStr = fraction.toString().padStart(USDC_DECIMALS, "0").replace(/0+$/, "");
2354
+ return `${whole}.${fractionStr}`;
2355
+ }
2356
+
2357
+ // src/pricing/charge-context.ts
2358
+ function createChargeContext(args) {
2359
+ const { tickCost, maxPrice, route } = args;
2360
+ const tickAtomic = decimalToAtomic(tickCost);
2361
+ if (tickAtomic <= 0n) {
2362
+ throw new Error(`route '${route}': tickCost '${tickCost}' must be a positive decimal string`);
2363
+ }
2364
+ const capAtomic = maxPrice !== void 0 ? decimalToAtomic(maxPrice) : null;
2365
+ let ticks = 0;
2366
+ let atomic = 0n;
2367
+ let channelCharge = null;
2368
+ const charge = async () => {
2369
+ const nextAtomic = atomic + tickAtomic;
2370
+ if (capAtomic !== null && nextAtomic > capAtomic) {
2371
+ throw Object.assign(
2372
+ new Error(
2373
+ `route '${route}': charge() running total ($${atomicToDecimal(nextAtomic)}) exceeds maxPrice ($${atomicToDecimal(capAtomic)})`
2374
+ ),
2375
+ { status: 400, code: "CHARGE_OVER_CAP" }
2376
+ );
2377
+ }
2378
+ ticks += 1;
2379
+ atomic = nextAtomic;
2380
+ if (channelCharge) await channelCharge();
2381
+ };
2382
+ return {
2383
+ charge,
2384
+ bindChannelCharge: (fn) => {
2385
+ channelCharge = fn;
2386
+ },
2387
+ tickCount: () => ticks,
2388
+ atomicTotal: () => atomic
2389
+ };
2390
+ }
2391
+
2392
+ // src/pipeline/flows/dynamic/dynamic-invoke.ts
2393
+ async function invokeDynamic(ctx, wallet, account, body, payment) {
2394
+ const streaming = ctx.routeEntry.streaming === true;
2395
+ const chargeContext = streaming ? createChargeContext({
2396
+ tickCost: ctx.routeEntry.tickCost,
2397
+ maxPrice: ctx.routeEntry.maxPrice,
2398
+ route: ctx.routeEntry.key
2399
+ }) : null;
2400
+ const baseHandlerCtx = {
2401
+ body,
2402
+ query: parseQuery(ctx.request, ctx.routeEntry),
2403
+ request: ctx.request,
2404
+ requestId: ctx.meta.requestId,
2405
+ route: ctx.routeEntry.key,
2406
+ wallet,
2407
+ payment,
2408
+ account,
2409
+ alert: ctx.report,
2410
+ setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
2411
+ };
2412
+ const handlerCtx = chargeContext !== null ? { ...baseHandlerCtx, charge: chargeContext.charge } : baseHandlerCtx;
2413
+ let returned;
2414
+ try {
2415
+ returned = ctx.handler(handlerCtx);
2416
+ } catch (error) {
2417
+ return errorResult2(error, chargeContext);
2418
+ }
2419
+ if (isAsyncIterable2(returned) && !isThenable2(returned)) {
2420
+ if (!chargeContext) {
2421
+ return errorResult2(
2422
+ new HttpError(
2423
+ "route returned an async iterable from a non-streaming handler \u2014 use .stream(async function*(...)) instead of .handler() to opt into streaming",
2424
+ 500
2425
+ ),
2426
+ null
2427
+ );
2428
+ }
2429
+ return {
2430
+ kind: "stream",
2431
+ source: returned,
2432
+ chargeContext
2433
+ };
2434
+ }
2435
+ let rawResult;
2436
+ try {
2437
+ rawResult = await returned;
2438
+ } catch (error) {
2439
+ return errorResult2(error, chargeContext);
2440
+ }
2441
+ const response = rawResult instanceof Response ? rawResult : NextResponse6.json(rawResult);
2442
+ return { kind: "request", response, rawResult };
2443
+ }
2444
+ function errorResult2(error, chargeContext) {
2445
+ const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
2446
+ const message = error instanceof Error ? error.message : "Internal error";
2447
+ void chargeContext;
2448
+ return {
2449
+ kind: "request",
2450
+ response: NextResponse6.json({ success: false, error: message }, { status }),
2451
+ rawResult: void 0,
2452
+ handlerError: error
2453
+ };
2454
+ }
2455
+ function isAsyncIterable2(value) {
2456
+ return value != null && typeof value === "object" && Symbol.asyncIterator in value;
2457
+ }
2458
+ function isThenable2(value) {
2459
+ return value != null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
2460
+ }
2461
+
2462
+ // src/pipeline/flows/dynamic/dynamic-preflight.ts
2463
+ function resolveDynamicPreflight(strategy, request, routeEntry) {
2464
+ const outcome = strategy.preflight?.(request, routeEntry) ?? null;
2465
+ return {
2466
+ skipBody: outcome?.skipBody ?? false,
2467
+ skipHandler: outcome?.skipHandler ?? false
2468
+ };
2469
+ }
2470
+
2471
+ // src/pipeline/flows/dynamic/dynamic-request.ts
2472
+ async function runDynamicRequestFlow(args) {
2473
+ const { ctx, strategy, verifyOutcome, account, body, result } = args;
2474
+ const { routeEntry } = ctx;
2475
+ const settleScope = {
2476
+ wallet: verifyOutcome.wallet,
2477
+ account,
2478
+ body,
2479
+ payment: verifyOutcome.payment,
2480
+ response: result.response,
2481
+ rawResult: result.rawResult,
2482
+ handlerError: result.handlerError
2483
+ };
2484
+ if (result.response.status >= 400) {
2485
+ return finalize(ctx, result.response, result.rawResult, body);
2486
+ }
2487
+ const beforeErr = await runBeforeSettle(ctx, settleScope);
2488
+ if (beforeErr) return beforeErr;
2489
+ const billedAmount = routeEntry.tickCost;
2490
+ return settleAndFinalizeRequest({
2491
+ ctx,
2492
+ strategy,
2493
+ verifyOutcome,
2494
+ scope: settleScope,
2495
+ rawResult: result.rawResult,
2496
+ body,
2497
+ billedAmount,
2498
+ onSettleError: async (error, failMessage) => {
2499
+ await runSettlementError(ctx, settleScope, error, "settle");
2500
+ ctx.report(
2501
+ "critical",
2502
+ `${strategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`
2503
+ );
2504
+ }
2505
+ });
2506
+ }
2507
+
2508
+ // src/pipeline/flows/dynamic/dynamic-stream.ts
2509
+ async function runDynamicStreamFlow(args) {
2510
+ const { ctx, strategy, verifyOutcome, account, body, result } = args;
2511
+ return settleAndFinalizeStream({
2512
+ ctx,
2513
+ strategy,
2514
+ verifyOutcome,
2515
+ source: result.source,
2516
+ account,
2517
+ body,
2518
+ bindChannelCharge: result.chargeContext.bindChannelCharge
2519
+ });
2520
+ }
2521
+
2522
+ // src/pipeline/flows/dynamic/dynamic-paid.ts
2523
+ async function runDynamicPaidFlow(ctx) {
2524
+ const { request, routeEntry, deps, report } = ctx;
2525
+ const apiKeyGate = await runApiKeyGate(ctx);
2526
+ if (!apiKeyGate.ok) return apiKeyGate.response;
2527
+ const { account } = apiKeyGate;
2528
+ const pricing = selectPricing(routeEntry.pricing, {
2529
+ alert: report,
2530
+ maxPrice: routeEntry.maxPrice,
2531
+ minPrice: routeEntry.minPrice,
2532
+ route: routeEntry.key
2533
+ });
2534
+ const incomingStrategy = selectIncomingStrategy(request, routeEntry.protocols);
2535
+ const earlyResolution = await resolveEarlyBody({ ctx, pricing, incomingStrategy });
2536
+ if (!earlyResolution.ok) return earlyResolution.response;
2537
+ const { earlyBody } = earlyResolution;
2538
+ const siwxFastPath = await trySiwxFastPath(ctx, account);
2539
+ if (siwxFastPath) return siwxFastPath;
2540
+ if (!incomingStrategy) {
2541
+ const initError = protocolInitError(routeEntry, deps);
2542
+ if (initError) return fail(ctx, 500, initError);
2543
+ return build402(ctx, pricing, earlyBody);
2544
+ }
2545
+ const { skipBody, skipHandler } = resolveDynamicPreflight(incomingStrategy, request, routeEntry);
2546
+ if (skipHandler) {
2547
+ return runDynamicChannelMgmtFlow({
2548
+ ctx,
2549
+ strategy: incomingStrategy,
2550
+ account,
2551
+ pricing,
2552
+ skipBody
1935
2553
  });
2554
+ }
2555
+ const bodyAndPrice = await resolveDynamicBodyAndPrice({ ctx, pricing, skipBody });
2556
+ if (!bodyAndPrice.ok) return bodyAndPrice.response;
2557
+ const { parsedBody, price } = bodyAndPrice;
2558
+ const verifyOutcome = await incomingStrategy.verify({
2559
+ request,
2560
+ body: parsedBody,
2561
+ price,
2562
+ routeEntry,
2563
+ deps,
2564
+ report
2565
+ });
2566
+ if (verifyOutcome.ok === false) {
2567
+ if (verifyOutcome.kind === "config") {
2568
+ return fail(ctx, 500, verifyOutcome.message, parsedBody);
2569
+ }
2570
+ return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
2571
+ }
2572
+ ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
2573
+ firePaymentVerified(ctx, {
2574
+ protocol: incomingStrategy.protocol,
2575
+ payer: verifyOutcome.wallet,
2576
+ amount: price,
2577
+ network: verifyOutcome.payment.network
2578
+ });
2579
+ const result = await invokeDynamic(
2580
+ ctx,
2581
+ verifyOutcome.wallet,
2582
+ account,
2583
+ parsedBody,
2584
+ verifyOutcome.payment
2585
+ );
2586
+ switch (result.kind) {
2587
+ case "stream":
2588
+ return runDynamicStreamFlow({
2589
+ ctx,
2590
+ strategy: incomingStrategy,
2591
+ verifyOutcome,
2592
+ account,
2593
+ body: parsedBody,
2594
+ result
2595
+ });
2596
+ case "request":
2597
+ return runDynamicRequestFlow({
2598
+ ctx,
2599
+ strategy: incomingStrategy,
2600
+ verifyOutcome,
2601
+ account,
2602
+ body: parsedBody,
2603
+ result
2604
+ });
2605
+ }
2606
+ }
2607
+
2608
+ // src/pipeline/flows/static/static-body-and-price.ts
2609
+ async function resolveStaticBodyAndPrice(args) {
2610
+ const { ctx, pricing } = args;
2611
+ const body = await parseBody(ctx);
2612
+ if (!body.ok) return { ok: false, response: body.response };
2613
+ const validateErr = await runValidate(ctx, body.data);
2614
+ if (validateErr) {
2615
+ return { ok: false, response: validateErr };
2616
+ }
2617
+ if (!pricing) {
2618
+ return { ok: false, response: fail(ctx, 500, "Pricing not configured", body.data) };
2619
+ }
2620
+ try {
2621
+ const price = await pricing.quote(body.data);
2622
+ return { ok: true, parsedBody: body.data, price };
2623
+ } catch (err) {
2624
+ return {
2625
+ ok: false,
2626
+ response: fail(
2627
+ ctx,
2628
+ errorStatus(err, 500),
2629
+ errorMessage(err, "Price calculation failed"),
2630
+ body.data
2631
+ )
2632
+ };
2633
+ }
2634
+ }
2635
+
2636
+ // src/pipeline/flows/static/static-request.ts
2637
+ async function runStaticRequestFlow(args) {
2638
+ const { ctx, strategy, verifyOutcome, account, body, price, result } = args;
2639
+ const settleScope = {
2640
+ wallet: verifyOutcome.wallet,
2641
+ account,
2642
+ body,
2643
+ payment: verifyOutcome.payment,
2644
+ response: result.response,
2645
+ rawResult: result.rawResult,
2646
+ handlerError: result.handlerError
1936
2647
  };
2648
+ if (verifyOutcome.alreadySettled) {
2649
+ if (result.response.status >= 400) {
2650
+ const settledScope = settleScope;
2651
+ await runSettledHandlerError(ctx, settledScope);
2652
+ return finalize(ctx, result.response, result.rawResult, body);
2653
+ }
2654
+ return settleAndFinalizeRequest({
2655
+ ctx,
2656
+ strategy,
2657
+ verifyOutcome,
2658
+ scope: settleScope,
2659
+ rawResult: result.rawResult,
2660
+ body,
2661
+ billedAmount: price
2662
+ });
2663
+ }
2664
+ if (result.response.status >= 400) {
2665
+ return finalize(ctx, result.response, result.rawResult, body);
2666
+ }
2667
+ const beforeErr = await runBeforeSettle(ctx, settleScope);
2668
+ if (beforeErr) return beforeErr;
2669
+ return settleAndFinalizeRequest({
2670
+ ctx,
2671
+ strategy,
2672
+ verifyOutcome,
2673
+ scope: settleScope,
2674
+ rawResult: result.rawResult,
2675
+ body,
2676
+ billedAmount: price,
2677
+ onSettleError: async (error, failMessage) => {
2678
+ await runSettlementError(ctx, settleScope, error, "settle");
2679
+ ctx.report(
2680
+ "critical",
2681
+ `${strategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`
2682
+ );
2683
+ }
2684
+ });
2685
+ }
2686
+
2687
+ // src/pipeline/flows/static/static-paid.ts
2688
+ async function runStaticPaidFlow(ctx) {
2689
+ const { request, routeEntry, deps, report } = ctx;
2690
+ const apiKeyGate = await runApiKeyGate(ctx);
2691
+ if (!apiKeyGate.ok) return apiKeyGate.response;
2692
+ const { account } = apiKeyGate;
1937
2693
  const pricing = selectPricing(routeEntry.pricing, {
1938
- alert: alertFn,
2694
+ alert: report,
1939
2695
  maxPrice: routeEntry.maxPrice,
1940
2696
  minPrice: routeEntry.minPrice,
1941
2697
  route: routeEntry.key
1942
2698
  });
1943
2699
  const incomingStrategy = selectIncomingStrategy(request, routeEntry.protocols);
1944
- let earlyBody = void 0;
1945
- if (shouldParseBodyEarly(incomingStrategy, routeEntry, pricing)) {
1946
- const earlyClone = request.clone();
1947
- const earlyResult = await parseBody(earlyClone, routeEntry);
1948
- if (earlyResult.ok) {
1949
- earlyBody = earlyResult.data;
1950
- const validateErr2 = await runValidate(ctx, earlyBody);
1951
- if (validateErr2) return validateErr2;
1952
- } else {
1953
- firePluginResponse(ctx, earlyResult.response);
1954
- return earlyResult.response;
1955
- }
1956
- }
2700
+ const earlyResolution = await resolveEarlyBody({ ctx, pricing, incomingStrategy });
2701
+ if (!earlyResolution.ok) return earlyResolution.response;
2702
+ const { earlyBody } = earlyResolution;
1957
2703
  const siwxFastPath = await trySiwxFastPath(ctx, account);
1958
2704
  if (siwxFastPath) return siwxFastPath;
1959
2705
  if (!incomingStrategy) {
@@ -1961,99 +2707,139 @@ async function runPaidFlow(ctx) {
1961
2707
  if (initError) return fail(ctx, 500, initError);
1962
2708
  return build402(ctx, pricing, earlyBody);
1963
2709
  }
1964
- const body = await parseBody(request, routeEntry);
1965
- if (!body.ok) {
1966
- firePluginResponse(ctx, body.response);
1967
- return body.response;
1968
- }
1969
- const validateErr = await runValidate(ctx, body.data);
1970
- if (validateErr) return validateErr;
1971
- if (!pricing) {
1972
- return fail(ctx, 500, "Pricing not configured", body.data);
1973
- }
1974
- let price;
1975
- try {
1976
- price = await pricing.quote(body.data);
1977
- } catch (err) {
1978
- return fail(
1979
- ctx,
1980
- errorStatus(err, 500),
1981
- errorMessage(err, "Price calculation failed"),
1982
- body.data
1983
- );
1984
- }
2710
+ const bodyAndPrice = await resolveStaticBodyAndPrice({ ctx, pricing });
2711
+ if (!bodyAndPrice.ok) return bodyAndPrice.response;
2712
+ const { parsedBody, price } = bodyAndPrice;
1985
2713
  const verifyOutcome = await incomingStrategy.verify({
1986
2714
  request,
1987
- body: body.data,
2715
+ body: parsedBody,
1988
2716
  price,
1989
2717
  routeEntry,
1990
- deps
2718
+ deps,
2719
+ report
1991
2720
  });
1992
2721
  if (verifyOutcome.ok === false) {
1993
2722
  if (verifyOutcome.kind === "config") {
1994
- return fail(ctx, 500, verifyOutcome.message, body.data);
2723
+ return fail(ctx, 500, verifyOutcome.message, parsedBody);
1995
2724
  }
1996
- return build402(ctx, pricing, body.data);
2725
+ return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
1997
2726
  }
1998
2727
  ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
1999
- firePluginHook(deps.plugin, "onPaymentVerified", ctx.pluginCtx, {
2728
+ firePaymentVerified(ctx, {
2000
2729
  protocol: incomingStrategy.protocol,
2001
2730
  payer: verifyOutcome.wallet,
2002
2731
  amount: price,
2003
2732
  network: verifyOutcome.payment.network
2004
2733
  });
2005
- const result = await invoke(ctx, verifyOutcome.wallet, account, body.data, verifyOutcome.payment);
2006
- const settleScope = {
2007
- wallet: verifyOutcome.wallet,
2734
+ const result = await invokePaidStatic(
2735
+ ctx,
2736
+ verifyOutcome.wallet,
2008
2737
  account,
2009
- body: body.data,
2010
- payment: verifyOutcome.payment,
2011
- response: result.response,
2012
- rawResult: result.rawResult,
2013
- handlerError: result.handlerError
2014
- };
2015
- if (verifyOutcome.alreadySettled) {
2016
- if (result.response.status >= 400) {
2017
- const settledScope = settleScope;
2018
- await runSettledHandlerError(ctx, settledScope);
2019
- return finalize(ctx, result.response, result.rawResult, body.data);
2020
- }
2021
- return settleAndFinalize({
2022
- ctx,
2023
- strategy: incomingStrategy,
2024
- verifyOutcome,
2025
- scope: settleScope,
2026
- rawResult: result.rawResult,
2027
- body: body.data
2028
- });
2029
- }
2030
- if (result.response.status >= 400) {
2031
- return finalize(ctx, result.response, result.rawResult, body.data);
2032
- }
2033
- const beforeErr = await runBeforeSettle(ctx, settleScope);
2034
- if (beforeErr) return beforeErr;
2035
- return settleAndFinalize({
2738
+ parsedBody,
2739
+ verifyOutcome.payment
2740
+ );
2741
+ return runStaticRequestFlow({
2036
2742
  ctx,
2037
2743
  strategy: incomingStrategy,
2038
2744
  verifyOutcome,
2039
- scope: settleScope,
2040
- rawResult: result.rawResult,
2041
- body: body.data,
2042
- onSettleError: async (error, failMessage) => {
2043
- await runSettlementError(ctx, settleScope, error, "settle");
2044
- firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
2045
- level: "critical",
2046
- message: `${incomingStrategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`,
2047
- route: routeEntry.key
2048
- });
2049
- }
2745
+ account,
2746
+ body: parsedBody,
2747
+ price,
2748
+ result
2050
2749
  });
2051
2750
  }
2052
2751
 
2752
+ // src/pipeline/flows/paid.ts
2753
+ async function runPaidFlow(ctx) {
2754
+ const dynamicPrice = ctx.routeEntry.dynamicPrice ?? false;
2755
+ switch (dynamicPrice) {
2756
+ case true:
2757
+ return runDynamicPaidFlow(ctx);
2758
+ case false:
2759
+ return runStaticPaidFlow(ctx);
2760
+ }
2761
+ }
2762
+
2053
2763
  // src/pipeline/flows/siwx-only.ts
2054
- import { NextResponse as NextResponse5 } from "next/server";
2764
+ import { NextResponse as NextResponse7 } from "next/server";
2765
+
2766
+ // src/kv-store/client.ts
2767
+ function restKvStore(url, token) {
2768
+ const base = url.replace(/\/+$/, "");
2769
+ const authHeader = { Authorization: `Bearer ${token}` };
2770
+ const jsonHeaders = { ...authHeader, "Content-Type": "application/json" };
2771
+ async function exec(command) {
2772
+ const res = await fetch(base, {
2773
+ method: "POST",
2774
+ headers: jsonHeaders,
2775
+ body: JSON.stringify(command)
2776
+ });
2777
+ if (!res.ok) {
2778
+ throw new Error(`[kv-store] ${command[0]} ${command[1] ?? ""}: ${res.status}`);
2779
+ }
2780
+ const body = await res.json();
2781
+ if (body.error) throw new Error(`[kv-store] ${command[0]}: ${body.error}`);
2782
+ return body.result ?? null;
2783
+ }
2784
+ async function get(key) {
2785
+ const res = await fetch(`${base}/get/${encodeURIComponent(key)}`, { headers: authHeader });
2786
+ if (!res.ok) throw new Error(`[kv-store] GET ${key}: ${res.status}`);
2787
+ const { result } = await res.json();
2788
+ return result ?? null;
2789
+ }
2790
+ async function set(key, value) {
2791
+ await exec(["SET", key, JSON.stringify(value)]);
2792
+ }
2793
+ async function del(key) {
2794
+ await exec(["DEL", key]);
2795
+ }
2796
+ async function setNxEx(key, value, ttlSeconds) {
2797
+ const result = await exec(["SET", key, JSON.stringify(value), "EX", ttlSeconds, "NX"]);
2798
+ return result === "OK";
2799
+ }
2800
+ async function sadd(key, member) {
2801
+ await exec(["SADD", key, member]);
2802
+ }
2803
+ async function sismember(key, member) {
2804
+ const result = await exec(["SISMEMBER", key, member]);
2805
+ return result === 1;
2806
+ }
2807
+ async function update(key, fn) {
2808
+ const current = await get(key);
2809
+ const change = fn(current);
2810
+ if (change.op === "set") await set(key, change.value);
2811
+ if (change.op === "delete") await del(key);
2812
+ return change.result;
2813
+ }
2814
+ return { get, set, del, setNxEx, sadd, sismember, update };
2815
+ }
2816
+ function isRestConfig(input) {
2817
+ return typeof input === "object" && input !== null && typeof input.url === "string" && typeof input.token === "string" && typeof input.get !== "function";
2818
+ }
2819
+ function resolveKvStore(input, env = process.env) {
2820
+ if (input) {
2821
+ if (isRestConfig(input)) return restKvStore(input.url, input.token);
2822
+ return input;
2823
+ }
2824
+ const url = env.KV_REST_API_URL;
2825
+ const token = env.KV_REST_API_TOKEN;
2826
+ if (url && token) return restKvStore(url, token);
2827
+ return void 0;
2828
+ }
2829
+ function withPrefix(kv, prefix) {
2830
+ const k = (key) => `${prefix}${key}`;
2831
+ return {
2832
+ get: (key) => kv.get(k(key)),
2833
+ set: (key, value) => kv.set(k(key), value),
2834
+ del: (key) => kv.del(k(key)),
2835
+ setNxEx: (key, value, ttl) => kv.setNxEx(k(key), value, ttl),
2836
+ sadd: (key, member) => kv.sadd(k(key), member),
2837
+ sismember: (key, member) => kv.sismember(k(key), member),
2838
+ update: (key, fn) => kv.update(k(key), fn)
2839
+ };
2840
+ }
2055
2841
 
2056
- // src/auth/nonce.ts
2842
+ // src/kv-store/nonce.ts
2057
2843
  var SIWX_CHALLENGE_EXPIRY_MS = 5 * 60 * 1e3;
2058
2844
  var MemoryNonceStore = class {
2059
2845
  seen = /* @__PURE__ */ new Map();
@@ -2070,48 +2856,74 @@ var MemoryNonceStore = class {
2070
2856
  }
2071
2857
  }
2072
2858
  };
2073
- function detectRedisClientType(client) {
2074
- if (!client || typeof client !== "object") {
2075
- throw new Error(
2076
- "createRedisNonceStore requires a Redis client. Supported: @upstash/redis, ioredis. Pass your Redis client instance as the first argument."
2077
- );
2078
- }
2079
- if ("options" in client && "status" in client) {
2080
- return "ioredis";
2081
- }
2082
- const constructor = client.constructor?.name;
2083
- if (constructor === "Redis" && "url" in client) {
2084
- return "upstash";
2859
+ function createKvNonceStore(kv, options) {
2860
+ const prefix = options?.prefix ?? "siwx:nonce:";
2861
+ const ttlSeconds = Math.ceil((options?.ttlMs ?? SIWX_CHALLENGE_EXPIRY_MS) / 1e3);
2862
+ return {
2863
+ async check(nonce) {
2864
+ return kv.setNxEx(`${prefix}${nonce}`, 1, ttlSeconds);
2865
+ }
2866
+ };
2867
+ }
2868
+
2869
+ // src/kv-store/entitlement.ts
2870
+ var MemoryEntitlementStore = class {
2871
+ routeToWallets = /* @__PURE__ */ new Map();
2872
+ async has(route, wallet) {
2873
+ const wallets = this.routeToWallets.get(route);
2874
+ if (!wallets) return false;
2875
+ return wallets.has(normalizeWalletAddress(wallet));
2085
2876
  }
2086
- if (typeof client.set === "function") {
2087
- return "upstash";
2877
+ async grant(route, wallet) {
2878
+ const normalized = normalizeWalletAddress(wallet);
2879
+ let wallets = this.routeToWallets.get(route);
2880
+ if (!wallets) {
2881
+ wallets = /* @__PURE__ */ new Set();
2882
+ this.routeToWallets.set(route, wallets);
2883
+ }
2884
+ wallets.add(normalized);
2088
2885
  }
2089
- throw new Error(
2090
- "Unrecognized Redis client. Supported: @upstash/redis, ioredis. If using a different client, implement NonceStore interface directly."
2091
- );
2092
- }
2093
- function createRedisNonceStore(client, opts) {
2094
- const prefix = opts?.prefix ?? "siwx:nonce:";
2095
- const ttlSeconds = Math.ceil((opts?.ttlMs ?? SIWX_CHALLENGE_EXPIRY_MS) / 1e3);
2096
- const clientType = detectRedisClientType(client);
2886
+ };
2887
+ function createKvEntitlementStore(kv, options) {
2888
+ const prefix = options?.prefix ?? "siwx:ent:";
2097
2889
  return {
2098
- async check(nonce) {
2099
- const key = `${prefix}${nonce}`;
2100
- if (clientType === "upstash") {
2101
- const redis = client;
2102
- const result = await redis.set(key, "1", { ex: ttlSeconds, nx: true });
2103
- return result !== null;
2104
- }
2105
- if (clientType === "ioredis") {
2106
- const redis = client;
2107
- const result = await redis.set(key, "1", "EX", ttlSeconds, "NX");
2108
- return result === "OK";
2109
- }
2110
- throw new Error("Unknown Redis client type");
2890
+ async has(route, wallet) {
2891
+ return kv.sismember(`${prefix}${route}`, normalizeWalletAddress(wallet));
2892
+ },
2893
+ async grant(route, wallet) {
2894
+ await kv.sadd(`${prefix}${route}`, normalizeWalletAddress(wallet));
2111
2895
  }
2112
2896
  };
2113
2897
  }
2114
2898
 
2899
+ // src/kv-store/mpp.ts
2900
+ async function createKvMppStore(kv, options) {
2901
+ const prefix = options?.prefix ?? "mpp:";
2902
+ const namespaced = withPrefix(kv, prefix);
2903
+ const { Store: StoreNs } = await import("mppx");
2904
+ return StoreNs.upstash({
2905
+ get: (key) => namespaced.get(key),
2906
+ set: (key, value) => namespaced.set(key, value),
2907
+ del: (key) => namespaced.del(key),
2908
+ update: (key, fn) => namespaced.update(key, fn)
2909
+ });
2910
+ }
2911
+
2912
+ // src/protocols/detect.ts
2913
+ function detectProtocol(request) {
2914
+ if (request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)) {
2915
+ return "x402";
2916
+ }
2917
+ const auth = request.headers.get(HEADERS.AUTHORIZATION);
2918
+ if (auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT)) {
2919
+ return "mpp";
2920
+ }
2921
+ if (request.headers.get(HEADERS.SIWX)) {
2922
+ return "siwx";
2923
+ }
2924
+ return null;
2925
+ }
2926
+
2115
2927
  // src/protocols/mpp/siwx-mode.ts
2116
2928
  import { Credential as Credential2 } from "mppx";
2117
2929
  async function verifyMppSiwx(request, mppx) {
@@ -2130,12 +2942,11 @@ async function runSiwxOnlyFlow(ctx) {
2130
2942
  const { request, routeEntry, deps } = ctx;
2131
2943
  if (routeEntry.validateFn && routeEntry.bodySchema && !request.headers.get(HEADERS.SIWX)) {
2132
2944
  const earlyClone = request.clone();
2133
- const earlyBody = await parseBody(earlyClone, routeEntry);
2945
+ const earlyBody = await parseBody(ctx, earlyClone);
2134
2946
  if (earlyBody.ok) {
2135
2947
  const validateErr = await runValidate(ctx, earlyBody.data);
2136
2948
  if (validateErr) return validateErr;
2137
2949
  } else {
2138
- firePluginResponse(ctx, earlyBody.response);
2139
2950
  return earlyBody.response;
2140
2951
  }
2141
2952
  }
@@ -2147,20 +2958,12 @@ async function runSiwxOnlyFlow(ctx) {
2147
2958
  mppSiwxResult = await verifyMppSiwx(request, deps.mppx);
2148
2959
  } catch (err) {
2149
2960
  const message = err instanceof Error ? err.message : String(err);
2150
- firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
2151
- level: "critical",
2152
- message: `MPP SIWX verification failed: ${message}`,
2153
- route: routeEntry.key
2154
- });
2961
+ ctx.report("critical", `MPP SIWX verification failed: ${message}`);
2155
2962
  return fail(ctx, 500, `MPP SIWX verification failed: ${message}`);
2156
2963
  }
2157
2964
  if (mppSiwxResult.valid) {
2158
2965
  ctx.pluginCtx.setVerifiedWallet(mppSiwxResult.wallet);
2159
- firePluginHook(deps.plugin, "onAuthVerified", ctx.pluginCtx, {
2160
- authMode: "siwx",
2161
- wallet: mppSiwxResult.wallet,
2162
- route: routeEntry.key
2163
- });
2966
+ fireAuthVerified(ctx, { authMode: "siwx", wallet: mppSiwxResult.wallet });
2164
2967
  const authResponse = await runHandlerOnly(ctx, mppSiwxResult.wallet, void 0);
2165
2968
  if (authResponse.status < 400) {
2166
2969
  return mppSiwxResult.withReceipt(authResponse);
@@ -2173,7 +2976,7 @@ async function runSiwxOnlyFlow(ctx) {
2173
2976
  }
2174
2977
  const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
2175
2978
  if (!siwx.valid) {
2176
- const response = NextResponse5.json(
2979
+ const response = NextResponse7.json(
2177
2980
  { error: siwx.code, message: SIWX_ERROR_MESSAGES[siwx.code] },
2178
2981
  { status: 402 }
2179
2982
  );
@@ -2182,11 +2985,7 @@ async function runSiwxOnlyFlow(ctx) {
2182
2985
  }
2183
2986
  const wallet = normalizeWalletAddress(siwx.wallet);
2184
2987
  ctx.pluginCtx.setVerifiedWallet(wallet);
2185
- firePluginHook(deps.plugin, "onAuthVerified", ctx.pluginCtx, {
2186
- authMode: "siwx",
2187
- wallet,
2188
- route: routeEntry.key
2189
- });
2988
+ fireAuthVerified(ctx, { authMode: "siwx", wallet });
2190
2989
  return runHandlerOnly(ctx, wallet, void 0);
2191
2990
  }
2192
2991
  async function buildSiwxChallenge(ctx) {
@@ -2223,7 +3022,6 @@ async function buildSiwxChallenge(ctx) {
2223
3022
  extensions: {
2224
3023
  "sign-in-with-x": {
2225
3024
  info: siwxInfo,
2226
- // Required by MCP tools at the top level for chain detection.
2227
3025
  supportedChains,
2228
3026
  ...siwxSchema ? { schema: siwxSchema } : {}
2229
3027
  }
@@ -2234,13 +3032,12 @@ async function buildSiwxChallenge(ctx) {
2234
3032
  const { encodePaymentRequiredHeader } = await import("@x402/core/http");
2235
3033
  encoded = encodePaymentRequiredHeader(paymentRequired);
2236
3034
  } catch (err) {
2237
- firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
2238
- level: "warn",
2239
- message: `SIWX challenge header encoding failed: ${err instanceof Error ? err.message : String(err)}`,
2240
- route: routeEntry.key
2241
- });
3035
+ ctx.report(
3036
+ "warn",
3037
+ `SIWX challenge header encoding failed: ${err instanceof Error ? err.message : String(err)}`
3038
+ );
2242
3039
  }
2243
- const response = new NextResponse5(JSON.stringify(paymentRequired), {
3040
+ const response = new NextResponse7(JSON.stringify(paymentRequired), {
2244
3041
  status: 402,
2245
3042
  headers: { "Content-Type": "application/json", "Cache-Control": "no-store" }
2246
3043
  });
@@ -2281,7 +3078,7 @@ async function runUnprotectedFlow(ctx) {
2281
3078
  return runHandlerOnly(ctx, null, void 0);
2282
3079
  }
2283
3080
 
2284
- // src/orchestrate.ts
3081
+ // src/pipeline/orchestrate.ts
2285
3082
  function createRequestHandler(routeEntry, handler, deps) {
2286
3083
  return async (request) => {
2287
3084
  await deps.initPromise;
@@ -2346,6 +3143,12 @@ var RouteBuilder = class {
2346
3143
  /** @internal */
2347
3144
  _minPrice;
2348
3145
  /** @internal */
3146
+ _dynamicPrice = false;
3147
+ /** @internal */
3148
+ _tickCost;
3149
+ /** @internal */
3150
+ _unitType;
3151
+ /** @internal */
2349
3152
  _payTo;
2350
3153
  /** @internal */
2351
3154
  _bodySchema;
@@ -2390,7 +3193,8 @@ var RouteBuilder = class {
2390
3193
  next._protocols = [...this._protocols];
2391
3194
  return next;
2392
3195
  }
2393
- paid(pricing, options) {
3196
+ paid(pricingOrOptions, options) {
3197
+ const { pricing, resolvedOptions } = resolvePaidArgs(this._key, pricingOrOptions, options);
2394
3198
  if (this._authMode === "unprotected") {
2395
3199
  throw new Error(
2396
3200
  `route '${this._key}': Cannot combine .unprotected() and .paid() on the same route.`
@@ -2404,16 +3208,24 @@ var RouteBuilder = class {
2404
3208
  const next = this.fork();
2405
3209
  next._authMode = "paid";
2406
3210
  next._pricing = pricing;
2407
- if (options?.protocols) {
2408
- next._protocols = [...options.protocols];
3211
+ if (resolvedOptions?.protocols) {
3212
+ next._protocols = [...resolvedOptions.protocols];
2409
3213
  } else if (next._protocols.length === 0) {
2410
3214
  next._protocols = ["x402"];
2411
3215
  }
2412
- if (options?.maxPrice) next._maxPrice = options.maxPrice;
2413
- if (options?.minPrice) next._minPrice = options.minPrice;
2414
- if (options?.payTo) next._payTo = options.payTo;
2415
- if (options?.mpp) next._mppInfo = options.mpp;
3216
+ if (resolvedOptions?.maxPrice) next._maxPrice = resolvedOptions.maxPrice;
3217
+ if (resolvedOptions?.minPrice) next._minPrice = resolvedOptions.minPrice;
3218
+ if (resolvedOptions?.payTo) next._payTo = resolvedOptions.payTo;
3219
+ if (resolvedOptions?.mpp) next._mppInfo = resolvedOptions.mpp;
3220
+ if (resolvedOptions?.dynamic) next._dynamicPrice = true;
3221
+ if (resolvedOptions?.tickCost) next._tickCost = resolvedOptions.tickCost;
3222
+ if (resolvedOptions?.unitType) next._unitType = resolvedOptions.unitType;
2416
3223
  if (typeof pricing === "object" && "tiers" in pricing) {
3224
+ if (next._dynamicPrice) {
3225
+ throw new Error(
3226
+ `route '${this._key}': .paid({ dynamic: true }) is incompatible with tiered pricing`
3227
+ );
3228
+ }
2417
3229
  for (const [tierKey, tierConfig] of Object.entries(pricing.tiers)) {
2418
3230
  if (!tierKey) {
2419
3231
  throw new Error(`route '${this._key}': tier key cannot be empty`);
@@ -2426,16 +3238,40 @@ var RouteBuilder = class {
2426
3238
  }
2427
3239
  }
2428
3240
  }
2429
- if (options?.maxPrice !== void 0) {
2430
- const parsed = parseFloat(options.maxPrice);
3241
+ if (resolvedOptions?.maxPrice !== void 0) {
3242
+ const parsed = parseFloat(resolvedOptions.maxPrice);
3243
+ if (isNaN(parsed) || parsed <= 0) {
3244
+ throw new Error(
3245
+ `route '${this._key}': maxPrice '${resolvedOptions.maxPrice}' must be a positive decimal string`
3246
+ );
3247
+ }
3248
+ }
3249
+ if (resolvedOptions?.tickCost !== void 0) {
3250
+ const parsed = parseFloat(resolvedOptions.tickCost);
2431
3251
  if (isNaN(parsed) || parsed <= 0) {
2432
3252
  throw new Error(
2433
- `route '${this._key}': maxPrice '${options.maxPrice}' must be a positive decimal string`
3253
+ `route '${this._key}': tickCost '${resolvedOptions.tickCost}' must be a positive decimal string`
2434
3254
  );
2435
3255
  }
2436
3256
  }
3257
+ if (next._dynamicPrice && !next._maxPrice) {
3258
+ throw new Error(`route '${this._key}': .paid({ dynamic: true }) requires maxPrice`);
3259
+ }
3260
+ if (next._dynamicPrice && !next._tickCost) {
3261
+ throw new Error(`route '${this._key}': .paid({ dynamic: true }) requires tickCost`);
3262
+ }
2437
3263
  return next;
2438
3264
  }
3265
+ /**
3266
+ * Require Sign-In-with-X wallet identity on this route — clients prove
3267
+ * control of a wallet via a signed challenge. Combine with `.paid()` to gate
3268
+ * a paid route on a verified wallet identity.
3269
+ *
3270
+ * @example
3271
+ * ```ts
3272
+ * router.route('profile').siwx().handler(async ({ wallet }) => getProfile(wallet));
3273
+ * ```
3274
+ */
2439
3275
  siwx() {
2440
3276
  if (this._authMode === "unprotected") {
2441
3277
  throw new Error(
@@ -2458,6 +3294,19 @@ var RouteBuilder = class {
2458
3294
  next._protocols = [];
2459
3295
  return next;
2460
3296
  }
3297
+ /**
3298
+ * Require an `X-API-Key` header (or `Authorization: Bearer <key>`); the
3299
+ * resolver returns the account record, or `null` for 401. Composes with
3300
+ * `.paid()` — key is checked first, payment second.
3301
+ *
3302
+ * @example
3303
+ * ```ts
3304
+ * router
3305
+ * .route('admin/users')
3306
+ * .apiKey(async (key) => db.admin.findByKey(key))
3307
+ * .handler(async ({ account }) => db.user.list(account.orgId));
3308
+ * ```
3309
+ */
2461
3310
  apiKey(resolver) {
2462
3311
  if (this._siwxEnabled) {
2463
3312
  throw new Error(
@@ -2469,6 +3318,15 @@ var RouteBuilder = class {
2469
3318
  next._apiKeyResolver = resolver;
2470
3319
  return next;
2471
3320
  }
3321
+ /**
3322
+ * Mark the route as public — no auth, no payment, no SIWX. The handler
3323
+ * receives `null` for `wallet`, `payment`, and `account`.
3324
+ *
3325
+ * @example
3326
+ * ```ts
3327
+ * router.route('health').unprotected().handler(async () => ({ status: 'ok' }));
3328
+ * ```
3329
+ */
2472
3330
  unprotected() {
2473
3331
  if (this._authMode && this._authMode !== "unprotected") {
2474
3332
  throw new Error(
@@ -2485,60 +3343,82 @@ var RouteBuilder = class {
2485
3343
  next._protocols = [];
2486
3344
  return next;
2487
3345
  }
2488
- // -------------------------------------------------------------------------
2489
- // Provider monitoring
2490
- // -------------------------------------------------------------------------
3346
+ /**
3347
+ * Tag the route with an upstream provider for discovery and provider-side
3348
+ * monitoring. The provider name and config surface in `well-known` and
3349
+ * OpenAPI output.
3350
+ *
3351
+ * @example
3352
+ * ```ts
3353
+ * router
3354
+ * .route('search')
3355
+ * .paid('0.01')
3356
+ * .provider('exa', { quotaPerMonth: 1000 })
3357
+ * .handler(handler);
3358
+ * ```
3359
+ */
2491
3360
  provider(name, config) {
2492
3361
  const next = this.fork();
2493
3362
  next._providerName = name;
2494
3363
  next._providerConfig = config ?? {};
2495
3364
  return next;
2496
3365
  }
2497
- body(schema, example) {
3366
+ /**
3367
+ * Declare the request body's Zod schema. Parsed body is typed as `ctx.body`
3368
+ * in the handler. Use `.inputExample()` to attach a discovery example.
3369
+ *
3370
+ * @example
3371
+ * ```ts
3372
+ * .body(z.object({ query: z.string() }))
3373
+ * .handler(async ({ body }) => search(body.query));
3374
+ * ```
3375
+ */
3376
+ body(schema) {
2498
3377
  const next = this.fork();
2499
3378
  next._bodySchema = schema;
2500
- if (example !== void 0) {
2501
- next._inputExample = example;
2502
- next._hasInputExample = true;
2503
- }
2504
3379
  return next;
2505
3380
  }
2506
- query(schema, example) {
3381
+ /**
3382
+ * Declare a query-string Zod schema and switch the route to `GET`. Parsed
3383
+ * query is typed as `ctx.query` in the handler. Use `.inputExample()` to
3384
+ * attach a discovery example.
3385
+ *
3386
+ * @example
3387
+ * ```ts
3388
+ * .query(z.object({ id: z.string() }))
3389
+ * .handler(async ({ query }) => getById(query.id));
3390
+ * ```
3391
+ */
3392
+ query(schema) {
2507
3393
  const next = this.fork();
2508
3394
  next._querySchema = schema;
2509
- if (example !== void 0) {
2510
- next._inputExample = example;
2511
- next._hasInputExample = true;
2512
- }
2513
3395
  next._method = "GET";
2514
3396
  return next;
2515
3397
  }
2516
- output(schema, example) {
3398
+ /**
3399
+ * Declare the response output's Zod schema for OpenAPI generation. The
3400
+ * runtime does not validate handler return values — use Zod's `.parse()`
3401
+ * inside the handler if strict output validation is required. Use
3402
+ * `.outputExample()` to attach a discovery example.
3403
+ *
3404
+ * @example
3405
+ * ```ts
3406
+ * .output(z.object({ result: z.string() }))
3407
+ * .handler(async () => ({ result: 'ok' }));
3408
+ * ```
3409
+ */
3410
+ output(schema) {
2517
3411
  const next = this.fork();
2518
3412
  next._outputSchema = schema;
2519
- if (example !== void 0) {
2520
- next._outputExample = example;
2521
- next._hasOutputExample = true;
2522
- }
2523
3413
  return next;
2524
3414
  }
2525
3415
  /**
2526
- * Provide a conforming example of the request input (body or query params).
2527
- *
2528
- * Optional. When provided, the example is validated against the request schema
2529
- * at route registration and embedded in the bazaar discovery extension so
2530
- * indexers can advertise a working sample call.
2531
- *
2532
- * For the common case, pass the example directly to `.body(schema, example)` or
2533
- * `.query(schema, example)` instead.
3416
+ * Attach an example of the request body or query for discovery output,
3417
+ * validated against the registered schema at registration.
2534
3418
  *
2535
3419
  * @example
2536
3420
  * ```ts
2537
- * router.route('search')
2538
- * .paid('0.01')
2539
- * .body(z.object({ q: z.string() }))
2540
- * .inputExample({ q: 'hello world' })
2541
- * .handler(async ({ body }) => { ... });
3421
+ * .body(searchSchema).inputExample({ query: 'cats' });
2542
3422
  * ```
2543
3423
  */
2544
3424
  inputExample(example) {
@@ -2548,32 +3428,12 @@ var RouteBuilder = class {
2548
3428
  return next;
2549
3429
  }
2550
3430
  /**
2551
- * Provide a conforming example of the response output.
2552
- *
2553
- * Optional. When provided, the example is validated against the output schema
2554
- * at route registration and embedded in the bazaar discovery extension so
2555
- * indexers can advertise the response shape.
2556
- *
2557
- * For the common case, pass the example directly to `.output(schema, example)` instead.
2558
- *
2559
- * Accepts any JSON value (objects, arrays, or primitives) — top-level array
2560
- * or primitive responses (e.g. `z.array(...)`) are supported alongside the
2561
- * common object case.
3431
+ * Attach an example response for discovery output, validated against the
3432
+ * registered output schema at registration.
2562
3433
  *
2563
3434
  * @example
2564
3435
  * ```ts
2565
- * router.route('search')
2566
- * .paid('0.01')
2567
- * .output(z.object({ results: z.array(z.string()) }))
2568
- * .outputExample({ results: ['a', 'b'] })
2569
- * .handler(async () => { ... });
2570
- *
2571
- * // Top-level array response
2572
- * router.route('chains')
2573
- * .paid('0.01')
2574
- * .output(z.array(z.object({ name: z.string() })))
2575
- * .outputExample([{ name: 'Ethereum' }])
2576
- * .handler(async () => { ... });
3436
+ * .output(resultSchema).outputExample({ result: 'ok' });
2577
3437
  * ```
2578
3438
  */
2579
3439
  outputExample(example) {
@@ -2582,71 +3442,125 @@ var RouteBuilder = class {
2582
3442
  next._hasOutputExample = true;
2583
3443
  return next;
2584
3444
  }
3445
+ /**
3446
+ * Set a human-readable summary of the route. Surfaces in OpenAPI,
3447
+ * `well-known`, and `llms.txt` discovery output.
3448
+ *
3449
+ * @example
3450
+ * ```ts
3451
+ * .description('Search indexed web pages by full-text query');
3452
+ * ```
3453
+ */
2585
3454
  description(text) {
2586
3455
  const next = this.fork();
2587
3456
  next._description = text;
2588
3457
  return next;
2589
3458
  }
3459
+ /**
3460
+ * Override the URL path advertised in discovery output. Defaults to the
3461
+ * registry key passed to `.route()`.
3462
+ *
3463
+ * @example
3464
+ * ```ts
3465
+ * router.route('search').path('/v2/search').handler(handler);
3466
+ * ```
3467
+ */
2590
3468
  path(p) {
2591
3469
  const next = this.fork();
2592
3470
  next._path = p;
2593
3471
  return next;
2594
3472
  }
3473
+ /**
3474
+ * Override the HTTP method advertised in discovery. Defaults to `POST`, or
3475
+ * `GET` when `.query()` has been called.
3476
+ *
3477
+ * @example
3478
+ * ```ts
3479
+ * router.route('items/delete').method('DELETE').handler(handler);
3480
+ * ```
3481
+ */
2595
3482
  method(m) {
2596
3483
  const next = this.fork();
2597
3484
  next._method = m;
2598
3485
  return next;
2599
3486
  }
2600
- // -------------------------------------------------------------------------
2601
- // Pre-payment validation
2602
- // -------------------------------------------------------------------------
2603
3487
  /**
2604
- * Add pre-payment validation that runs after body parsing but before the 402
2605
- * challenge is shown. Use this for async business logic like "is this resource
2606
- * available?" or "has this user hit their rate limit?".
3488
+ * Run validation against the parsed body before the 402 challenge. Throw
3489
+ * `Object.assign(new Error('...'), { status })` to reject with a custom
3490
+ * status code; defaults to 400. Requires `.body()` to be called first.
3491
+ *
3492
+ * @example
3493
+ * ```ts
3494
+ * .body(RegisterSchema).validate(async (body) => {
3495
+ * if (await isTaken(body.name)) {
3496
+ * throw Object.assign(new Error('taken'), { status: 409 });
3497
+ * }
3498
+ * });
3499
+ * ```
3500
+ */
3501
+ validate(fn) {
3502
+ const next = this.fork();
3503
+ next._validateFn = fn;
3504
+ return next;
3505
+ }
3506
+ /**
3507
+ * Hook into the settlement lifecycle. `beforeSettle` runs after the handler
3508
+ * succeeds but before on-chain settlement and can cancel the charge;
3509
+ * `afterSettle` runs after settlement completes (success or failure).
2607
3510
  *
2608
- * Requires `.body()` — call `.body()` before `.validate()` for type inference.
3511
+ * @example
3512
+ * ```ts
3513
+ * .settlement({
3514
+ * beforeSettle: ({ result }) => (result.refund ? 'skip' : 'continue'),
3515
+ * afterSettle: ({ tx }) => analytics.track('settled', { tx }),
3516
+ * });
3517
+ * ```
3518
+ */
3519
+ settlement(lifecycle) {
3520
+ const next = this.fork();
3521
+ next._settlement = lifecycle;
3522
+ return next;
3523
+ }
3524
+ /**
3525
+ * Register the request handler and return the Next.js route function. The
3526
+ * handler receives a typed context and may return a value (serialized to
3527
+ * JSON), a raw `Response`, or throw an `HttpError` for a non-2xx status.
2609
3528
  *
2610
3529
  * @example
2611
- * ```typescript
2612
- * router
2613
- * .route('domain/register')
2614
- * .paid(calculatePrice)
2615
- * .body(RegisterSchema) // .body() first for type inference
2616
- * .validate(async (body) => {
2617
- * if (await isDomainTaken(body.domain)) {
2618
- * throw Object.assign(new Error('Domain taken'), { status: 409 });
2619
- * }
2620
- * })
2621
- * .handler(async ({ body }) => { ... });
3530
+ * ```ts
3531
+ * export const POST = router
3532
+ * .route('search')
3533
+ * .paid('0.01')
3534
+ * .body(schema)
3535
+ * .handler(async ({ body, wallet }) => searchService(body, wallet));
2622
3536
  * ```
2623
3537
  */
2624
- validate(fn) {
2625
- const next = this.fork();
2626
- next._validateFn = fn;
2627
- return next;
3538
+ handler(fn) {
3539
+ return this.register(fn, false);
2628
3540
  }
2629
- // -------------------------------------------------------------------------
2630
- // Settlement lifecycle
2631
- // -------------------------------------------------------------------------
2632
3541
  /**
2633
- * Add route-specific settlement hooks.
3542
+ * Register a streaming handler (`async function*`) and return the Next.js
3543
+ * route function. Each `charge()` call bills one tick (`tickCost` USDC) up
3544
+ * to `maxPrice`; requires `.paid({ dynamic: true, ... })` and MPP session mode.
2634
3545
  *
2635
- * `beforeSettle` runs after a successful handler response but before
2636
- * router-controlled settlement/broadcast, so it can still prevent the charge
2637
- * for x402 and MPP transaction-payload flows. `afterSettle` runs after
2638
- * settlement and is intended for durable ledgers or app-owned refund queues.
3546
+ * @example
3547
+ * ```ts
3548
+ * export const POST = router
3549
+ * .route('llm/stream')
3550
+ * .paid({ dynamic: true, tickCost: '0.0001', unitType: 'token', maxPrice: '0.05' })
3551
+ * .body(schema)
3552
+ * .stream(async function* ({ body, charge }) {
3553
+ * for await (const token of streamLLM(body.prompt)) {
3554
+ * await charge();
3555
+ * yield token;
3556
+ * }
3557
+ * });
3558
+ * ```
2639
3559
  */
2640
- settlement(lifecycle) {
2641
- const next = this.fork();
2642
- next._settlement = lifecycle;
2643
- return next;
3560
+ stream(fn) {
3561
+ return this.register(fn, true);
2644
3562
  }
2645
- // -------------------------------------------------------------------------
2646
- // Terminal method
2647
- // -------------------------------------------------------------------------
2648
- handler(fn) {
2649
- const handlerFn = fn;
3563
+ register(handlerFn, streaming) {
2650
3564
  if (!this._authMode) {
2651
3565
  throw new Error(
2652
3566
  `route '${this._key}': Select an auth mode: .paid(pricing), .siwx(), .apiKey(resolver), or .unprotected()`
@@ -2660,6 +3574,26 @@ var RouteBuilder = class {
2660
3574
  if (this._settlement && !this._pricing) {
2661
3575
  throw new Error(`route '${this._key}': .settlement() requires a paid route`);
2662
3576
  }
3577
+ if (this._dynamicPrice && this._protocols.includes("x402")) {
3578
+ const hasUpto = this._deps.x402Accepts.some((accept) => accept.scheme === "upto");
3579
+ if (!hasUpto) {
3580
+ throw new Error(
3581
+ `route '${this._key}': .paid({ dynamic: true }) on an x402 route requires an 'upto' accept on at least one configured network. Add { scheme: 'upto', network, asset } to RouterConfig.x402.accepts.`
3582
+ );
3583
+ }
3584
+ }
3585
+ if (this._dynamicPrice && this._protocols.includes("mpp")) {
3586
+ if (!this._deps.mppSessionConfig) {
3587
+ throw new Error(
3588
+ `route '${this._key}': .paid({ dynamic: true }) on an MPP route requires session mode. Set RouterConfig.mpp.session = {} and provide mpp.operatorKey.`
3589
+ );
3590
+ }
3591
+ }
3592
+ if (streaming && !this._dynamicPrice) {
3593
+ throw new Error(
3594
+ `route '${this._key}': .stream() requires .paid({ dynamic: true }) \u2014 static/free routes can't meter per-chunk billing.`
3595
+ );
3596
+ }
2663
3597
  validateExamples(
2664
3598
  this._key,
2665
3599
  this._bodySchema,
@@ -2675,6 +3609,8 @@ var RouteBuilder = class {
2675
3609
  authMode: this._authMode,
2676
3610
  siwxEnabled: this._siwxEnabled,
2677
3611
  pricing: this._pricing,
3612
+ dynamicPrice: this._dynamicPrice ? true : void 0,
3613
+ streaming: streaming ? true : void 0,
2678
3614
  protocols: this._protocols,
2679
3615
  bodySchema: this._bodySchema,
2680
3616
  querySchema: this._querySchema,
@@ -2692,81 +3628,28 @@ var RouteBuilder = class {
2692
3628
  providerConfig: this._providerConfig,
2693
3629
  validateFn: this._validateFn,
2694
3630
  settlement: this._settlement,
2695
- mppInfo: this._mppInfo
3631
+ mppInfo: this._mppInfo,
3632
+ tickCost: this._tickCost,
3633
+ unitType: this._unitType
2696
3634
  };
2697
3635
  this._registry.register(entry);
2698
- return createRequestHandler(
2699
- entry,
2700
- handlerFn,
2701
- this._deps
2702
- );
3636
+ return createRequestHandler(entry, handlerFn, this._deps);
2703
3637
  }
2704
3638
  };
2705
-
2706
- // src/auth/entitlement.ts
2707
- var MemoryEntitlementStore = class {
2708
- routeToWallets = /* @__PURE__ */ new Map();
2709
- async has(route, wallet) {
2710
- const wallets = this.routeToWallets.get(route);
2711
- if (!wallets) return false;
2712
- return wallets.has(normalizeWalletAddress(wallet));
2713
- }
2714
- async grant(route, wallet) {
2715
- const normalized = normalizeWalletAddress(wallet);
2716
- let wallets = this.routeToWallets.get(route);
2717
- if (!wallets) {
2718
- wallets = /* @__PURE__ */ new Set();
2719
- this.routeToWallets.set(route, wallets);
3639
+ function resolvePaidArgs(routeKey, pricingOrOptions, options) {
3640
+ const isHandlerDynamicShape = typeof pricingOrOptions === "object" && pricingOrOptions !== null && typeof pricingOrOptions !== "function" && !("tiers" in pricingOrOptions) && "dynamic" in pricingOrOptions && pricingOrOptions.dynamic;
3641
+ if (isHandlerDynamicShape) {
3642
+ const opts = pricingOrOptions;
3643
+ if (!opts.maxPrice) {
3644
+ throw new Error(`route '${routeKey}': .paid({ dynamic: true }) requires maxPrice`);
2720
3645
  }
2721
- wallets.add(normalized);
2722
- }
2723
- };
2724
- function detectRedisClientType2(client) {
2725
- if (!client || typeof client !== "object") {
2726
- throw new Error(
2727
- "createRedisEntitlementStore requires a Redis client. Supported: @upstash/redis, ioredis."
2728
- );
3646
+ return { pricing: opts.maxPrice, resolvedOptions: opts };
2729
3647
  }
2730
- if ("options" in client && "status" in client) return "ioredis";
2731
- const constructor = client.constructor?.name;
2732
- if (constructor === "Redis" && "url" in client) return "upstash";
2733
- if (typeof client.sadd === "function" && typeof client.sismember === "function") {
2734
- return "upstash";
2735
- }
2736
- throw new Error("Unrecognized Redis client for entitlement store.");
2737
- }
2738
- function createRedisEntitlementStore(client, options) {
2739
- const clientType = detectRedisClientType2(client);
2740
- const prefix = options?.prefix ?? "siwx:entitlement:";
2741
- return {
2742
- async has(route, wallet) {
2743
- const key = `${prefix}${route}`;
2744
- const normalized = normalizeWalletAddress(wallet);
2745
- if (clientType === "upstash") {
2746
- const redis2 = client;
2747
- const result2 = await redis2.sismember(key, normalized);
2748
- return result2 === 1 || result2 === true;
2749
- }
2750
- const redis = client;
2751
- const result = await redis.sismember(key, normalized);
2752
- return result === 1;
2753
- },
2754
- async grant(route, wallet) {
2755
- const key = `${prefix}${route}`;
2756
- const normalized = normalizeWalletAddress(wallet);
2757
- if (clientType === "upstash") {
2758
- const redis2 = client;
2759
- await redis2.sadd(key, normalized);
2760
- return;
2761
- }
2762
- const redis = client;
2763
- await redis.sadd(key, normalized);
2764
- }
2765
- };
3648
+ return { pricing: pricingOrOptions, resolvedOptions: options };
2766
3649
  }
2767
3650
 
2768
3651
  // src/discovery/well-known.ts
2769
- import { NextResponse as NextResponse6 } from "next/server";
3652
+ import { NextResponse as NextResponse8 } from "next/server";
2770
3653
 
2771
3654
  // src/discovery/utils/guidance.ts
2772
3655
  async function resolveGuidance(discovery) {
@@ -2810,7 +3693,7 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, discovery) {
2810
3693
  if (instructions) {
2811
3694
  body.instructions = instructions;
2812
3695
  }
2813
- return NextResponse6.json(body, {
3696
+ return NextResponse8.json(body, {
2814
3697
  headers: {
2815
3698
  "Access-Control-Allow-Origin": "*",
2816
3699
  "Access-Control-Allow-Methods": "GET",
@@ -2828,13 +3711,13 @@ function toDiscoveryResource(method, url, mode) {
2828
3711
 
2829
3712
  // src/discovery/openapi.ts
2830
3713
  init_constants();
2831
- import { NextResponse as NextResponse7 } from "next/server";
3714
+ import { NextResponse as NextResponse9 } from "next/server";
2832
3715
  function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
2833
3716
  const normalizedBase = baseUrl.replace(/\/+$/, "");
2834
3717
  let cached = null;
2835
3718
  let validated = false;
2836
3719
  return async (_request) => {
2837
- if (cached) return NextResponse7.json(cached);
3720
+ if (cached) return NextResponse9.json(cached);
2838
3721
  if (!validated && pricesKeys) {
2839
3722
  registry.validate(pricesKeys);
2840
3723
  validated = true;
@@ -2897,7 +3780,7 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
2897
3780
  paths
2898
3781
  };
2899
3782
  cached = createDocument(openApiDocument);
2900
- return NextResponse7.json(cached);
3783
+ return NextResponse9.json(cached);
2901
3784
  };
2902
3785
  }
2903
3786
  function deriveTag(routeKey) {
@@ -2970,7 +3853,7 @@ function toProtocolObject(protocol, mppInfo) {
2970
3853
  mpp: {
2971
3854
  method: mppInfo?.method ?? "tempo",
2972
3855
  intent: mppInfo?.intent ?? "charge",
2973
- currency: mppInfo?.currency ?? TEMPO_USDC_CURRENCY
3856
+ currency: mppInfo?.currency ?? TEMPO_USDC_ADDRESS
2974
3857
  }
2975
3858
  };
2976
3859
  }
@@ -3020,11 +3903,11 @@ function buildPricingInfo(entry) {
3020
3903
  }
3021
3904
 
3022
3905
  // src/discovery/llms-txt.ts
3023
- import { NextResponse as NextResponse8 } from "next/server";
3906
+ import { NextResponse as NextResponse10 } from "next/server";
3024
3907
  function createLlmsTxtHandler(discovery) {
3025
3908
  return async (_request) => {
3026
3909
  const guidance = await resolveGuidance(discovery) ?? "";
3027
- return new NextResponse8(guidance, {
3910
+ return new NextResponse10(guidance, {
3028
3911
  headers: {
3029
3912
  "Content-Type": "text/plain; charset=utf-8",
3030
3913
  "Access-Control-Allow-Origin": "*"
@@ -3037,11 +3920,7 @@ function createLlmsTxtHandler(discovery) {
3037
3920
  init_accepts();
3038
3921
  init_constants();
3039
3922
 
3040
- // src/config.ts
3041
- init_constants();
3042
- init_evm();
3043
- init_solana();
3044
- init_accepts();
3923
+ // src/config/error.ts
3045
3924
  var RouterConfigError = class extends Error {
3046
3925
  issues;
3047
3926
  constructor(issues) {
@@ -3050,184 +3929,260 @@ var RouterConfigError = class extends Error {
3050
3929
  this.issues = issues;
3051
3930
  }
3052
3931
  };
3053
- function validateRouterConfig(config, options = {}) {
3054
- const issues = getRouterConfigIssues(config, options);
3055
- if (issues.length > 0) throw new RouterConfigError(issues);
3932
+ function formatRouterConfigIssues(issues) {
3933
+ return issues.map((issue) => issue.message).join("\n");
3056
3934
  }
3057
- function getRouterConfigIssues(config, options = {}) {
3058
- const env = options.env ?? process.env;
3059
- const issues = [];
3060
- const protocols = config.protocols ?? ["x402"];
3061
- if (!config.baseUrl) {
3062
- issues.push({
3063
- code: "missing_base_url",
3064
- message: '[router] baseUrl is required in RouterConfig. Set it to your production domain (e.g., "https://api.example.com"). The realm is used for payment matching and must be correct.'
3065
- });
3935
+
3936
+ // src/config/schema.ts
3937
+ init_constants();
3938
+ import { z } from "zod";
3939
+
3940
+ // src/config/utils.ts
3941
+ import { privateKeyToAccount } from "viem/accounts";
3942
+ var EVM_ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
3943
+ var EVM_PRIVATE_KEY_RE = /^0x[a-fA-F0-9]{64}$/;
3944
+ var SOLANA_ADDRESS_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
3945
+ var ZERO_EVM_ADDRESS_RE = /^0x0{40}$/i;
3946
+ function isUrl(value) {
3947
+ try {
3948
+ new URL(value);
3949
+ return true;
3950
+ } catch {
3951
+ return false;
3952
+ }
3953
+ }
3954
+ var isEvmAddress = (v) => EVM_ADDRESS_RE.test(v);
3955
+ var isEvmPrivateKey = (v) => EVM_PRIVATE_KEY_RE.test(v);
3956
+ var isPlaceholderEvm = (v) => ZERO_EVM_ADDRESS_RE.test(v);
3957
+ var isSolanaAddress = (v) => SOLANA_ADDRESS_RE.test(v);
3958
+ var isX402Network = (v) => v.startsWith("eip155:") || v.startsWith("solana:");
3959
+ var canonicalizeEvm = (addr) => addr.toLowerCase();
3960
+ function operatorAddressesCollide(opKey, fpKey) {
3961
+ if (!opKey || !fpKey || !isEvmPrivateKey(opKey) || !isEvmPrivateKey(fpKey)) return null;
3962
+ const op = privateKeyToAccount(opKey).address.toLowerCase();
3963
+ const fp = privateKeyToAccount(fpKey).address.toLowerCase();
3964
+ return op === fp ? op : null;
3965
+ }
3966
+ function trimAll(raw) {
3967
+ const out = {};
3968
+ for (const [k, v] of Object.entries(raw)) {
3969
+ if (typeof v !== "string") {
3970
+ out[k] = void 0;
3971
+ continue;
3972
+ }
3973
+ const trimmed = v.trim();
3974
+ out[k] = trimmed.length > 0 ? trimmed : void 0;
3066
3975
  }
3067
- if (config.protocols && config.protocols.length === 0) {
3068
- issues.push({
3069
- code: "empty_protocols",
3070
- message: "RouterConfig.protocols cannot be empty. Omit the field to use default ['x402'] or specify protocols explicitly."
3071
- });
3976
+ return out;
3977
+ }
3978
+
3979
+ // src/config/schema.ts
3980
+ function addIssue(ctx, params, message, path = []) {
3981
+ ctx.addIssue({ code: "custom", path, params, message });
3982
+ }
3983
+ var x402 = { protocol: "x402" };
3984
+ var mpp = { protocol: "mpp" };
3985
+ var envShape = {
3986
+ BASE_URL: z.string().refine(isUrl, {
3987
+ params: { code: "invalid_base_url" },
3988
+ message: "BASE_URL must be a valid URL \u2014 the public origin used as the 402 realm, OpenAPI server URL, and MPP memo prefix. Must match the public domain."
3989
+ }).optional(),
3990
+ EVM_PAYEE_ADDRESS: z.string().refine(isEvmAddress, {
3991
+ params: { code: "invalid_x402_payee", ...x402 },
3992
+ message: "EVM_PAYEE_ADDRESS must be a 0x-prefixed 20-byte EVM address \u2014 the wallet that receives x402 and MPP payments."
3993
+ }).refine((v) => !isPlaceholderEvm(v), {
3994
+ params: { code: "placeholder_payee", ...x402 },
3995
+ message: "EVM_PAYEE_ADDRESS is the zero address (0x000\u2026000) \u2014 payments to this address are unrecoverable. Set it to a wallet you control."
3996
+ }).optional(),
3997
+ CDP_API_KEY_ID: z.string().optional(),
3998
+ CDP_API_KEY_SECRET: z.string().optional(),
3999
+ SOLANA_PAYEE_ADDRESS: z.string().refine(isSolanaAddress, {
4000
+ params: { code: "invalid_solana_payee", ...x402 },
4001
+ message: "SOLANA_PAYEE_ADDRESS must be a base58 Solana address (32\u201344 chars). When set, the router also accepts Solana payments."
4002
+ }).optional(),
4003
+ SOLANA_FACILITATOR_URL: z.string().refine(isUrl, {
4004
+ params: { code: "invalid_solana_facilitator_url", ...x402 },
4005
+ message: "SOLANA_FACILITATOR_URL must be a valid URL \u2014 override for the Solana x402 facilitator. Defaults to DEFAULT_SOLANA_FACILITATOR_URL."
4006
+ }).optional(),
4007
+ MPP_SECRET_KEY: z.string().optional(),
4008
+ MPP_CURRENCY: z.string().refine(isEvmAddress, {
4009
+ params: { code: "invalid_mpp_currency", ...mpp },
4010
+ message: "MPP_CURRENCY must be a 0x-prefixed 20-byte Tempo currency address \u2014 the token contract MPP charges in. Use TEMPO_USDC_ADDRESS for Tempo USDC."
4011
+ }).optional(),
4012
+ TEMPO_RPC_URL: z.string().refine(isUrl, {
4013
+ params: { code: "invalid_mpp_rpc_url", ...mpp },
4014
+ message: "TEMPO_RPC_URL must be a valid URL \u2014 authenticated Tempo JSON-RPC endpoint. Public rpc.tempo.xyz returns 401."
4015
+ }).optional(),
4016
+ MPP_OPERATOR_KEY: z.string().refine(isEvmPrivateKey, {
4017
+ params: { code: "invalid_mpp_operator_key", ...mpp },
4018
+ message: "MPP_OPERATOR_KEY must be a 0x-prefixed 32-byte EVM private key \u2014 signs server-side close/settle; presence enables MPP session mode."
4019
+ }).optional(),
4020
+ MPP_FEE_PAYER_KEY: z.string().refine(isEvmPrivateKey, {
4021
+ params: { code: "invalid_mpp_fee_payer_key", ...mpp },
4022
+ message: "MPP_FEE_PAYER_KEY must be a 0x-prefixed 32-byte EVM private key \u2014 sponsors client gas for channel open/topUp. Must resolve to a different address than MPP_OPERATOR_KEY."
4023
+ }).optional(),
4024
+ KV_REST_API_URL: z.string().optional(),
4025
+ KV_REST_API_TOKEN: z.string().optional(),
4026
+ NODE_ENV: z.string().optional()
4027
+ };
4028
+ var ENV_KEYS = Object.keys(envShape);
4029
+ var EnvInputSchema = z.object(envShape).passthrough().superRefine((env, ctx) => {
4030
+ if (env.BASE_URL === void 0) {
4031
+ addIssue(
4032
+ ctx,
4033
+ { code: "missing_base_url" },
4034
+ "BASE_URL is required \u2014 the public origin used as the 402 realm, OpenAPI server URL, and MPP memo prefix. Set it to your production domain.",
4035
+ ["BASE_URL"]
4036
+ );
4037
+ }
4038
+ if (env.EVM_PAYEE_ADDRESS === void 0) {
4039
+ addIssue(
4040
+ ctx,
4041
+ { code: "missing_x402_payee", ...x402 },
4042
+ "EVM_PAYEE_ADDRESS is required \u2014 the EVM address that receives x402 and MPP payments.",
4043
+ ["EVM_PAYEE_ADDRESS"]
4044
+ );
3072
4045
  }
3073
- if (protocols.includes("x402")) {
3074
- issues.push(...validateX402Config(config, env, options));
4046
+ if (env.MPP_SECRET_KEY) {
4047
+ if (env.MPP_CURRENCY === void 0) {
4048
+ addIssue(
4049
+ ctx,
4050
+ { code: "missing_mpp_currency", ...mpp },
4051
+ "MPP_CURRENCY is required when MPP is enabled \u2014 the Tempo currency address MPP charges in. Use TEMPO_USDC_ADDRESS for Tempo USDC.",
4052
+ ["MPP_CURRENCY"]
4053
+ );
4054
+ }
4055
+ if (env.TEMPO_RPC_URL === void 0) {
4056
+ addIssue(
4057
+ ctx,
4058
+ { code: "missing_mpp_rpc_url", ...mpp },
4059
+ "TEMPO_RPC_URL is required when MPP is enabled \u2014 authenticated Tempo JSON-RPC endpoint. Public rpc.tempo.xyz returns 401.",
4060
+ ["TEMPO_RPC_URL"]
4061
+ );
4062
+ }
3075
4063
  }
3076
- if (protocols.includes("mpp")) {
3077
- issues.push(...validateMppConfig(config, env));
4064
+ const collision = operatorAddressesCollide(env.MPP_OPERATOR_KEY, env.MPP_FEE_PAYER_KEY);
4065
+ if (collision) {
4066
+ addIssue(
4067
+ ctx,
4068
+ { code: "mpp_operator_equals_fee_payer", ...mpp },
4069
+ `MPP_OPERATOR_KEY and MPP_FEE_PAYER_KEY resolve to the same address (${collision}). Tempo rejects fee-delegated txs with sender === feePayer. Use two distinct wallets, or unset MPP_FEE_PAYER_KEY to let clients pay their own gas.`,
4070
+ ["MPP_FEE_PAYER_KEY"]
4071
+ );
3078
4072
  }
3079
- return issues;
3080
- }
3081
- function formatRouterConfigIssues(issues) {
3082
- return issues.map((issue) => issue.message).join("\n");
3083
- }
3084
- function mppFromEnv(env, options = {}) {
3085
- const secretKey = env.MPP_SECRET_KEY;
3086
- const currency = env.MPP_CURRENCY;
3087
- const rpcUrl = env.TEMPO_RPC_URL;
3088
- const feePayerKey = options.feePayerKey ?? env.MPP_FEE_PAYER_KEY;
3089
- const feePayerKeySource = options.feePayerKey !== void 0 ? "feePayerKey" : "MPP_FEE_PAYER_KEY";
3090
- const hasAnyMppEnv = Boolean(secretKey || currency || rpcUrl || options.require);
3091
- if (!hasAnyMppEnv) return void 0;
3092
- const missing = [
3093
- secretKey ? null : "MPP_SECRET_KEY",
3094
- currency ? null : "MPP_CURRENCY",
3095
- rpcUrl ? null : "TEMPO_RPC_URL"
3096
- ].filter(Boolean);
3097
- if (missing.length > 0) {
3098
- throw new Error(`MPP env is incomplete. Missing: ${missing.join(", ")}`);
3099
- }
3100
- if (!isEvmAddress(currency)) {
3101
- throw new Error("MPP_CURRENCY must be a 0x-prefixed 20-byte Tempo currency address");
3102
- }
3103
- if (options.recipient && !isEvmAddress(options.recipient)) {
3104
- throw new Error("MPP recipient must be a 0x-prefixed EVM address");
3105
- }
3106
- if (feePayerKey && !isEvmPrivateKey(feePayerKey)) {
3107
- throw new Error(`${feePayerKeySource} must be a 0x-prefixed 32-byte EVM private key`);
4073
+ });
4074
+ function collectKvWarnings(env, kvStoreOptionProvided) {
4075
+ if (kvStoreOptionProvided) return [];
4076
+ const warn = (code, message) => ({
4077
+ code,
4078
+ severity: "warning",
4079
+ message
4080
+ });
4081
+ if (env.KV_REST_API_URL && !env.KV_REST_API_TOKEN) {
4082
+ return [
4083
+ warn(
4084
+ "kv_url_without_token",
4085
+ "KV_REST_API_URL is set but KV_REST_API_TOKEN is missing \u2014 falling back to in-memory KV (unsafe in serverless production)."
4086
+ )
4087
+ ];
3108
4088
  }
3109
- return {
3110
- secretKey,
3111
- currency,
3112
- rpcUrl,
3113
- ...options.recipient ? { recipient: options.recipient } : {},
3114
- ...feePayerKey ? { feePayerKey } : {},
3115
- ...options.useDefaultStore !== void 0 ? { useDefaultStore: options.useDefaultStore } : {}
3116
- };
3117
- }
3118
- function x402AcceptsFromEnv(env, options = {}) {
3119
- const payeeEnv = options.payeeEnv ?? "X402_WALLET_ADDRESS";
3120
- const solanaPayeeEnv = options.solanaPayeeEnv ?? "SOLANA_PAYEE_ADDRESS";
3121
- const payeeAddress = options.payeeAddress ?? env[payeeEnv];
3122
- if (!payeeAddress) {
3123
- throw new Error(`${payeeEnv} is required to build x402 accepts`);
4089
+ if (env.KV_REST_API_TOKEN && !env.KV_REST_API_URL) {
4090
+ return [
4091
+ warn(
4092
+ "kv_token_without_url",
4093
+ "KV_REST_API_TOKEN is set but KV_REST_API_URL is missing \u2014 falling back to in-memory KV (unsafe in serverless production)."
4094
+ )
4095
+ ];
3124
4096
  }
3125
- const accepts = [
4097
+ if (env.KV_REST_API_URL && env.KV_REST_API_TOKEN && !isUrl(env.KV_REST_API_URL)) {
4098
+ return [
4099
+ warn(
4100
+ "invalid_kv_url",
4101
+ `KV_REST_API_URL is not a valid URL \u2014 KV calls will fail at request time. Got: ${env.KV_REST_API_URL}`
4102
+ )
4103
+ ];
4104
+ }
4105
+ if (!env.KV_REST_API_URL && !env.KV_REST_API_TOKEN && env.NODE_ENV === "production") {
4106
+ return [
4107
+ warn(
4108
+ "missing_kv_in_production",
4109
+ "No KV_REST_API_URL/KV_REST_API_TOKEN set in production \u2014 using the in-memory KV store. SIWX nonce, SIWX entitlement, and MPP replay state will be lost across instances. Configure Upstash/Vercel KV or pass a custom kvStore."
4110
+ )
4111
+ ];
4112
+ }
4113
+ return [];
4114
+ }
4115
+ function getConfiguredX402Accepts2(config) {
4116
+ if (config.x402?.accepts?.length) return [...config.x402.accepts];
4117
+ return [
3126
4118
  {
3127
4119
  scheme: "exact",
3128
- network: options.network ?? BASE_NETWORK,
3129
- payTo: payeeAddress
4120
+ network: config.network ?? BASE_MAINNET_NETWORK,
4121
+ payTo: config.payeeAddress
3130
4122
  }
3131
4123
  ];
3132
- const solanaPayeeAddress = options.solanaPayeeAddress ?? env[solanaPayeeEnv];
3133
- if (solanaPayeeAddress) {
3134
- accepts.push({
3135
- scheme: "exact",
3136
- network: SOLANA_MAINNET_NETWORK,
3137
- payTo: solanaPayeeAddress
3138
- });
3139
- }
3140
- return accepts;
3141
- }
3142
- function paidOptionsForProtocols(protocols) {
3143
- return { protocols: [...protocols] };
3144
4124
  }
3145
4125
  function validateX402Config(config, env, options) {
4126
+ const accepts = getConfiguredX402Accepts2(config);
3146
4127
  const issues = [];
3147
- const accepts = getConfiguredX402Accepts(config);
4128
+ const push = (code, message) => issues.push({ code, protocol: "x402", message });
3148
4129
  if (accepts.length === 0) {
3149
- issues.push({
3150
- code: "missing_x402_accepts",
3151
- protocol: "x402",
3152
- message: "x402 requires at least one accept configuration."
3153
- });
4130
+ push("missing_x402_accepts", "x402 requires at least one accept configuration.");
3154
4131
  return issues;
3155
4132
  }
3156
- const acceptWithoutNetwork = accepts.find((accept) => !accept.network);
3157
- if (acceptWithoutNetwork) {
3158
- issues.push({
3159
- code: "missing_x402_network",
3160
- protocol: "x402",
3161
- message: "x402 accepts require a network."
3162
- });
4133
+ if (accepts.some((a) => !a.network)) {
4134
+ push("missing_x402_network", "x402 accepts require a network.");
3163
4135
  }
3164
- const unsupported = accepts.find(
3165
- (accept) => accept.network && !isSupportedX402Network(accept.network)
3166
- );
4136
+ const unsupported = accepts.find((a) => a.network && !isX402Network(a.network));
3167
4137
  if (unsupported) {
3168
- issues.push({
3169
- code: "unsupported_x402_network",
3170
- protocol: "x402",
3171
- message: `unsupported x402 network '${unsupported.network}'. Use eip155:* or solana:*.`
3172
- });
4138
+ push(
4139
+ "unsupported_x402_network",
4140
+ `unsupported x402 network '${unsupported.network}'. Use eip155:* or solana:*.`
4141
+ );
3173
4142
  }
3174
- const missingAsset = accepts.find(
3175
- (accept) => (accept.scheme ?? "exact") !== "exact" && !accept.asset
3176
- );
3177
- if (missingAsset) {
3178
- issues.push({
3179
- code: "missing_x402_asset",
3180
- protocol: "x402",
3181
- message: "non-exact x402 accepts require an asset."
3182
- });
4143
+ if (accepts.some((a) => (a.scheme ?? "exact") !== "exact" && !a.asset)) {
4144
+ push("missing_x402_asset", "non-exact x402 accepts require an asset.");
3183
4145
  }
3184
- const invalidDecimals = accepts.find(
3185
- (accept) => accept.decimals !== void 0 && (!Number.isInteger(accept.decimals) || accept.decimals < 0)
3186
- );
3187
- if (invalidDecimals) {
3188
- issues.push({
3189
- code: "invalid_x402_decimals",
3190
- protocol: "x402",
3191
- message: "x402 accept decimals must be a non-negative integer."
3192
- });
4146
+ if (accepts.some(
4147
+ (a) => a.decimals !== void 0 && (!Number.isInteger(a.decimals) || a.decimals < 0)
4148
+ )) {
4149
+ push("invalid_x402_decimals", "x402 accept decimals must be a non-negative integer.");
3193
4150
  }
3194
- if (accepts.some((accept) => !accept.payTo) && !config.payeeAddress) {
3195
- issues.push({
3196
- code: "missing_x402_payee",
3197
- protocol: "x402",
3198
- message: "x402 requires payeeAddress in router config or payTo on every x402 accept."
3199
- });
4151
+ if (!config.payeeAddress && accepts.some((a) => !a.payTo)) {
4152
+ push(
4153
+ "missing_x402_payee",
4154
+ "x402 requires payeeAddress in router config or payTo on every x402 accept."
4155
+ );
3200
4156
  }
3201
- const placeholder = findPlaceholderPayee([
4157
+ const placeholder = [
3202
4158
  config.payeeAddress,
3203
- ...accepts.map((accept) => typeof accept.payTo === "string" ? accept.payTo : void 0)
3204
- ]);
4159
+ ...accepts.map((a) => typeof a.payTo === "string" ? a.payTo : void 0)
4160
+ ].find((v) => v !== void 0 && isPlaceholderEvm(v));
3205
4161
  if (placeholder) {
3206
- issues.push({
3207
- code: "placeholder_payee",
3208
- protocol: "x402",
3209
- message: `x402 payee '${placeholder}' is a placeholder address and cannot receive payments.`
3210
- });
4162
+ push(
4163
+ "placeholder_payee",
4164
+ `x402 payee '${placeholder}' is a placeholder address and cannot receive payments.`
4165
+ );
3211
4166
  }
3212
- if (options.requireCdpKeys !== false && usesDefaultEvmFacilitator(config)) {
3213
- const missing = [
3214
- env.CDP_API_KEY_ID ? null : "CDP_API_KEY_ID",
3215
- env.CDP_API_KEY_SECRET ? null : "CDP_API_KEY_SECRET"
3216
- ].filter(Boolean);
3217
- if (missing.length > 0) {
3218
- issues.push({
3219
- code: "missing_cdp_keys",
3220
- protocol: "x402",
3221
- message: `default EVM x402 facilitator requires ${missing.join(" and ")}.`
3222
- });
4167
+ if (options.requireCdpKeys !== false) {
4168
+ const hasEvm = accepts.some(
4169
+ (a) => typeof a.network === "string" && a.network.startsWith("eip155:")
4170
+ );
4171
+ if (hasEvm) {
4172
+ const missing = ["CDP_API_KEY_ID", "CDP_API_KEY_SECRET"].filter((k) => !env[k]);
4173
+ if (missing.length > 0) {
4174
+ push(
4175
+ "missing_cdp_keys",
4176
+ `x402 EVM facilitator (Coinbase) requires ${missing.join(" and ")}.`
4177
+ );
4178
+ }
3223
4179
  }
3224
4180
  }
3225
4181
  return issues;
3226
4182
  }
3227
- function validateMppConfig(config, env) {
3228
- const issues = [];
3229
- const mpp = config.mpp;
3230
- if (!mpp) {
4183
+ function validateMppConfig(config) {
4184
+ const m = config.mpp;
4185
+ if (!m) {
3231
4186
  return [
3232
4187
  {
3233
4188
  code: "missing_mpp_config",
@@ -3236,98 +4191,343 @@ function validateMppConfig(config, env) {
3236
4191
  }
3237
4192
  ];
3238
4193
  }
3239
- if (!mpp.secretKey) {
3240
- issues.push({
3241
- code: "missing_mpp_secret_key",
3242
- protocol: "mpp",
3243
- message: "MPP requires secretKey. Set MPP_SECRET_KEY or pass mpp.secretKey."
3244
- });
4194
+ const issues = [];
4195
+ const push = (code, message) => issues.push({ code, protocol: "mpp", message });
4196
+ if (!m.secretKey) {
4197
+ push(
4198
+ "missing_mpp_secret_key",
4199
+ "MPP requires secretKey. Set MPP_SECRET_KEY or pass mpp.secretKey."
4200
+ );
3245
4201
  }
3246
- if (!mpp.currency) {
3247
- issues.push({
3248
- code: "missing_mpp_currency",
3249
- protocol: "mpp",
3250
- message: "MPP requires currency. Set MPP_CURRENCY or pass mpp.currency."
3251
- });
3252
- } else if (!isEvmAddress(mpp.currency)) {
3253
- issues.push({
3254
- code: "invalid_mpp_currency",
3255
- protocol: "mpp",
3256
- message: "MPP currency must be a 0x-prefixed 20-byte Tempo currency address. Use TEMPO_USDC_CURRENCY for Tempo USDC."
4202
+ if (!m.currency) {
4203
+ push("missing_mpp_currency", "MPP requires currency. Set MPP_CURRENCY or pass mpp.currency.");
4204
+ } else if (!isEvmAddress(m.currency)) {
4205
+ push(
4206
+ "invalid_mpp_currency",
4207
+ "MPP currency must be a 0x-prefixed 20-byte Tempo currency address. Use TEMPO_USDC_ADDRESS for Tempo USDC."
4208
+ );
4209
+ }
4210
+ const recipient = m.recipient ?? config.payeeAddress;
4211
+ if (!recipient) {
4212
+ push(
4213
+ "missing_mpp_recipient",
4214
+ "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config."
4215
+ );
4216
+ } else if (!isEvmAddress(recipient)) {
4217
+ push(
4218
+ "invalid_mpp_recipient",
4219
+ "MPP recipient must be a 0x-prefixed EVM address. Solana recipients require x402."
4220
+ );
4221
+ }
4222
+ const placeholder = [m.recipient, config.payeeAddress].find(
4223
+ (v) => typeof v === "string" && isPlaceholderEvm(v)
4224
+ );
4225
+ if (placeholder) {
4226
+ push(
4227
+ "placeholder_payee",
4228
+ `MPP recipient '${placeholder}' is a placeholder address and cannot receive payments.`
4229
+ );
4230
+ }
4231
+ if (!m.rpcUrl) {
4232
+ push(
4233
+ "missing_mpp_rpc_url",
4234
+ "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object."
4235
+ );
4236
+ }
4237
+ if (m.feePayerKey && !isEvmPrivateKey(m.feePayerKey)) {
4238
+ push(
4239
+ "invalid_mpp_fee_payer_key",
4240
+ "MPP feePayerKey must be a 0x-prefixed 32-byte EVM private key."
4241
+ );
4242
+ }
4243
+ if (m.operatorKey && !isEvmPrivateKey(m.operatorKey)) {
4244
+ push(
4245
+ "invalid_mpp_operator_key",
4246
+ "MPP operatorKey must be a 0x-prefixed 32-byte EVM private key."
4247
+ );
4248
+ }
4249
+ const collision = operatorAddressesCollide(m.operatorKey, m.feePayerKey);
4250
+ if (collision) {
4251
+ push(
4252
+ "mpp_operator_equals_fee_payer",
4253
+ `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).`
4254
+ );
4255
+ }
4256
+ return issues;
4257
+ }
4258
+ function translateZodIssues(error) {
4259
+ return error.issues.map((issue) => {
4260
+ const params = issue.params;
4261
+ if (!params?.code) {
4262
+ throw new Error(
4263
+ `[router] schema issue missing params.code (path=${issue.path.join(".")}, message=${issue.message}). Every refinement / addIssue call must set params.code.`
4264
+ );
4265
+ }
4266
+ return {
4267
+ code: params.code,
4268
+ message: issue.message,
4269
+ ...params.protocol ? { protocol: params.protocol } : {},
4270
+ ...params.severity ? { severity: params.severity } : {}
4271
+ };
4272
+ });
4273
+ }
4274
+ function routerConfigFromEnv(options) {
4275
+ const rawEnv = options.env ?? process.env;
4276
+ const env = trimAll(rawEnv);
4277
+ const optionIssues = [];
4278
+ if (!options.title?.trim()) {
4279
+ optionIssues.push({
4280
+ code: "missing_discovery_title",
4281
+ message: "discovery `title` is required. Pass a short product name."
3257
4282
  });
3258
4283
  }
3259
- const mppRecipient = mpp.recipient ?? config.payeeAddress;
3260
- if (!mppRecipient) {
3261
- issues.push({
3262
- code: "missing_mpp_recipient",
3263
- protocol: "mpp",
3264
- message: "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config."
4284
+ if (!options.description?.trim()) {
4285
+ optionIssues.push({
4286
+ code: "missing_discovery_description",
4287
+ message: "discovery `description` is required. One sentence is enough."
3265
4288
  });
3266
- } else if (!isEvmAddress(mppRecipient)) {
3267
- issues.push({
3268
- code: "invalid_mpp_recipient",
3269
- protocol: "mpp",
3270
- message: "MPP recipient must be a 0x-prefixed EVM address. Solana recipients require x402."
4289
+ }
4290
+ if (options.guidance === void 0) {
4291
+ optionIssues.push({
4292
+ code: "missing_discovery_guidance",
4293
+ message: "discovery `guidance` is required. Provide an empty string to opt out of `/llms.txt`."
3271
4294
  });
3272
4295
  }
3273
- const placeholder = findPlaceholderPayee([mpp.recipient, config.payeeAddress]);
3274
- if (placeholder) {
3275
- issues.push({
3276
- code: "placeholder_payee",
3277
- protocol: "mpp",
3278
- message: `MPP recipient '${placeholder}' is a placeholder address and cannot receive payments.`
4296
+ if (options.serverUrl !== void 0 && !isUrl(options.serverUrl)) {
4297
+ optionIssues.push({
4298
+ code: "invalid_server_url",
4299
+ message: `discovery \`serverUrl\` must be a valid URL. Got: ${options.serverUrl}`
3279
4300
  });
3280
4301
  }
3281
- if (!(mpp.rpcUrl ?? env.TEMPO_RPC_URL)) {
3282
- issues.push({
3283
- code: "missing_mpp_rpc_url",
3284
- protocol: "mpp",
3285
- message: "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object."
4302
+ const parsed = EnvInputSchema.safeParse(env);
4303
+ const envIssues = parsed.success ? [] : translateZodIssues(parsed.error);
4304
+ const issues = [...envIssues, ...optionIssues];
4305
+ if (issues.length > 0) throw new RouterConfigError(issues);
4306
+ for (const warning of collectKvWarnings(env, options.kvStore !== void 0)) {
4307
+ console.warn(`[router] ${warning.message}`);
4308
+ }
4309
+ const payeeAddress = canonicalizeEvm(env.EVM_PAYEE_ADDRESS);
4310
+ const accepts = [
4311
+ { scheme: "exact", network: BASE_MAINNET_NETWORK, payTo: payeeAddress },
4312
+ {
4313
+ scheme: "upto",
4314
+ network: BASE_MAINNET_NETWORK,
4315
+ payTo: payeeAddress,
4316
+ asset: BASE_USDC_ADDRESS,
4317
+ decimals: BASE_USDC_DECIMALS
4318
+ }
4319
+ ];
4320
+ if (env.SOLANA_PAYEE_ADDRESS) {
4321
+ accepts.push({
4322
+ scheme: "exact",
4323
+ network: SOLANA_MAINNET_NETWORK,
4324
+ payTo: env.SOLANA_PAYEE_ADDRESS
3286
4325
  });
3287
4326
  }
3288
- if (mpp.feePayerKey && !isEvmPrivateKey(mpp.feePayerKey)) {
4327
+ const configuredSolanaFacilitator = options.x402Facilitators?.solana;
4328
+ const solanaFacilitator = typeof configuredSolanaFacilitator === "string" ? configuredSolanaFacilitator : configuredSolanaFacilitator ?? env.SOLANA_FACILITATOR_URL ?? DEFAULT_SOLANA_FACILITATOR_URL;
4329
+ const mppEnabled = options.protocols?.includes("mpp") ?? Boolean(env.MPP_SECRET_KEY);
4330
+ const protocols = options.protocols ? [...options.protocols] : mppEnabled ? ["x402", "mpp"] : ["x402"];
4331
+ const mppConfig = mppEnabled ? {
4332
+ secretKey: env.MPP_SECRET_KEY,
4333
+ currency: canonicalizeEvm(env.MPP_CURRENCY),
4334
+ rpcUrl: env.TEMPO_RPC_URL,
4335
+ recipient: payeeAddress,
4336
+ ...env.MPP_FEE_PAYER_KEY ? { feePayerKey: env.MPP_FEE_PAYER_KEY } : {},
4337
+ ...env.MPP_OPERATOR_KEY ? { operatorKey: env.MPP_OPERATOR_KEY, session: {} } : {}
4338
+ } : void 0;
4339
+ return {
4340
+ payeeAddress,
4341
+ baseUrl: env.BASE_URL,
4342
+ network: BASE_MAINNET_NETWORK,
4343
+ protocols,
4344
+ x402: {
4345
+ accepts,
4346
+ facilitators: {
4347
+ ...options.x402Facilitators,
4348
+ solana: solanaFacilitator
4349
+ }
4350
+ },
4351
+ ...mppConfig ? { mpp: mppConfig } : {},
4352
+ discovery: {
4353
+ title: options.title,
4354
+ version: options.version ?? "1.0.0",
4355
+ description: options.description,
4356
+ guidance: options.guidance,
4357
+ ...options.contact ? { contact: options.contact } : {},
4358
+ ...options.ownershipProofs ? { ownershipProofs: options.ownershipProofs } : {},
4359
+ ...options.methodHints ? { methodHints: options.methodHints } : {},
4360
+ ...options.serverUrl ? { serverUrl: options.serverUrl } : {}
4361
+ },
4362
+ ...options.prices ? { prices: options.prices } : {},
4363
+ ...options.plugin ? { plugin: options.plugin } : {},
4364
+ ...options.kvStore ? { kvStore: options.kvStore } : {},
4365
+ strictRoutes: options.strictRoutes ?? false
4366
+ };
4367
+ }
4368
+ function getRouterConfigIssues(config, options = {}) {
4369
+ const env = options.env ?? {};
4370
+ const protocols = config.protocols ?? ["x402"];
4371
+ const issues = [];
4372
+ if (!config.baseUrl) {
3289
4373
  issues.push({
3290
- code: "invalid_mpp_fee_payer_key",
3291
- protocol: "mpp",
3292
- message: "MPP feePayerKey must be a 0x-prefixed 32-byte EVM private key."
4374
+ code: "missing_base_url",
4375
+ message: '[router] baseUrl is required in RouterConfig. Set it to your production domain (e.g., "https://api.example.com"). The realm is used for payment matching and must be correct.'
3293
4376
  });
3294
4377
  }
3295
- if (mpp.useDefaultStore && !mpp.store && (!env.KV_REST_API_URL || !env.KV_REST_API_TOKEN)) {
4378
+ if (config.protocols && config.protocols.length === 0) {
3296
4379
  issues.push({
3297
- code: "missing_mpp_default_store_env",
3298
- protocol: "mpp",
3299
- message: "mpp.useDefaultStore requires KV_REST_API_URL and KV_REST_API_TOKEN environment variables. These are automatically set by Vercel KV."
4380
+ code: "empty_protocols",
4381
+ message: "RouterConfig.protocols cannot be empty. Omit the field to use default ['x402'] or specify protocols explicitly."
3300
4382
  });
3301
4383
  }
4384
+ if (protocols.includes("x402")) issues.push(...validateX402Config(config, env, options));
4385
+ if (protocols.includes("mpp")) issues.push(...validateMppConfig(config));
3302
4386
  return issues;
3303
4387
  }
3304
- function usesDefaultEvmFacilitator(config) {
3305
- return getConfiguredX402Networks(config).some(
3306
- (network) => typeof network === "string" && isEvmNetwork(network)
3307
- ) && config.x402?.facilitators?.evm === void 0;
3308
- }
3309
- function isSupportedX402Network(network) {
3310
- return isEvmNetwork(network) || isSolanaNetwork(network);
4388
+
4389
+ // src/init/x402.ts
4390
+ async function initX402(config, configError) {
4391
+ if (configError) return { initError: configError };
4392
+ try {
4393
+ const { createX402Server: createX402Server2 } = await Promise.resolve().then(() => (init_x402_server(), x402_server_exports));
4394
+ const result = await createX402Server2(config);
4395
+ await result.initPromise;
4396
+ return {
4397
+ server: result.server,
4398
+ facilitatorsByNetwork: result.facilitatorsByNetwork
4399
+ };
4400
+ } catch (err) {
4401
+ return { initError: err instanceof Error ? err.message : String(err) };
4402
+ }
3311
4403
  }
3312
- function isEvmAddress(value) {
3313
- return /^0x[a-fA-F0-9]{40}$/.test(value);
4404
+
4405
+ // src/init/mppx.ts
4406
+ function getMppxRequestContext(args) {
4407
+ const {
4408
+ Mppx,
4409
+ tempo,
4410
+ mppConfig,
4411
+ payeeAddress,
4412
+ getClient,
4413
+ feePayerAccount,
4414
+ resolvedStore,
4415
+ sessionEnabled,
4416
+ sharedSessionParams,
4417
+ realm
4418
+ } = args;
4419
+ const instance = Mppx.create({
4420
+ methods: [
4421
+ tempo.charge({
4422
+ currency: mppConfig.currency,
4423
+ recipient: mppConfig.recipient ?? payeeAddress,
4424
+ getClient,
4425
+ ...feePayerAccount ? { feePayer: feePayerAccount } : {},
4426
+ ...resolvedStore ? { store: resolvedStore } : {}
4427
+ }),
4428
+ ...sessionEnabled ? [
4429
+ tempo.session({
4430
+ ...sharedSessionParams,
4431
+ sse: false
4432
+ })
4433
+ ] : []
4434
+ ],
4435
+ secretKey: mppConfig.secretKey,
4436
+ realm
4437
+ });
4438
+ return instance;
4439
+ }
4440
+ function getMppxStreamingContext(args) {
4441
+ if (!args.sessionEnabled) return null;
4442
+ const { Mppx, tempo, mppConfig, sharedSessionParams, realm } = args;
4443
+ const instance = Mppx.create({
4444
+ methods: [
4445
+ tempo.session({
4446
+ ...sharedSessionParams,
4447
+ sse: true
4448
+ })
4449
+ ],
4450
+ secretKey: mppConfig.secretKey,
4451
+ realm
4452
+ });
4453
+ return instance;
3314
4454
  }
3315
- function isEvmPrivateKey(value) {
3316
- return /^0x[a-fA-F0-9]{64}$/.test(value);
4455
+
4456
+ // src/init/mpp.ts
4457
+ async function initMpp(config, resolvedBaseUrl, kvStore, configError) {
4458
+ if (configError) return { initError: configError };
4459
+ if (!config.mpp) return {};
4460
+ try {
4461
+ const { Mppx, tempo } = await import("mppx/server");
4462
+ const { createClient, http } = await import("viem");
4463
+ const { tempo: tempoChain } = await import("viem/chains");
4464
+ const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
4465
+ const rpcUrl = config.mpp.rpcUrl ?? process.env.TEMPO_RPC_URL;
4466
+ const tempoClient = createClient({ chain: tempoChain, transport: http(rpcUrl) });
4467
+ const getClient = async () => tempoClient;
4468
+ const operatorAccount = config.mpp.operatorKey ? privateKeyToAccount2(config.mpp.operatorKey) : void 0;
4469
+ const feePayerAccount = config.mpp.feePayerKey ? privateKeyToAccount2(config.mpp.feePayerKey) : void 0;
4470
+ if (config.mpp.session && operatorAccount) {
4471
+ assertOperatorMatchesRecipient(config, operatorAccount.address);
4472
+ }
4473
+ const resolvedStore = kvStore ? await createKvMppStore(kvStore) : void 0;
4474
+ const realm = new URL(resolvedBaseUrl).host;
4475
+ const mppConfig = config.mpp;
4476
+ const sessionEnabled = !!(mppConfig.session && operatorAccount);
4477
+ const sharedSessionParams = {
4478
+ currency: mppConfig.currency,
4479
+ decimals: 6,
4480
+ recipient: mppConfig.recipient ?? config.payeeAddress,
4481
+ getClient,
4482
+ ...operatorAccount ? { account: operatorAccount } : {},
4483
+ ...feePayerAccount ? { feePayer: feePayerAccount } : {},
4484
+ ...resolvedStore ? { store: resolvedStore } : {}
4485
+ };
4486
+ const mppxArgs = {
4487
+ Mppx,
4488
+ tempo,
4489
+ mppConfig,
4490
+ payeeAddress: config.payeeAddress ?? "",
4491
+ getClient,
4492
+ feePayerAccount,
4493
+ resolvedStore,
4494
+ sessionEnabled,
4495
+ sharedSessionParams,
4496
+ realm
4497
+ };
4498
+ const primary = getMppxRequestContext(mppxArgs);
4499
+ const streaming = getMppxStreamingContext(mppxArgs);
4500
+ const mppx = {
4501
+ charge: primary.charge,
4502
+ ...primary.session ? { sessionRequest: primary.session } : {},
4503
+ ...streaming?.session ? { sessionStream: streaming.session } : {}
4504
+ };
4505
+ return { mppx, tempoClient };
4506
+ } catch (err) {
4507
+ return { initError: err instanceof Error ? err.message : String(err) };
4508
+ }
3317
4509
  }
3318
- function findPlaceholderPayee(values) {
3319
- return values.find((value) => value !== void 0 && /^0x0{40}$/i.test(value)) ?? null;
4510
+ function assertOperatorMatchesRecipient(config, operatorAddress) {
4511
+ const recipient = (config.mpp?.recipient ?? config.payeeAddress)?.toLowerCase();
4512
+ const opAddr = operatorAddress.toLowerCase();
4513
+ if (recipient && opAddr !== recipient) {
4514
+ throw new Error(
4515
+ `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}.`
4516
+ );
4517
+ }
3320
4518
  }
3321
4519
 
3322
4520
  // src/index.ts
3323
4521
  init_constants();
3324
4522
  function createRouter(config) {
3325
4523
  const registry = new RouteRegistry();
3326
- const nonceStore = config.siwx?.nonceStore ?? new MemoryNonceStore();
3327
- const entitlementStore = config.siwx?.entitlementStore ?? new MemoryEntitlementStore();
3328
- const network = config.network ?? BASE_NETWORK;
4524
+ const kvStore = resolveKvStore(config.kvStore);
4525
+ const nonceStore = kvStore ? createKvNonceStore(kvStore) : new MemoryNonceStore();
4526
+ const entitlementStore = kvStore ? createKvEntitlementStore(kvStore) : new MemoryEntitlementStore();
4527
+ const network = config.network ?? BASE_MAINNET_NETWORK;
3329
4528
  const x402Accepts = getConfiguredX402Accepts(config);
3330
4529
  const configIssues = getRouterConfigIssues(config, {
4530
+ env: process.env,
3331
4531
  requireCdpKeys: process.env.NODE_ENV === "production"
3332
4532
  });
3333
4533
  const baseUrlIssue = configIssues.find((issue) => issue.code === "missing_base_url");
@@ -3370,69 +4570,20 @@ function createRouter(config) {
3370
4570
  x402FacilitatorsByNetwork: void 0,
3371
4571
  x402Accepts,
3372
4572
  mppx: null,
3373
- tempoClient: null
4573
+ tempoClient: null,
4574
+ mppSessionConfig: config.mpp?.session ? { depositMultiplier: config.mpp.session.depositMultiplier ?? 10 } : null
3374
4575
  };
3375
4576
  deps.initPromise = (async () => {
3376
- if (x402ConfigError) {
3377
- deps.x402InitError = x402ConfigError;
3378
- } else {
3379
- try {
3380
- const { createX402Server: createX402Server2 } = await Promise.resolve().then(() => (init_server(), server_exports));
3381
- const result = await createX402Server2(config);
3382
- deps.x402Server = result.server;
3383
- deps.x402FacilitatorsByNetwork = result.facilitatorsByNetwork;
3384
- await result.initPromise;
3385
- } catch (err) {
3386
- deps.x402Server = null;
3387
- deps.x402InitError = err instanceof Error ? err.message : String(err);
3388
- }
3389
- }
3390
- if (mppConfigError) {
3391
- deps.mppInitError = mppConfigError;
3392
- } else if (config.mpp) {
3393
- try {
3394
- const { Mppx, tempo } = await import("mppx/server");
3395
- const rpcUrl = config.mpp.rpcUrl ?? process.env.TEMPO_RPC_URL;
3396
- const { createClient, http } = await import("viem");
3397
- const { tempo: tempoChain } = await import("viem/chains");
3398
- deps.tempoClient = createClient({ chain: tempoChain, transport: http(rpcUrl) });
3399
- const getClient = async () => deps.tempoClient;
3400
- let feePayerAccount;
3401
- if (config.mpp.feePayerKey) {
3402
- const { privateKeyToAccount } = await import("viem/accounts");
3403
- feePayerAccount = privateKeyToAccount(config.mpp.feePayerKey);
3404
- }
3405
- let resolvedStore = config.mpp.store;
3406
- if (!resolvedStore && config.mpp.useDefaultStore) {
3407
- const kvUrl = process.env.KV_REST_API_URL;
3408
- const kvToken = process.env.KV_REST_API_TOKEN;
3409
- if (!kvUrl || !kvToken) {
3410
- throw new Error(
3411
- "mpp.useDefaultStore requires KV_REST_API_URL and KV_REST_API_TOKEN environment variables. These are automatically set by Vercel KV."
3412
- );
3413
- }
3414
- const { Store } = await import("mppx");
3415
- const { createUpstashRest: createUpstashRest2 } = await Promise.resolve().then(() => (init_upstash_rest(), upstash_rest_exports));
3416
- resolvedStore = Store.upstash(createUpstashRest2(kvUrl, kvToken));
3417
- }
3418
- deps.mppx = Mppx.create({
3419
- methods: [
3420
- tempo.charge({
3421
- currency: config.mpp.currency,
3422
- recipient: config.mpp.recipient ?? config.payeeAddress,
3423
- getClient,
3424
- ...feePayerAccount ? { feePayer: feePayerAccount } : {},
3425
- ...resolvedStore ? { store: resolvedStore } : {}
3426
- })
3427
- ],
3428
- secretKey: config.mpp.secretKey,
3429
- realm: new URL(resolvedBaseUrl).host
3430
- });
3431
- } catch (err) {
3432
- deps.mppx = null;
3433
- deps.mppInitError = err instanceof Error ? err.message : String(err);
3434
- console.error(`[router] MPP initialization failed: ${deps.mppInitError}`);
3435
- }
4577
+ const x402Result = await initX402(config, x402ConfigError);
4578
+ deps.x402Server = x402Result.server ?? null;
4579
+ deps.x402FacilitatorsByNetwork = x402Result.facilitatorsByNetwork;
4580
+ if (x402Result.initError) deps.x402InitError = x402Result.initError;
4581
+ const mppResult = await initMpp(config, resolvedBaseUrl, kvStore, mppConfigError);
4582
+ deps.mppx = mppResult.mppx ?? null;
4583
+ deps.tempoClient = mppResult.tempoClient ?? null;
4584
+ if (mppResult.initError) {
4585
+ deps.mppInitError = mppResult.initError;
4586
+ console.error(`[router] MPP initialization failed: ${mppResult.initError}`);
3436
4587
  }
3437
4588
  })();
3438
4589
  const pricesKeys = config.prices ? Object.keys(config.prices) : void 0;
@@ -3499,27 +4650,21 @@ function normalizePath(path) {
3499
4650
  normalized = normalized.replace(/^api\/+/, "");
3500
4651
  return normalized.replace(/\/+$/, "");
3501
4652
  }
4653
+ function createRouterFromEnv(options) {
4654
+ return createRouter(routerConfigFromEnv(options));
4655
+ }
3502
4656
  export {
3503
- BASE_NETWORK,
4657
+ BASE_MAINNET_NETWORK,
4658
+ BASE_USDC_ADDRESS,
4659
+ BASE_USDC_DECIMALS,
4660
+ DEFAULT_SOLANA_FACILITATOR_URL,
3504
4661
  HttpError,
3505
- MemoryEntitlementStore,
3506
- MemoryNonceStore,
3507
- RouteBuilder,
3508
- RouteRegistry,
3509
4662
  RouterConfigError,
3510
- SIWX_CHALLENGE_EXPIRY_MS,
3511
- SIWX_ERROR_MESSAGES,
3512
4663
  SOLANA_MAINNET_NETWORK,
3513
- TEMPO_USDC_CURRENCY,
4664
+ TEMPO_USDC_ADDRESS,
4665
+ TEMPO_USDC_DECIMALS,
3514
4666
  ZERO_EVM_ADDRESS,
3515
- consolePlugin,
3516
- createRedisEntitlementStore,
3517
- createRedisNonceStore,
3518
4667
  createRouter,
3519
- formatRouterConfigIssues,
3520
- getRouterConfigIssues,
3521
- mppFromEnv,
3522
- paidOptionsForProtocols,
3523
- validateRouterConfig,
3524
- x402AcceptsFromEnv
4668
+ createRouterFromEnv,
4669
+ routerConfigFromEnv
3525
4670
  };