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