@agentcash/router 1.5.1 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8,7 +8,65 @@ var __export = (target, all) => {
8
8
  __defProp(target, name, { get: all[name], enumerable: true });
9
9
  };
10
10
 
11
- // src/protocols/evm.ts
11
+ // src/constants.ts
12
+ var BASE_NETWORK, SOLANA_MAINNET_NETWORK, TEMPO_USDC_CURRENCY, ZERO_EVM_ADDRESS;
13
+ var init_constants = __esm({
14
+ "src/constants.ts"() {
15
+ "use strict";
16
+ BASE_NETWORK = "eip155:8453";
17
+ SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
18
+ TEMPO_USDC_CURRENCY = "0x20c000000000000000000000b9537d11c60e8b50";
19
+ ZERO_EVM_ADDRESS = "0x0000000000000000000000000000000000000000";
20
+ }
21
+ });
22
+
23
+ // src/protocols/x402/accepts.ts
24
+ async function resolvePayToValue(payTo, request, fallback, body) {
25
+ if (!payTo) return fallback;
26
+ if (typeof payTo === "string") return payTo;
27
+ return payTo(request, body);
28
+ }
29
+ function getConfiguredX402Accepts(config) {
30
+ if (config.x402?.accepts?.length) {
31
+ return [...config.x402.accepts];
32
+ }
33
+ return [
34
+ {
35
+ scheme: "exact",
36
+ network: config.network ?? BASE_NETWORK,
37
+ payTo: config.payeeAddress
38
+ }
39
+ ];
40
+ }
41
+ function getConfiguredX402Networks(config) {
42
+ return [...new Set(getConfiguredX402Accepts(config).map((accept) => accept.network))];
43
+ }
44
+ async function resolveX402Accepts(request, routeEntry, accepts, fallbackPayTo, body) {
45
+ return Promise.all(
46
+ accepts.map(async (accept) => ({
47
+ network: accept.network,
48
+ scheme: accept.scheme ?? "exact",
49
+ payTo: await resolvePayToValue(
50
+ routeEntry.payTo ?? accept.payTo,
51
+ request,
52
+ fallbackPayTo,
53
+ body
54
+ ),
55
+ ...accept.asset ? { asset: accept.asset } : {},
56
+ ...accept.decimals !== void 0 ? { decimals: accept.decimals } : {},
57
+ ...accept.maxTimeoutSeconds !== void 0 ? { maxTimeoutSeconds: accept.maxTimeoutSeconds } : {},
58
+ ...accept.extra ? { extra: accept.extra } : {}
59
+ }))
60
+ );
61
+ }
62
+ var init_accepts = __esm({
63
+ "src/protocols/x402/accepts.ts"() {
64
+ "use strict";
65
+ init_constants();
66
+ }
67
+ });
68
+
69
+ // src/protocols/x402/evm.ts
12
70
  function isEvmNetwork(network) {
13
71
  return network.startsWith("eip155:");
14
72
  }
@@ -26,12 +84,12 @@ function buildEvmExactOptions(accepts, price) {
26
84
  }));
27
85
  }
28
86
  var init_evm = __esm({
29
- "src/protocols/evm.ts"() {
87
+ "src/protocols/x402/evm.ts"() {
30
88
  "use strict";
31
89
  }
32
90
  });
33
91
 
34
- // src/protocols/solana.ts
92
+ // src/protocols/x402/solana.ts
35
93
  function isSolanaNetwork(network) {
36
94
  return network.startsWith("solana:");
37
95
  }
@@ -81,13 +139,13 @@ function isSolanaRequirement(requirement) {
81
139
  return isSolanaNetwork(requirement.network);
82
140
  }
83
141
  var init_solana = __esm({
84
- "src/protocols/solana.ts"() {
142
+ "src/protocols/x402/solana.ts"() {
85
143
  "use strict";
86
- init_x402_facilitators();
144
+ init_facilitators();
87
145
  }
88
146
  });
89
147
 
90
- // src/x402-facilitators.ts
148
+ // src/protocols/x402/facilitators.ts
91
149
  function getResolvedX402Facilitator(config, network, defaultEvmFacilitator) {
92
150
  const family = getNetworkFamily(network);
93
151
  if (!family) return null;
@@ -157,8 +215,8 @@ function sameFacilitatorConfig(a, b) {
157
215
  return a.url === b.url && a.createAuthHeaders === b.createAuthHeaders && a.createAcceptsHeaders === b.createAcceptsHeaders;
158
216
  }
159
217
  var DEFAULT_SOLANA_FACILITATOR_URL;
160
- var init_x402_facilitators = __esm({
161
- "src/x402-facilitators.ts"() {
218
+ var init_facilitators = __esm({
219
+ "src/protocols/x402/facilitators.ts"() {
162
220
  "use strict";
163
221
  init_evm();
164
222
  init_solana();
@@ -166,64 +224,6 @@ var init_x402_facilitators = __esm({
166
224
  }
167
225
  });
168
226
 
169
- // src/constants.ts
170
- var BASE_NETWORK, SOLANA_MAINNET_NETWORK, TEMPO_USDC_CURRENCY, ZERO_EVM_ADDRESS;
171
- var init_constants = __esm({
172
- "src/constants.ts"() {
173
- "use strict";
174
- BASE_NETWORK = "eip155:8453";
175
- SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
176
- TEMPO_USDC_CURRENCY = "0x20c000000000000000000000b9537d11c60e8b50";
177
- ZERO_EVM_ADDRESS = "0x0000000000000000000000000000000000000000";
178
- }
179
- });
180
-
181
- // src/x402-config.ts
182
- async function resolvePayToValue(payTo, request, fallback, body) {
183
- if (!payTo) return fallback;
184
- if (typeof payTo === "string") return payTo;
185
- return payTo(request, body);
186
- }
187
- function getConfiguredX402Accepts(config) {
188
- if (config.x402?.accepts?.length) {
189
- return [...config.x402.accepts];
190
- }
191
- return [
192
- {
193
- scheme: "exact",
194
- network: config.network ?? BASE_NETWORK,
195
- payTo: config.payeeAddress
196
- }
197
- ];
198
- }
199
- function getConfiguredX402Networks(config) {
200
- return [...new Set(getConfiguredX402Accepts(config).map((accept) => accept.network))];
201
- }
202
- async function resolveX402Accepts(request, routeEntry, accepts, fallbackPayTo, body) {
203
- return Promise.all(
204
- accepts.map(async (accept) => ({
205
- network: accept.network,
206
- scheme: accept.scheme ?? "exact",
207
- payTo: await resolvePayToValue(
208
- routeEntry.payTo ?? accept.payTo,
209
- request,
210
- fallbackPayTo,
211
- body
212
- ),
213
- ...accept.asset ? { asset: accept.asset } : {},
214
- ...accept.decimals !== void 0 ? { decimals: accept.decimals } : {},
215
- ...accept.maxTimeoutSeconds !== void 0 ? { maxTimeoutSeconds: accept.maxTimeoutSeconds } : {},
216
- ...accept.extra ? { extra: accept.extra } : {}
217
- }))
218
- );
219
- }
220
- var init_x402_config = __esm({
221
- "src/x402-config.ts"() {
222
- "use strict";
223
- init_constants();
224
- }
225
- });
226
-
227
227
  // src/server.ts
228
228
  var server_exports = {};
229
229
  __export(server_exports, {
@@ -308,8 +308,8 @@ var init_server = __esm({
308
308
  "use strict";
309
309
  init_evm();
310
310
  init_solana();
311
- init_x402_facilitators();
312
- init_x402_config();
311
+ init_facilitators();
312
+ init_accepts();
313
313
  }
314
314
  });
315
315
 
@@ -415,13 +415,32 @@ var RouteRegistry = class {
415
415
  }
416
416
  };
417
417
 
418
- // src/orchestrate.ts
419
- import { NextResponse as NextResponse2 } from "next/server";
420
-
421
- // src/auth/normalize-wallet.ts
422
- function normalizeWalletAddress(address) {
423
- return /^0x/i.test(address) ? address.toLowerCase() : address;
424
- }
418
+ // src/headers.ts
419
+ var HEADERS = {
420
+ // ---- Standard HTTP ----
421
+ AUTHORIZATION: "Authorization",
422
+ WWW_AUTHENTICATE: "WWW-Authenticate",
423
+ // ---- Auth ----
424
+ API_KEY: "X-API-Key",
425
+ // ---- Request meta (used by plugin/observability) ----
426
+ WALLET_ADDRESS: "X-Wallet-Address",
427
+ CLIENT_ID: "X-Client-ID",
428
+ SESSION_ID: "X-Session-ID",
429
+ // ---- SIWX ----
430
+ SIWX: "SIGN-IN-WITH-X",
431
+ // ---- x402 (payment) ----
432
+ X402_PAYMENT_SIGNATURE: "PAYMENT-SIGNATURE",
433
+ /** Legacy x402 payment header — accepted alongside PAYMENT-SIGNATURE. */
434
+ X402_PAYMENT_LEGACY: "X-PAYMENT",
435
+ X402_PAYMENT_REQUIRED: "PAYMENT-REQUIRED",
436
+ X402_PAYMENT_RESPONSE: "PAYMENT-RESPONSE",
437
+ // ---- MPP (payment) ----
438
+ MPP_PAYMENT_RECEIPT: "Payment-Receipt"
439
+ };
440
+ var AUTH_SCHEME = {
441
+ BEARER: "Bearer ",
442
+ MPP_PAYMENT: "Payment "
443
+ };
425
444
 
426
445
  // src/plugin.ts
427
446
  function createDefaultContext(meta) {
@@ -498,106 +517,31 @@ function consolePlugin() {
498
517
  };
499
518
  }
500
519
 
501
- // src/auth/nonce.ts
502
- var SIWX_CHALLENGE_EXPIRY_MS = 5 * 60 * 1e3;
503
- var MemoryNonceStore = class {
504
- seen = /* @__PURE__ */ new Map();
505
- async check(nonce) {
506
- this.evict();
507
- if (this.seen.has(nonce)) return false;
508
- this.seen.set(nonce, Date.now() + SIWX_CHALLENGE_EXPIRY_MS);
509
- return true;
510
- }
511
- evict() {
512
- const now = Date.now();
513
- for (const [n, exp] of this.seen) {
514
- if (exp < now) this.seen.delete(n);
515
- }
516
- }
517
- };
518
- function detectRedisClientType(client) {
519
- if (!client || typeof client !== "object") {
520
- throw new Error(
521
- "createRedisNonceStore requires a Redis client. Supported: @upstash/redis, ioredis. Pass your Redis client instance as the first argument."
522
- );
523
- }
524
- if ("options" in client && "status" in client) {
525
- return "ioredis";
526
- }
527
- const constructor = client.constructor?.name;
528
- if (constructor === "Redis" && "url" in client) {
529
- return "upstash";
530
- }
531
- if (typeof client.set === "function") {
532
- return "upstash";
533
- }
534
- throw new Error(
535
- "Unrecognized Redis client. Supported: @upstash/redis, ioredis. If using a different client, implement NonceStore interface directly."
536
- );
520
+ // src/pipeline/context/preflight.ts
521
+ function preflight(routeEntry, handler, deps, request) {
522
+ const meta = buildMeta(request, routeEntry);
523
+ const pluginCtx = firePluginHook(deps.plugin, "onRequest", meta) ?? createDefaultContext(meta);
524
+ return { routeEntry, handler, deps, request, meta, pluginCtx };
537
525
  }
538
- function createRedisNonceStore(client, opts) {
539
- const prefix = opts?.prefix ?? "siwx:nonce:";
540
- const ttlSeconds = Math.ceil((opts?.ttlMs ?? SIWX_CHALLENGE_EXPIRY_MS) / 1e3);
541
- const clientType = detectRedisClientType(client);
526
+ function buildMeta(request, routeEntry) {
542
527
  return {
543
- async check(nonce) {
544
- const key = `${prefix}${nonce}`;
545
- if (clientType === "upstash") {
546
- const redis = client;
547
- const result = await redis.set(key, "1", { ex: ttlSeconds, nx: true });
548
- return result !== null;
549
- }
550
- if (clientType === "ioredis") {
551
- const redis = client;
552
- const result = await redis.set(key, "1", "EX", ttlSeconds, "NX");
553
- return result === "OK";
554
- }
555
- throw new Error("Unknown Redis client type");
556
- }
528
+ requestId: crypto.randomUUID(),
529
+ method: request.method,
530
+ route: routeEntry.key,
531
+ origin: request.headers.get("origin") ?? new URL(request.url).origin,
532
+ referer: request.headers.get("referer"),
533
+ walletAddress: request.headers.get(HEADERS.WALLET_ADDRESS),
534
+ clientId: request.headers.get(HEADERS.CLIENT_ID),
535
+ sessionId: request.headers.get(HEADERS.SESSION_ID),
536
+ contentType: request.headers.get("content-type"),
537
+ headers: Object.fromEntries(request.headers.entries()),
538
+ startTime: Date.now()
557
539
  };
558
540
  }
559
541
 
560
- // src/protocols/detect.ts
561
- function detectProtocol(request) {
562
- if (request.headers.get("PAYMENT-SIGNATURE") || request.headers.get("X-PAYMENT")) {
563
- return "x402";
564
- }
565
- const auth = request.headers.get("Authorization");
566
- if (auth && auth.startsWith("Payment ")) {
567
- return "mpp";
568
- }
569
- if (request.headers.get("SIGN-IN-WITH-X")) {
570
- return "siwx";
571
- }
572
- return null;
573
- }
574
-
575
- // src/handler.ts
542
+ // src/pipeline/context/parse-body.ts
576
543
  import { NextResponse } from "next/server";
577
544
 
578
- // src/types.ts
579
- var HttpError = class extends Error {
580
- constructor(message, status) {
581
- super(message);
582
- this.status = status;
583
- this.name = "HttpError";
584
- }
585
- };
586
-
587
- // src/handler.ts
588
- async function safeCallHandler(handler, ctx, options = {}) {
589
- try {
590
- const result = await handler(ctx);
591
- if (result instanceof Response) return result;
592
- return NextResponse.json(result);
593
- } catch (error) {
594
- options.onError?.(error);
595
- const status = error instanceof HttpError ? error.status : typeof error.status === "number" ? error.status : 500;
596
- const message = error instanceof Error ? error.message : "Internal error";
597
- return NextResponse.json({ success: false, error: message }, { status });
598
- }
599
- }
600
-
601
545
  // src/body.ts
602
546
  async function bufferBody(request) {
603
547
  const text = await request.text();
@@ -622,299 +566,329 @@ function validateBody(parsed, schema) {
622
566
  };
623
567
  }
624
568
 
625
- // src/pricing.ts
626
- async function resolvePrice(pricing, body) {
627
- if (typeof pricing === "string") {
628
- return pricing;
629
- }
630
- if (typeof pricing === "function") {
631
- return pricing(body);
632
- }
633
- const { field, tiers, default: defaultTier } = pricing;
634
- const tierKey = body != null ? String(body[field] ?? "") : "";
635
- if (tierKey && tiers[tierKey]) {
636
- return tiers[tierKey].price;
637
- }
638
- if (defaultTier && tiers[defaultTier]) {
639
- return tiers[defaultTier].price;
640
- }
641
- if (!tierKey) {
642
- throw Object.assign(new Error(`Missing required field '${field}' for tier pricing`), {
643
- status: 400
644
- });
645
- }
646
- throw Object.assign(
647
- new Error(
648
- `Unknown tier '${tierKey}' for field '${field}'. Valid tiers: ${Object.keys(tiers).join(", ")}`
649
- ),
650
- { status: 400 }
651
- );
652
- }
653
- function resolveMaxPrice(pricing) {
654
- if (typeof pricing === "string") return pricing;
655
- if (typeof pricing === "function") {
656
- throw new Error("Dynamic pricing requires maxPrice \u2014 this should be caught at registration");
657
- }
658
- const { tiers } = pricing;
659
- let max = "0";
660
- for (const tier of Object.values(tiers)) {
661
- if (parseFloat(tier.price) > parseFloat(max)) {
662
- max = tier.price;
663
- }
664
- }
665
- return max;
569
+ // src/pipeline/context/parse-body.ts
570
+ async function parseBody(request, routeEntry) {
571
+ if (!routeEntry.bodySchema) return { ok: true, data: void 0 };
572
+ const raw = await bufferBody(request);
573
+ const result = validateBody(raw, routeEntry.bodySchema);
574
+ if (result.success) return { ok: true, data: result.data };
575
+ return {
576
+ ok: false,
577
+ response: NextResponse.json(
578
+ { success: false, error: result.error, issues: result.issues },
579
+ { status: 400 }
580
+ )
581
+ };
666
582
  }
667
583
 
668
- // src/orchestrate.ts
669
- import { Credential as Credential2, Receipt } from "mppx";
670
- import { call as viemCall } from "viem/actions";
671
- import { Transaction as TempoTransaction } from "viem/tempo";
672
- import { isAddress as isAddress2, getAddress as getAddress2 } from "viem";
584
+ // src/pipeline/context/parse-query.ts
585
+ function parseQuery(request, routeEntry) {
586
+ if (!routeEntry.querySchema) return void 0;
587
+ const params = Object.fromEntries(request.nextUrl.searchParams.entries());
588
+ const result = routeEntry.querySchema.safeParse(params);
589
+ return result.success ? result.data : params;
590
+ }
673
591
 
674
- // src/protocols/x402.ts
675
- init_x402_facilitators();
676
- init_evm();
677
- init_solana();
678
- async function buildX402Challenge(opts) {
679
- const { server, routeEntry, request, price, accepts, facilitatorsByNetwork, extensions } = opts;
680
- const { encodePaymentRequiredHeader } = await import("@x402/core/http");
681
- const resource = buildChallengeResource(request, routeEntry);
682
- const requirements = await buildChallengeRequirements(
683
- server,
684
- request,
685
- price,
686
- accepts,
687
- resource,
688
- facilitatorsByNetwork
689
- );
690
- const paymentRequired = await server.createPaymentRequiredResponse(
691
- requirements,
692
- resource,
693
- void 0,
694
- extensions
695
- );
696
- const encoded = encodePaymentRequiredHeader(paymentRequired);
697
- return { encoded, requirements };
592
+ // src/pipeline/context/errors.ts
593
+ function errorStatus(error, fallback) {
594
+ const status = error?.status;
595
+ return typeof status === "number" ? status : fallback;
698
596
  }
699
- async function verifyX402Payment(opts) {
700
- const { server, request, price, accepts } = opts;
701
- const payload = await readPaymentPayload(request);
702
- if (!payload) return null;
703
- const requirements = await buildExpectedRequirements(server, request, price, accepts);
704
- const matching = findVerifiableRequirements(server, requirements, payload);
705
- if (!matching) {
706
- return invalidPaymentVerification();
597
+ function errorMessage(error, fallback) {
598
+ return error instanceof Error ? error.message : fallback;
599
+ }
600
+ function handlerFailureError(response) {
601
+ const message = response.statusText || `Handler returned HTTP ${response.status}`;
602
+ return Object.assign(new Error(message), { status: response.status });
603
+ }
604
+
605
+ // src/pipeline/context/fail.ts
606
+ import { NextResponse as NextResponse2 } from "next/server";
607
+
608
+ // src/pipeline/context/fire-plugin-response.ts
609
+ function firePluginResponse(ctx, response, requestBody, responseBody) {
610
+ firePluginHook(ctx.deps.plugin, "onResponse", ctx.pluginCtx, {
611
+ statusCode: response.status,
612
+ statusText: response.statusText,
613
+ duration: Date.now() - ctx.meta.startTime,
614
+ contentType: response.headers.get("content-type"),
615
+ headers: Object.fromEntries(response.headers.entries()),
616
+ requestBody,
617
+ responseBody
618
+ });
619
+ if (response.status >= 400 && response.status !== 402) {
620
+ firePluginHook(ctx.deps.plugin, "onError", ctx.pluginCtx, {
621
+ status: response.status,
622
+ message: response.statusText || `HTTP ${response.status}`,
623
+ settled: false
624
+ });
707
625
  }
708
- let verify;
626
+ }
627
+
628
+ // src/pipeline/context/fail.ts
629
+ function fail(ctx, status, message, requestBody) {
630
+ const response = NextResponse2.json({ success: false, error: message }, { status });
631
+ firePluginResponse(ctx, response, requestBody);
632
+ return response;
633
+ }
634
+
635
+ // src/pipeline/context/run-validate.ts
636
+ async function runValidate(ctx, body) {
637
+ if (!ctx.routeEntry.validateFn) return null;
709
638
  try {
710
- verify = await server.verifyPayment(payload, matching);
639
+ await ctx.routeEntry.validateFn(body);
640
+ return null;
711
641
  } catch (err) {
712
- const sc = err.statusCode;
713
- if (sc && sc >= 400 && sc < 500) return invalidPaymentVerification();
714
- throw err;
715
- }
716
- if (!verify.isValid) return invalidPaymentVerification();
717
- if (typeof verify.payer !== "string" || verify.payer.length === 0) {
718
- throw new Error("x402 verification succeeded without a payer address");
642
+ return fail(ctx, errorStatus(err, 400), errorMessage(err, "Validation failed"), body);
719
643
  }
720
- return {
721
- valid: true,
722
- payer: verify.payer,
723
- payload,
724
- requirements: matching
725
- };
726
644
  }
727
- function findVerifiableRequirements(server, requirements, payload) {
728
- const strictMatch = server.findMatchingRequirements(requirements, payload);
729
- if (strictMatch) {
730
- return payload.x402Version === 2 ? payload.accepted : strictMatch;
645
+
646
+ // src/handler.ts
647
+ import { NextResponse as NextResponse3 } from "next/server";
648
+
649
+ // src/types.ts
650
+ var HttpError = class extends Error {
651
+ constructor(message, status) {
652
+ super(message);
653
+ this.status = status;
654
+ this.name = "HttpError";
731
655
  }
732
- if (payload.x402Version !== 2) {
733
- return null;
656
+ };
657
+
658
+ // src/handler.ts
659
+ async function safeCallHandler(handler, ctx, options = {}) {
660
+ try {
661
+ const result = await handler(ctx);
662
+ if (result instanceof Response) return result;
663
+ return NextResponse3.json(result);
664
+ } catch (error) {
665
+ options.onError?.(error);
666
+ const status = error instanceof HttpError ? error.status : typeof error.status === "number" ? error.status : 500;
667
+ const message = error instanceof Error ? error.message : "Internal error";
668
+ return NextResponse3.json({ success: false, error: message }, { status });
734
669
  }
735
- const stableMatch = requirements.find(
736
- (requirement) => matchesStableFields(requirement, payload.accepted)
737
- );
738
- return stableMatch ? payload.accepted : null;
739
- }
740
- function matchesStableFields(requirement, accepted) {
741
- return requirement.scheme === accepted.scheme && requirement.network === accepted.network && requirement.payTo === accepted.payTo && requirement.asset === accepted.asset && requirement.amount === accepted.amount && requirement.maxTimeoutSeconds === accepted.maxTimeoutSeconds;
742
- }
743
- async function buildExpectedRequirements(server, request, price, accepts) {
744
- const exactRequirements = await buildExactRequirements(server, request, price, accepts);
745
- const customRequirements = buildCustomRequirements(price, accepts);
746
- return [...exactRequirements, ...customRequirements];
747
670
  }
748
- async function buildExactRequirements(server, request, price, accepts) {
749
- const exactGroups = [
750
- buildEvmExactOptions(accepts, price),
751
- buildSolanaExactOptions(accepts, price)
752
- ].filter((options) => options.length > 0);
753
- if (exactGroups.length === 0) return [];
754
- const requirements = [];
755
- const failures = [];
756
- for (const options of exactGroups) {
757
- try {
758
- requirements.push(
759
- ...await server.buildPaymentRequirementsFromOptions(options, { request })
760
- );
761
- } catch (error) {
762
- const err = error instanceof Error ? error : new Error(String(error));
763
- failures.push(err);
764
- if (exactGroups.length === 1) {
765
- throw err;
671
+
672
+ // src/pipeline/context/invoke.ts
673
+ async function invoke(ctx, wallet, account, body, payment) {
674
+ const handlerCtx = {
675
+ body,
676
+ query: parseQuery(ctx.request, ctx.routeEntry),
677
+ request: ctx.request,
678
+ requestId: ctx.meta.requestId,
679
+ route: ctx.routeEntry.key,
680
+ wallet,
681
+ payment,
682
+ account,
683
+ alert(level, message, alertMeta) {
684
+ firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
685
+ level,
686
+ message,
687
+ route: ctx.routeEntry.key,
688
+ meta: alertMeta
689
+ });
690
+ },
691
+ setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
692
+ };
693
+ let rawResult;
694
+ let handlerError;
695
+ const response = await safeCallHandler(
696
+ async (c) => {
697
+ rawResult = await ctx.handler(c);
698
+ return rawResult;
699
+ },
700
+ handlerCtx,
701
+ {
702
+ onError(error) {
703
+ handlerError = error;
766
704
  }
767
- console.warn(
768
- `[router] Failed to build x402 exact requirements for ${options[0]?.network}: ${err.message}`
769
- );
770
705
  }
706
+ );
707
+ return { response, rawResult, handlerError };
708
+ }
709
+
710
+ // src/pipeline/context/fire-provider-quota.ts
711
+ function fireProviderQuota(ctx, response, handlerResult) {
712
+ const { providerName, providerConfig } = ctx.routeEntry;
713
+ if (!providerName || !providerConfig?.extractQuota) return;
714
+ if (response.status >= 400) return;
715
+ try {
716
+ const quota = providerConfig.extractQuota(handlerResult, response.headers);
717
+ if (!quota) return;
718
+ const level = computeQuotaLevel(quota.remaining, providerConfig.warn, providerConfig.critical);
719
+ const overage = providerConfig.overage ?? "same-rate";
720
+ const event = {
721
+ provider: providerName,
722
+ route: ctx.routeEntry.key,
723
+ remaining: quota.remaining,
724
+ limit: quota.limit,
725
+ spend: quota.spend,
726
+ level,
727
+ overage,
728
+ message: quota.remaining !== null ? `${providerName}: ${quota.remaining}${quota.limit ? `/${quota.limit}` : ""} remaining` : `${providerName}: quota info unavailable`
729
+ };
730
+ firePluginHook(ctx.deps.plugin, "onProviderQuota", ctx.pluginCtx, event);
731
+ } catch {
771
732
  }
772
- if (requirements.length > 0) {
773
- return requirements;
774
- }
775
- throw failures[0] ?? new Error("Failed to build x402 exact requirements");
776
733
  }
777
- function buildCustomRequirements(price, accepts) {
778
- return accepts.filter((accept) => accept.scheme !== "exact").map((accept) => buildCustomRequirement(price, accept));
734
+ function computeQuotaLevel(remaining, warn, critical) {
735
+ if (remaining === null) return "healthy";
736
+ if (critical !== void 0 && remaining <= critical) return "critical";
737
+ if (warn !== void 0 && remaining <= warn) return "warn";
738
+ return "healthy";
779
739
  }
780
- async function buildChallengeRequirements(server, request, price, accepts, resource, facilitatorsByNetwork) {
781
- const requirements = await buildExpectedRequirements(server, request, price, accepts);
782
- if (!needsFacilitatorEnrichment(accepts)) return requirements;
783
- return enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork);
740
+
741
+ // src/pipeline/context/finalize.ts
742
+ function finalize(ctx, response, rawResult, requestBody) {
743
+ fireProviderQuota(ctx, response, rawResult);
744
+ firePluginResponse(ctx, response, requestBody, rawResult);
745
+ return response;
784
746
  }
785
- function needsFacilitatorEnrichment(accepts) {
786
- return accepts.some((accept) => accept.scheme !== "exact") || hasSolanaAccepts(accepts);
747
+
748
+ // src/pipeline/context/run-handler-only.ts
749
+ async function runHandlerOnly(ctx, wallet, account) {
750
+ const body = await parseBody(ctx.request, ctx.routeEntry);
751
+ if (!body.ok) {
752
+ firePluginResponse(ctx, body.response);
753
+ return body.response;
754
+ }
755
+ const validateErr = await runValidate(ctx, body.data);
756
+ if (validateErr) return validateErr;
757
+ const result = await invoke(ctx, wallet, account, body.data, null);
758
+ return finalize(ctx, result.response, result.rawResult, body.data);
787
759
  }
788
- async function enrichGroup(group, resource) {
789
- const accepted = await enrichRequirementsWithFacilitatorAccepts(
790
- group.facilitator,
791
- resource,
792
- group.items.map(({ requirement }) => requirement)
793
- );
794
- if (accepted.length !== group.items.length) {
795
- throw new Error(
796
- `Facilitator /accepts returned ${accepted.length} requirements for ${group.items.length} inputs on ${group.facilitator.url ?? group.facilitator.network}`
760
+
761
+ // src/pipeline/context/settlement-context.ts
762
+ function settlementContext(ctx, scope) {
763
+ return {
764
+ route: ctx.routeEntry.key,
765
+ request: ctx.request,
766
+ body: scope.body,
767
+ wallet: scope.wallet,
768
+ account: scope.account,
769
+ payment: scope.payment,
770
+ response: scope.response,
771
+ result: scope.rawResult
772
+ };
773
+ }
774
+
775
+ // src/pipeline/context/run-before-settle.ts
776
+ async function runBeforeSettle(ctx, scope) {
777
+ const hook = ctx.routeEntry.settlement?.beforeSettle;
778
+ if (!hook) return null;
779
+ try {
780
+ await hook(settlementContext(ctx, scope));
781
+ return null;
782
+ } catch (error) {
783
+ return fail(
784
+ ctx,
785
+ errorStatus(error, 500),
786
+ errorMessage(error, "Pre-settlement validation failed"),
787
+ scope.body
797
788
  );
798
789
  }
799
- return accepted;
800
790
  }
801
- async function enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork) {
802
- const groups = collectEnrichmentGroups(requirements, facilitatorsByNetwork);
803
- if (groups.length === 0) return requirements;
804
- const results = await Promise.all(
805
- groups.map(async (group) => {
806
- try {
807
- return { success: true, group, accepted: await enrichGroup(group, resource) };
808
- } catch (err) {
809
- const label = group.facilitator.url ?? group.facilitator.network;
810
- const reason = err instanceof Error ? err.message : String(err);
811
- console.warn(
812
- `[router] ${label} /accepts failed, dropping ${group.items.length} requirement(s): ${reason}`
813
- );
814
- return { success: false, group };
815
- }
816
- })
817
- );
818
- const enriched = [...requirements];
819
- results.filter((r) => r.success).forEach(({ group, accepted }) => {
820
- accepted.forEach((req, offset) => {
821
- const index = group.items[offset]?.index;
822
- if (index !== void 0) enriched[index] = req;
791
+
792
+ // src/pipeline/context/run-settlement-error.ts
793
+ async function runSettlementError(ctx, scope, error, phase) {
794
+ const hook = ctx.routeEntry.settlement?.onSettlementError;
795
+ if (!hook) return;
796
+ try {
797
+ await hook({ ...settlementContext(ctx, scope), error, phase });
798
+ } catch (hookError) {
799
+ const message = errorMessage(hookError, "Settlement error hook failed");
800
+ console.error(`[router] ${ctx.routeEntry.key}: onSettlementError failed: ${message}`);
801
+ firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
802
+ level: "error",
803
+ message: `Settlement error hook failed: ${message}`,
804
+ route: ctx.routeEntry.key
823
805
  });
824
- });
825
- const failedIndices = new Set(
826
- results.filter((r) => !r.success).flatMap(({ group }) => group.items.map(({ index }) => index))
827
- );
828
- const remaining = enriched.filter((_, i) => !failedIndices.has(i));
829
- if (remaining.length === 0) {
830
- throw new Error(
831
- "All facilitator enrichments failed; no payment requirements remain for challenge"
832
- );
833
806
  }
834
- return remaining;
835
807
  }
836
- function collectEnrichmentGroups(requirements, facilitatorsByNetwork) {
837
- const groups = [];
838
- requirements.forEach((requirement, index) => {
839
- if (!requiresFacilitatorEnrichment(requirement)) return;
840
- const facilitator = getRequiredFacilitator(requirement, facilitatorsByNetwork);
841
- const existing = groups.find(
842
- (group) => sameResolvedX402Facilitator(group.facilitator, facilitator)
843
- );
844
- if (existing) {
845
- existing.items.push({ index, requirement });
846
- return;
847
- }
848
- groups.push({
849
- facilitator,
850
- items: [{ index, requirement }]
808
+
809
+ // src/pipeline/context/run-after-settle.ts
810
+ async function runAfterSettle(ctx, scope) {
811
+ const hook = ctx.routeEntry.settlement?.afterSettle;
812
+ if (!hook) return;
813
+ try {
814
+ await hook(settlementContext(ctx, scope));
815
+ } catch (error) {
816
+ const message = errorMessage(error, "Post-settlement hook failed");
817
+ console.error(`[router] ${ctx.routeEntry.key}: afterSettle failed: ${message}`);
818
+ firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
819
+ level: "error",
820
+ message: `Post-settlement hook failed: ${message}`,
821
+ route: ctx.routeEntry.key
851
822
  });
852
- });
853
- return groups;
854
- }
855
- function getRequiredFacilitator(requirement, facilitatorsByNetwork) {
856
- const facilitator = getFacilitatorForRequirement(facilitatorsByNetwork, requirement);
857
- if (!facilitator) {
858
- throw new Error(
859
- `Missing x402 facilitator for ${requirement.scheme} requirement on ${requirement.network}`
860
- );
823
+ await runSettlementError(ctx, scope, error, "afterSettle");
861
824
  }
862
- return facilitator;
863
- }
864
- function requiresFacilitatorEnrichment(requirement) {
865
- return requirement.scheme !== "exact" || isSolanaRequirement(requirement);
866
825
  }
867
- function buildCustomRequirement(price, accept) {
868
- if (!accept.asset) {
869
- throw new Error(
870
- `Custom x402 accept '${accept.scheme}' on '${accept.network}' is missing asset`
871
- );
826
+
827
+ // src/pipeline/context/run-settled-handler-error.ts
828
+ async function runSettledHandlerError(ctx, scope, error = scope.handlerError ?? handlerFailureError(scope.response)) {
829
+ const hook = ctx.routeEntry.settlement?.onSettledHandlerError;
830
+ if (!hook) return;
831
+ try {
832
+ await hook({ ...settlementContext(ctx, scope), error });
833
+ } catch (hookError) {
834
+ const message = errorMessage(hookError, "Settled handler error hook failed");
835
+ console.error(`[router] ${ctx.routeEntry.key}: onSettledHandlerError failed: ${message}`);
836
+ firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
837
+ level: "error",
838
+ message: `Settled handler error hook failed: ${message}`,
839
+ route: ctx.routeEntry.key
840
+ });
872
841
  }
873
- return {
874
- scheme: accept.scheme,
875
- network: accept.network,
876
- amount: decimalToAtomicUnits(price, accept.decimals ?? 6),
877
- asset: accept.asset,
878
- payTo: accept.payTo,
879
- maxTimeoutSeconds: accept.maxTimeoutSeconds ?? 300,
880
- extra: accept.extra ?? {}
881
- };
882
- }
883
- function buildChallengeResource(request, routeEntry) {
884
- return {
885
- url: request.url,
886
- method: routeEntry.method,
887
- description: routeEntry.description,
888
- mimeType: "application/json"
889
- };
890
- }
891
- async function readPaymentPayload(request) {
892
- const paymentHeader = request.headers.get("PAYMENT-SIGNATURE") ?? request.headers.get("X-PAYMENT");
893
- if (!paymentHeader) return null;
894
- const { decodePaymentSignatureHeader } = await import("@x402/core/http");
895
- return decodePaymentSignatureHeader(paymentHeader);
896
- }
897
- function invalidPaymentVerification() {
898
- return { valid: false, payload: null, requirements: null, payer: null };
899
842
  }
900
- function decimalToAtomicUnits(amount, decimals) {
901
- const match = /^(?<whole>\d+)(?:\.(?<fraction>\d+))?$/.exec(amount);
902
- if (!match?.groups) {
903
- throw new Error(`Invalid decimal amount '${amount}'`);
904
- }
905
- const whole = match.groups.whole;
906
- const fraction = match.groups.fraction ?? "";
907
- if (fraction.length > decimals) {
908
- throw new Error(`Amount '${amount}' exceeds ${decimals} decimal places`);
843
+
844
+ // src/pipeline/context/grant-entitlement.ts
845
+ async function grantEntitlementIfSiwx(ctx, wallet) {
846
+ if (!ctx.routeEntry.siwxEnabled) return;
847
+ try {
848
+ await ctx.deps.entitlementStore.grant(ctx.routeEntry.key, wallet);
849
+ } catch (error) {
850
+ firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
851
+ level: "warn",
852
+ message: `Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`,
853
+ route: ctx.routeEntry.key
854
+ });
909
855
  }
910
- const normalized = `${whole}${fraction.padEnd(decimals, "0")}`.replace(/^0+(?=\d)/, "");
911
- return normalized === "" ? "0" : normalized;
912
856
  }
913
- async function settleX402Payment(server, payload, requirements) {
914
- const { encodePaymentResponseHeader } = await import("@x402/core/http");
915
- const result = await server.settlePayment(payload, requirements);
916
- const encoded = encodePaymentResponseHeader(result);
917
- return { encoded, result };
857
+
858
+ // src/pipeline/context/settle-and-finalize.ts
859
+ async function settleAndFinalize(args) {
860
+ const { ctx, strategy, verifyOutcome, scope, rawResult, body, onSettleError } = args;
861
+ const { request, routeEntry, deps } = ctx;
862
+ const settle = await strategy.settle({
863
+ request,
864
+ response: scope.response,
865
+ payment: verifyOutcome.payment,
866
+ token: verifyOutcome.token,
867
+ routeEntry,
868
+ deps
869
+ });
870
+ if (!settle.ok) {
871
+ if (onSettleError) await onSettleError(settle.error, settle.failMessage);
872
+ return fail(ctx, settle.failStatus ?? 500, settle.failMessage, body);
873
+ }
874
+ await grantEntitlementIfSiwx(ctx, verifyOutcome.wallet);
875
+ firePluginHook(deps.plugin, "onPaymentSettled", ctx.pluginCtx, {
876
+ protocol: strategy.protocol,
877
+ payer: verifyOutcome.wallet,
878
+ transaction: settle.settledPayment.transaction ?? "",
879
+ network: settle.settledPayment.network
880
+ });
881
+ await runAfterSettle(ctx, {
882
+ ...scope,
883
+ payment: settle.settledPayment,
884
+ response: settle.response
885
+ });
886
+ return finalize(ctx, settle.response, rawResult, body);
887
+ }
888
+
889
+ // src/auth/normalize-wallet.ts
890
+ function normalizeWalletAddress(address) {
891
+ return /^0x/i.test(address) ? address.toLowerCase() : address;
918
892
  }
919
893
 
920
894
  // src/auth/siwx.ts
@@ -938,7 +912,7 @@ function categorizeValidationError(error) {
938
912
  }
939
913
  async function verifySIWX(request, _routeEntry, nonceStore) {
940
914
  const { parseSIWxHeader, validateSIWxMessage, verifySIWxSignature } = await import("@x402/extensions/sign-in-with-x");
941
- const header = request.headers.get("SIGN-IN-WITH-X");
915
+ const header = request.headers.get(HEADERS.SIWX);
942
916
  if (!header) {
943
917
  return { valid: false, wallet: null, code: "siwx_missing_header" };
944
918
  }
@@ -967,25 +941,52 @@ async function buildSIWXExtension() {
967
941
  return declareSIWxExtension();
968
942
  }
969
943
 
970
- // src/auth/mpp-siwx.ts
971
- import { Credential } from "mppx";
972
- import { isAddress, getAddress } from "viem";
973
- async function verifyMppSiwx(request, mppx) {
974
- const result = await mppx.charge({ amount: "0" })(request);
975
- if (result.status === 402) {
976
- return { valid: false, challenge: result.challenge };
944
+ // src/pipeline/context/try-siwx-fast-path.ts
945
+ async function trySiwxFastPath(ctx, account) {
946
+ const { request, routeEntry, deps } = ctx;
947
+ if (!routeEntry.siwxEnabled) return null;
948
+ const siwxHeader = request.headers.get(HEADERS.SIWX);
949
+ if (!siwxHeader) return null;
950
+ const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
951
+ if (!siwx.valid) return null;
952
+ const wallet = normalizeWalletAddress(siwx.wallet);
953
+ ctx.pluginCtx.setVerifiedWallet(wallet);
954
+ const entitled = await deps.entitlementStore.has(routeEntry.key, wallet);
955
+ if (!entitled) return null;
956
+ firePluginHook(deps.plugin, "onAuthVerified", ctx.pluginCtx, {
957
+ authMode: "siwx",
958
+ wallet,
959
+ route: routeEntry.key
960
+ });
961
+ return runHandlerOnly(ctx, wallet, account);
962
+ }
963
+
964
+ // src/pipeline/context/should-parse-body-early.ts
965
+ function shouldParseBodyEarly(incomingStrategy, routeEntry, pricing) {
966
+ if (incomingStrategy) return false;
967
+ if (!routeEntry.bodySchema) return false;
968
+ return (pricing?.needsBody ?? false) || !!routeEntry.validateFn;
969
+ }
970
+
971
+ // src/pipeline/context/protocol-init-error.ts
972
+ function protocolInitError(routeEntry, deps) {
973
+ if (!routeEntry.pricing) return null;
974
+ const errors = [];
975
+ for (const protocol of routeEntry.protocols) {
976
+ if (protocol === "x402" && deps.x402InitError) {
977
+ errors.push(`x402: ${deps.x402InitError}`);
978
+ }
979
+ if (protocol === "mpp" && deps.mppInitError) {
980
+ errors.push(`mpp: ${deps.mppInitError}`);
981
+ }
977
982
  }
978
- const credential = Credential.fromRequest(request);
979
- const rawSource = credential?.source ?? "";
980
- const didParts = rawSource.split(":");
981
- const lastPart = didParts[didParts.length - 1];
982
- const wallet = normalizeWalletAddress(isAddress(lastPart) ? getAddress(lastPart) : rawSource);
983
- return { valid: true, wallet, withReceipt: result.withReceipt };
983
+ if (errors.length === 0) return null;
984
+ return `Payment protocol initialization failed. ${errors.join("; ")}`;
984
985
  }
985
986
 
986
987
  // src/auth/api-key.ts
987
988
  async function verifyApiKey(request, resolver) {
988
- const apiKey = request.headers.get("X-API-Key") ?? extractBearerToken(request.headers.get("Authorization"));
989
+ const apiKey = request.headers.get(HEADERS.API_KEY) ?? extractBearerToken(request.headers.get(HEADERS.AUTHORIZATION));
989
990
  if (!apiKey) {
990
991
  return { valid: false, account: null };
991
992
  }
@@ -997,947 +998,873 @@ async function verifyApiKey(request, resolver) {
997
998
  }
998
999
  function extractBearerToken(header) {
999
1000
  if (!header) return null;
1000
- if (header.startsWith("Bearer ")) return header.slice(7);
1001
+ if (header.startsWith(AUTH_SCHEME.BEARER)) return header.slice(AUTH_SCHEME.BEARER.length);
1001
1002
  return null;
1002
1003
  }
1003
1004
 
1004
- // src/orchestrate.ts
1005
- init_x402_config();
1006
- function getRequirementNetwork(requirements, fallback) {
1007
- const network = requirements?.network;
1008
- return typeof network === "string" ? network : fallback;
1005
+ // src/pipeline/flows/api-key-only.ts
1006
+ async function runApiKeyOnlyFlow(ctx) {
1007
+ if (!ctx.routeEntry.apiKeyResolver) {
1008
+ return fail(ctx, 401, "API key resolver not configured");
1009
+ }
1010
+ const result = await verifyApiKey(ctx.request, ctx.routeEntry.apiKeyResolver);
1011
+ if (!result.valid) return fail(ctx, 401, "Invalid or missing API key");
1012
+ firePluginHook(ctx.deps.plugin, "onAuthVerified", ctx.pluginCtx, {
1013
+ authMode: "apiKey",
1014
+ wallet: null,
1015
+ route: ctx.routeEntry.key,
1016
+ account: result.account
1017
+ });
1018
+ return runHandlerOnly(ctx, null, result.account);
1009
1019
  }
1010
- function getRequirementRecipient(requirements) {
1011
- const payTo = requirements?.payTo;
1012
- return typeof payTo === "string" ? payTo : void 0;
1020
+
1021
+ // src/pricing/dynamic.ts
1022
+ var DynamicPricing = class {
1023
+ constructor(opts) {
1024
+ this.opts = opts;
1025
+ }
1026
+ needsBody = true;
1027
+ async quote(body) {
1028
+ try {
1029
+ const raw = await this.opts.fn(body);
1030
+ return this.cap(raw, body);
1031
+ } catch (err) {
1032
+ this.alert("error", `Pricing function failed: ${msg(err)}`, {
1033
+ error: err instanceof Error ? err.stack : String(err),
1034
+ body
1035
+ });
1036
+ if (this.opts.maxPrice) {
1037
+ this.alert("warn", `Using maxPrice ${this.opts.maxPrice} as fallback after pricing error`);
1038
+ return this.opts.maxPrice;
1039
+ }
1040
+ throw err;
1041
+ }
1042
+ }
1043
+ challengeQuote(body) {
1044
+ if (body === void 0) return Promise.resolve(this.opts.maxPrice ?? "0");
1045
+ return this.quote(body);
1046
+ }
1047
+ describe() {
1048
+ return {
1049
+ mode: "dynamic",
1050
+ min: this.opts.minPrice ?? "0",
1051
+ max: this.opts.maxPrice ?? "0"
1052
+ };
1053
+ }
1054
+ cap(raw, body) {
1055
+ if (!this.opts.maxPrice) return raw;
1056
+ const n = parseFloat(raw);
1057
+ const max = parseFloat(this.opts.maxPrice);
1058
+ if (!Number.isFinite(n) || n > max) {
1059
+ this.alert("warn", `Price ${raw} exceeds maxPrice ${this.opts.maxPrice}, capping`, {
1060
+ calculated: raw,
1061
+ maxPrice: this.opts.maxPrice,
1062
+ body
1063
+ });
1064
+ return this.opts.maxPrice;
1065
+ }
1066
+ return raw;
1067
+ }
1068
+ alert(level, message, meta) {
1069
+ this.opts.alert?.(level, message, meta);
1070
+ }
1071
+ };
1072
+ function msg(err) {
1073
+ return err instanceof Error ? err.message : String(err);
1013
1074
  }
1014
- function errorStatus(error, fallback) {
1015
- const status = error?.status;
1016
- return typeof status === "number" ? status : fallback;
1075
+
1076
+ // src/pricing/fixed.ts
1077
+ var FixedPricing = class {
1078
+ constructor(price) {
1079
+ this.price = price;
1080
+ }
1081
+ needsBody = false;
1082
+ quote() {
1083
+ return Promise.resolve(this.price);
1084
+ }
1085
+ challengeQuote() {
1086
+ return Promise.resolve(this.price);
1087
+ }
1088
+ describe() {
1089
+ return { mode: "fixed", amount: this.price };
1090
+ }
1091
+ };
1092
+
1093
+ // src/pricing/tiered.ts
1094
+ var TieredPricing = class {
1095
+ constructor(opts) {
1096
+ this.opts = opts;
1097
+ }
1098
+ needsBody = true;
1099
+ async quote(body) {
1100
+ const { field, tiers, default: defaultTier } = this.opts;
1101
+ const tierKey = body != null ? String(body[field] ?? "") : "";
1102
+ if (tierKey && tiers[tierKey]) return tiers[tierKey].price;
1103
+ if (defaultTier && tiers[defaultTier]) return tiers[defaultTier].price;
1104
+ if (!tierKey) {
1105
+ throw httpError(400, `Missing required field '${field}' for tier pricing`);
1106
+ }
1107
+ throw httpError(
1108
+ 400,
1109
+ `Unknown tier '${tierKey}' for field '${field}'. Valid tiers: ${Object.keys(tiers).join(", ")}`
1110
+ );
1111
+ }
1112
+ challengeQuote(body) {
1113
+ if (body !== void 0) {
1114
+ try {
1115
+ return this.quote(body);
1116
+ } catch {
1117
+ }
1118
+ }
1119
+ return Promise.resolve(this.maxTierPrice());
1120
+ }
1121
+ describe() {
1122
+ return {
1123
+ mode: "tiered",
1124
+ tiers: Object.entries(this.opts.tiers).map(([key, tier]) => ({
1125
+ key,
1126
+ price: tier.price,
1127
+ ...tier.label !== void 0 ? { label: tier.label } : {}
1128
+ })),
1129
+ ...this.opts.default !== void 0 ? { default: this.opts.default } : {}
1130
+ };
1131
+ }
1132
+ maxTierPrice() {
1133
+ let max = "0";
1134
+ for (const tier of Object.values(this.opts.tiers)) {
1135
+ if (parseFloat(tier.price) > parseFloat(max)) max = tier.price;
1136
+ }
1137
+ return max;
1138
+ }
1139
+ };
1140
+ function httpError(status, message) {
1141
+ return Object.assign(new Error(message), { status });
1017
1142
  }
1018
- function errorMessage(error, fallback) {
1019
- return error instanceof Error ? error.message : fallback;
1143
+
1144
+ // src/pricing/index.ts
1145
+ function selectPricing(raw, deps = {}) {
1146
+ if (raw == null) return null;
1147
+ if (typeof raw === "string") {
1148
+ return new FixedPricing(raw);
1149
+ }
1150
+ if (typeof raw === "function") {
1151
+ return new DynamicPricing({
1152
+ fn: raw,
1153
+ maxPrice: deps.maxPrice,
1154
+ minPrice: deps.minPrice,
1155
+ route: deps.route,
1156
+ alert: deps.alert
1157
+ });
1158
+ }
1159
+ if (typeof raw === "object" && "tiers" in raw) {
1160
+ return new TieredPricing({
1161
+ field: raw.field,
1162
+ tiers: raw.tiers,
1163
+ default: raw.default,
1164
+ maxPrice: deps.maxPrice,
1165
+ minPrice: deps.minPrice
1166
+ });
1167
+ }
1168
+ throw new Error(`Unknown pricing config: ${JSON.stringify(raw)}`);
1020
1169
  }
1021
- function handlerFailureError(response) {
1022
- const message = response.statusText || `Handler returned HTTP ${response.status}`;
1023
- return Object.assign(new Error(message), { status: response.status });
1170
+
1171
+ // src/protocols/mpp/credential.ts
1172
+ import { Credential } from "mppx";
1173
+ import { getAddress, isAddress } from "viem";
1174
+ function readMppCredential(request) {
1175
+ const credential = Credential.fromRequest(request);
1176
+ if (!credential) return null;
1177
+ const wallet = walletFromDid(credential.source ?? "");
1178
+ const rawType = credential.payload?.type;
1179
+ const payloadType = rawType === "transaction" ? "transaction" : rawType === "hash" ? "hash" : "unknown";
1180
+ return { credential, wallet, payloadType };
1024
1181
  }
1025
- function siwxSignatureType(network) {
1026
- return network.startsWith("solana:") ? "ed25519" : "eip191";
1182
+ function walletFromDid(rawSource) {
1183
+ const parts = rawSource.split(":");
1184
+ const last = parts[parts.length - 1];
1185
+ return normalizeWalletAddress(isAddress(last) ? getAddress(last) : rawSource);
1027
1186
  }
1028
- function getSupportedChains(x402Accepts, fallbackNetwork) {
1029
- const seen = /* @__PURE__ */ new Set();
1030
- const chains = [];
1031
- for (const accept of x402Accepts) {
1032
- if (accept.network && !seen.has(accept.network)) {
1033
- seen.add(accept.network);
1034
- chains.push({ chainId: accept.network, type: siwxSignatureType(accept.network) });
1035
- }
1187
+
1188
+ // src/protocols/mpp/transaction-mode.ts
1189
+ import { Transaction as TempoTransaction } from "viem/tempo";
1190
+ import { call as viemCall } from "viem/actions";
1191
+
1192
+ // src/protocols/mpp/receipt.ts
1193
+ import { Receipt } from "mppx";
1194
+ function extractTxHash(receiptHeader) {
1195
+ if (!receiptHeader) return "";
1196
+ try {
1197
+ return Receipt.deserialize(receiptHeader).reference;
1198
+ } catch {
1199
+ return "";
1036
1200
  }
1037
- if (chains.length === 0) {
1038
- chains.push({ chainId: fallbackNetwork, type: siwxSignatureType(fallbackNetwork) });
1201
+ }
1202
+ async function readChallengeReason(challenge) {
1203
+ try {
1204
+ const text = await challenge.clone().text();
1205
+ if (!text) return "";
1206
+ const problem = JSON.parse(text);
1207
+ return problem.detail || problem.title || "";
1208
+ } catch {
1209
+ return "";
1039
1210
  }
1040
- return chains;
1041
1211
  }
1042
- function createRequestHandler(routeEntry, handler, deps) {
1043
- async function invoke(request, meta, pluginCtx, wallet, account, parsedBody, payment) {
1044
- const ctx = {
1045
- body: parsedBody,
1046
- query: parseQuery(request, routeEntry),
1047
- request,
1048
- requestId: meta.requestId,
1049
- route: routeEntry.key,
1050
- wallet,
1051
- payment,
1052
- account,
1053
- alert(level, message, alertMeta) {
1054
- firePluginHook(deps.plugin, "onAlert", pluginCtx, {
1055
- level,
1056
- message,
1057
- route: routeEntry.key,
1058
- meta: alertMeta
1059
- });
1060
- },
1061
- setVerifiedWallet: (addr) => pluginCtx.setVerifiedWallet(addr)
1212
+
1213
+ // src/protocols/mpp/transaction-mode.ts
1214
+ async function verifyTxMode(args, info) {
1215
+ const { deps, price, routeEntry } = args;
1216
+ if (!deps.tempoClient) {
1217
+ return {
1218
+ ok: false,
1219
+ kind: "config",
1220
+ message: "tempoClient not configured for MPP transaction-payload mode"
1062
1221
  };
1063
- let rawResult;
1064
- let handlerError;
1065
- const response = await safeCallHandler(
1066
- async (c) => {
1067
- rawResult = await handler(c);
1068
- return rawResult;
1069
- },
1070
- ctx,
1071
- {
1072
- onError(error) {
1073
- handlerError = error;
1074
- }
1075
- }
1076
- );
1077
- return { response, rawResult, handlerError };
1078
1222
  }
1079
- function finalize(response, rawResult, meta, pluginCtx, requestBody) {
1080
- fireProviderQuota(routeEntry, response, rawResult, deps, pluginCtx);
1081
- firePluginResponse(deps, pluginCtx, meta, response, requestBody, rawResult);
1223
+ try {
1224
+ const serializedTx = info.credential.payload.signature;
1225
+ const transaction = TempoTransaction.deserialize(serializedTx);
1226
+ await viemCall(deps.tempoClient, {
1227
+ ...transaction,
1228
+ account: transaction.from,
1229
+ calls: transaction.calls ?? []
1230
+ });
1231
+ } catch (err) {
1232
+ const message = err instanceof Error ? err.message : String(err);
1233
+ console.warn(`[router] ${routeEntry.key}: MPP simulation failed \u2014 ${message}`);
1234
+ return { ok: false, kind: "invalid" };
1235
+ }
1236
+ const mppRecipient = deps.mppRecipient ?? deps.payeeAddress;
1237
+ const payment = {
1238
+ protocol: "mpp",
1239
+ status: "verified",
1240
+ payer: info.wallet,
1241
+ amount: price,
1242
+ network: "tempo:4217",
1243
+ ...mppRecipient ? { recipient: mppRecipient } : {}
1244
+ };
1245
+ return {
1246
+ ok: true,
1247
+ wallet: info.wallet,
1248
+ payment,
1249
+ token: { mode: "transaction", credential: info.credential },
1250
+ alreadySettled: false
1251
+ };
1252
+ }
1253
+ async function settleTxMode(args) {
1254
+ const { request, response, payment, deps, routeEntry } = args;
1255
+ if (!deps.mppx) {
1256
+ return {
1257
+ ok: false,
1258
+ error: new Error("mppx unavailable"),
1259
+ failMessage: "MPP not initialized",
1260
+ failStatus: 500
1261
+ };
1082
1262
  }
1083
- function fail(status, message, meta, pluginCtx, requestBody) {
1084
- const response = NextResponse2.json({ success: false, error: message }, { status });
1085
- firePluginResponse(deps, pluginCtx, meta, response, requestBody);
1086
- return response;
1263
+ let result;
1264
+ try {
1265
+ result = await deps.mppx.charge({ amount: payment.amount })(request);
1266
+ } catch (err) {
1267
+ const message = err instanceof Error ? err.message : String(err);
1268
+ console.error(`[router] ${routeEntry.key}: MPP broadcast failed after handler: ${message}`);
1269
+ return {
1270
+ ok: false,
1271
+ error: err,
1272
+ failMessage: `MPP payment processing failed: ${message}`,
1273
+ failStatus: 500
1274
+ };
1087
1275
  }
1088
- function settlementContext(scope) {
1276
+ if (result.status === 402) {
1277
+ const reason = await readChallengeReason(result.challenge);
1278
+ const detail = reason || "transaction reverted on-chain after handler execution";
1279
+ const settlementError = Object.assign(new Error(detail), {
1280
+ status: 402,
1281
+ detail,
1282
+ mppResult: result,
1283
+ challenge: result.challenge
1284
+ });
1285
+ console.error(`[router] ${routeEntry.key}: MPP payment failed after handler \u2014 ${detail}`);
1089
1286
  return {
1090
- route: routeEntry.key,
1091
- request: scope.request,
1092
- body: scope.parsedBody,
1093
- wallet: scope.wallet,
1094
- account: scope.account,
1095
- payment: scope.payment,
1096
- response: scope.response,
1097
- result: scope.rawResult
1287
+ ok: false,
1288
+ error: settlementError,
1289
+ failMessage: `MPP payment failed: ${detail}`,
1290
+ failStatus: 500
1098
1291
  };
1099
1292
  }
1100
- async function runBeforeSettle(scope) {
1101
- const hook = routeEntry.settlement?.beforeSettle;
1102
- if (!hook) return null;
1293
+ const receiptResponse = result.withReceipt(response);
1294
+ receiptResponse.headers.set("Cache-Control", "private");
1295
+ const receiptHeader = receiptResponse.headers.get(HEADERS.MPP_PAYMENT_RECEIPT) ?? void 0;
1296
+ const txHash = extractTxHash(receiptHeader);
1297
+ const settledPayment = {
1298
+ ...payment,
1299
+ status: "settled",
1300
+ ...txHash ? { transaction: txHash } : {},
1301
+ ...receiptHeader ? { receipt: receiptHeader } : {}
1302
+ };
1303
+ return { ok: true, response: receiptResponse, settledPayment };
1304
+ }
1305
+
1306
+ // src/protocols/mpp/hash-mode.ts
1307
+ async function verifyHashMode(args, info) {
1308
+ const { deps, price, routeEntry, request } = args;
1309
+ if (!deps.mppx) {
1310
+ const reason = deps.mppInitError ? `MPP initialization failed: ${deps.mppInitError}` : "MPP not initialized \u2014 ensure mppx is installed and mpp config (secretKey, currency, recipient) is correct";
1311
+ console.error(`[router] ${routeEntry.key}: ${reason}`);
1312
+ return { ok: false, kind: "config", message: reason };
1313
+ }
1314
+ let chargeResult;
1315
+ try {
1316
+ chargeResult = await deps.mppx.charge({ amount: price })(request);
1317
+ } catch (err) {
1318
+ const message = err instanceof Error ? err.message : String(err);
1319
+ console.error(`[router] ${routeEntry.key}: MPP charge failed: ${message}`);
1320
+ return { ok: false, kind: "config", message: `MPP payment processing failed: ${message}` };
1321
+ }
1322
+ if (chargeResult.status === 402) {
1323
+ const reason = await readChallengeReason(chargeResult.challenge);
1324
+ const detail = reason || "credential may be invalid, or check TEMPO_RPC_URL configuration";
1325
+ console.warn(`[router] ${routeEntry.key}: MPP credential rejected \u2014 ${detail}`);
1326
+ return { ok: false, kind: "invalid" };
1327
+ }
1328
+ const receiptHeader = chargeResult.withReceipt(new Response()).headers.get(
1329
+ HEADERS.MPP_PAYMENT_RECEIPT
1330
+ );
1331
+ const txHash = extractTxHash(receiptHeader);
1332
+ const mppRecipient = deps.mppRecipient ?? deps.payeeAddress;
1333
+ const payment = {
1334
+ protocol: "mpp",
1335
+ status: "settled",
1336
+ payer: info.wallet,
1337
+ amount: price,
1338
+ network: "tempo:4217",
1339
+ ...mppRecipient ? { recipient: mppRecipient } : {},
1340
+ ...txHash ? { transaction: txHash } : {},
1341
+ ...receiptHeader ? { receipt: receiptHeader } : {}
1342
+ };
1343
+ return {
1344
+ ok: true,
1345
+ wallet: info.wallet,
1346
+ payment,
1347
+ token: { mode: "hash", charge: chargeResult },
1348
+ alreadySettled: true
1349
+ };
1350
+ }
1351
+ function settleHashMode(args) {
1352
+ const { response, payment, token } = args;
1353
+ const hashToken = token;
1354
+ const receiptResponse = hashToken.charge.withReceipt(response);
1355
+ receiptResponse.headers.set("Cache-Control", "private");
1356
+ return {
1357
+ ok: true,
1358
+ response: receiptResponse,
1359
+ settledPayment: payment
1360
+ };
1361
+ }
1362
+
1363
+ // src/protocols/mpp/strategy.ts
1364
+ var mppStrategy = {
1365
+ protocol: "mpp",
1366
+ detects(request) {
1367
+ const auth = request.headers.get(HEADERS.AUTHORIZATION);
1368
+ return Boolean(auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT));
1369
+ },
1370
+ async verify(args) {
1371
+ const info = readMppCredential(args.request);
1372
+ if (!info) return { ok: false, kind: "invalid" };
1373
+ if (info.payloadType === "transaction" && args.deps.tempoClient) {
1374
+ return verifyTxMode(args, info);
1375
+ }
1376
+ return verifyHashMode(args, info);
1377
+ },
1378
+ async settle(args) {
1379
+ const token = args.token;
1380
+ if (token.mode === "transaction") return settleTxMode(args);
1381
+ return settleHashMode(args);
1382
+ },
1383
+ async buildChallenge(args) {
1384
+ if (!args.deps.mppx) return {};
1103
1385
  try {
1104
- await hook(settlementContext(scope));
1105
- return null;
1106
- } catch (error) {
1107
- return fail(
1108
- errorStatus(error, 500),
1109
- errorMessage(error, "Pre-settlement validation failed"),
1110
- scope.meta,
1111
- scope.pluginCtx,
1112
- scope.parsedBody
1386
+ const result = await args.deps.mppx.charge({ amount: args.price })(args.request);
1387
+ if (result.status === 402) {
1388
+ const wwwAuth = result.challenge.headers.get(HEADERS.WWW_AUTHENTICATE);
1389
+ if (wwwAuth) return { headers: { [HEADERS.WWW_AUTHENTICATE]: wwwAuth } };
1390
+ }
1391
+ } catch (err) {
1392
+ console.warn(
1393
+ `[router] MPP challenge build failed: ${err instanceof Error ? err.message : String(err)}`
1113
1394
  );
1395
+ throw err;
1114
1396
  }
1397
+ return {};
1115
1398
  }
1116
- async function runSettlementError(scope, error, phase) {
1117
- const hook = routeEntry.settlement?.onSettlementError;
1118
- if (!hook) return;
1399
+ };
1400
+
1401
+ // src/protocols/x402/strategy.ts
1402
+ init_accepts();
1403
+
1404
+ // src/protocols/x402/challenge.ts
1405
+ init_facilitators();
1406
+ init_solana();
1407
+
1408
+ // src/protocols/x402/requirements.ts
1409
+ init_evm();
1410
+ init_solana();
1411
+ async function buildExpectedRequirements(server, request, price, accepts) {
1412
+ const exactRequirements = await buildExactRequirements(server, request, price, accepts);
1413
+ const customRequirements = buildCustomRequirements(price, accepts);
1414
+ return [...exactRequirements, ...customRequirements];
1415
+ }
1416
+ async function buildExactRequirements(server, request, price, accepts) {
1417
+ const exactGroups = [
1418
+ buildEvmExactOptions(accepts, price),
1419
+ buildSolanaExactOptions(accepts, price)
1420
+ ].filter((options) => options.length > 0);
1421
+ if (exactGroups.length === 0) return [];
1422
+ const requirements = [];
1423
+ const failures = [];
1424
+ for (const options of exactGroups) {
1119
1425
  try {
1120
- await hook({
1121
- ...settlementContext(scope),
1122
- error,
1123
- phase
1124
- });
1125
- } catch (hookError) {
1126
- const message = errorMessage(hookError, "Settlement error hook failed");
1127
- console.error(`[router] ${routeEntry.key}: onSettlementError failed: ${message}`);
1128
- firePluginHook(deps.plugin, "onAlert", scope.pluginCtx, {
1129
- level: "error",
1130
- message: `Settlement error hook failed: ${message}`,
1131
- route: routeEntry.key
1132
- });
1426
+ requirements.push(
1427
+ ...await server.buildPaymentRequirementsFromOptions(options, { request })
1428
+ );
1429
+ } catch (error) {
1430
+ const err = error instanceof Error ? error : new Error(String(error));
1431
+ failures.push(err);
1432
+ if (exactGroups.length === 1) {
1433
+ throw err;
1434
+ }
1435
+ console.warn(
1436
+ `[router] Failed to build x402 exact requirements for ${options[0]?.network}: ${err.message}`
1437
+ );
1133
1438
  }
1134
1439
  }
1135
- async function runAfterSettle(scope) {
1136
- const hook = routeEntry.settlement?.afterSettle;
1137
- if (!hook) return;
1138
- try {
1139
- await hook(settlementContext(scope));
1140
- } catch (error) {
1141
- const message = errorMessage(error, "Post-settlement hook failed");
1142
- console.error(`[router] ${routeEntry.key}: afterSettle failed: ${message}`);
1143
- firePluginHook(deps.plugin, "onAlert", scope.pluginCtx, {
1144
- level: "error",
1145
- message: `Post-settlement hook failed: ${message}`,
1146
- route: routeEntry.key
1147
- });
1148
- await runSettlementError(scope, error, "afterSettle");
1149
- }
1440
+ if (requirements.length > 0) {
1441
+ return requirements;
1150
1442
  }
1151
- async function runSettledHandlerError(scope, error = scope.handlerError ?? handlerFailureError(scope.response)) {
1152
- const hook = routeEntry.settlement?.onSettledHandlerError;
1153
- if (!hook) return;
1154
- try {
1155
- await hook({
1156
- ...settlementContext(scope),
1157
- error
1158
- });
1159
- } catch (hookError) {
1160
- const message = errorMessage(hookError, "Settled handler error hook failed");
1161
- console.error(`[router] ${routeEntry.key}: onSettledHandlerError failed: ${message}`);
1162
- firePluginHook(deps.plugin, "onAlert", scope.pluginCtx, {
1163
- level: "error",
1164
- message: `Settled handler error hook failed: ${message}`,
1165
- route: routeEntry.key
1166
- });
1167
- }
1443
+ throw failures[0] ?? new Error("Failed to build x402 exact requirements");
1444
+ }
1445
+ function buildCustomRequirements(price, accepts) {
1446
+ return accepts.filter((accept) => accept.scheme !== "exact").map((accept) => buildCustomRequirement(price, accept));
1447
+ }
1448
+ function buildCustomRequirement(price, accept) {
1449
+ if (!accept.asset) {
1450
+ throw new Error(
1451
+ `Custom x402 accept '${accept.scheme}' on '${accept.network}' is missing asset`
1452
+ );
1168
1453
  }
1169
- return async (request) => {
1170
- await deps.initPromise;
1171
- const meta = buildMeta(request, routeEntry);
1172
- const pluginCtx = firePluginHook(deps.plugin, "onRequest", meta) ?? createDefaultContext(meta);
1173
- async function handleAuth(wallet, account2) {
1174
- const body2 = await parseBody(request, routeEntry);
1175
- if (!body2.ok) {
1176
- firePluginResponse(deps, pluginCtx, meta, body2.response);
1177
- return body2.response;
1178
- }
1179
- if (routeEntry.validateFn) {
1180
- try {
1181
- await routeEntry.validateFn(body2.data);
1182
- } catch (err) {
1183
- const status = err.status ?? 400;
1184
- const message = err instanceof Error ? err.message : "Validation failed";
1185
- return fail(status, message, meta, pluginCtx, body2.data);
1186
- }
1187
- }
1188
- const { response, rawResult } = await invoke(
1189
- request,
1190
- meta,
1191
- pluginCtx,
1192
- wallet,
1193
- account2,
1194
- body2.data,
1195
- null
1196
- );
1197
- finalize(response, rawResult, meta, pluginCtx, body2.data);
1198
- return response;
1199
- }
1200
- if (routeEntry.authMode === "unprotected") {
1201
- return handleAuth(null, void 0);
1202
- }
1203
- let account;
1204
- if (routeEntry.authMode === "apiKey" || routeEntry.apiKeyResolver) {
1205
- if (!routeEntry.apiKeyResolver) {
1206
- return fail(401, "API key resolver not configured", meta, pluginCtx);
1207
- }
1208
- const keyResult = await verifyApiKey(request, routeEntry.apiKeyResolver);
1209
- if (!keyResult.valid) {
1210
- return fail(401, "Invalid or missing API key", meta, pluginCtx);
1211
- }
1212
- account = keyResult.account;
1213
- firePluginHook(deps.plugin, "onAuthVerified", pluginCtx, {
1214
- authMode: "apiKey",
1215
- wallet: null,
1216
- route: routeEntry.key,
1217
- account
1218
- });
1219
- if (routeEntry.authMode === "apiKey" && !routeEntry.pricing) {
1220
- return handleAuth(null, account);
1221
- }
1222
- }
1223
- const protocol = detectProtocol(request);
1224
- let earlyBodyData;
1225
- const pricingNeedsBody = routeEntry.pricing != null && typeof routeEntry.pricing !== "string";
1226
- const needsEarlyParse = !protocol && routeEntry.bodySchema && (pricingNeedsBody || routeEntry.validateFn);
1227
- if (needsEarlyParse) {
1228
- const requestForPricing = request.clone();
1229
- const earlyBodyResult = await parseBody(requestForPricing, routeEntry);
1230
- if (earlyBodyResult.ok) {
1231
- earlyBodyData = earlyBodyResult.data;
1232
- if (routeEntry.validateFn) {
1233
- try {
1234
- await routeEntry.validateFn(earlyBodyData);
1235
- } catch (err) {
1236
- const status = err.status ?? 400;
1237
- const message = err instanceof Error ? err.message : "Validation failed";
1238
- return fail(status, message, meta, pluginCtx, earlyBodyData);
1239
- }
1240
- }
1241
- } else {
1242
- firePluginResponse(deps, pluginCtx, meta, earlyBodyResult.response);
1243
- return earlyBodyResult.response;
1244
- }
1245
- }
1246
- if (routeEntry.authMode === "siwx" || routeEntry.siwxEnabled) {
1247
- if (routeEntry.validateFn && routeEntry.bodySchema && !request.headers.get("SIGN-IN-WITH-X")) {
1248
- const requestForValidation = request.clone();
1249
- const earlyBodyResult = await parseBody(requestForValidation, routeEntry);
1250
- if (earlyBodyResult.ok) {
1251
- try {
1252
- await routeEntry.validateFn(earlyBodyResult.data);
1253
- } catch (err) {
1254
- const status = err.status ?? 400;
1255
- const message = err instanceof Error ? err.message : "Validation failed";
1256
- return fail(status, message, meta, pluginCtx, earlyBodyResult.data);
1257
- }
1258
- }
1259
- }
1260
- const siwxHeader = request.headers.get("SIGN-IN-WITH-X");
1261
- if (!siwxHeader && protocol === "mpp" && routeEntry.authMode === "siwx" && deps.mppx) {
1262
- let mppSiwxResult;
1263
- try {
1264
- mppSiwxResult = await verifyMppSiwx(request, deps.mppx);
1265
- } catch (err) {
1266
- const message = err instanceof Error ? err.message : String(err);
1267
- firePluginHook(deps.plugin, "onAlert", pluginCtx, {
1268
- level: "critical",
1269
- message: `MPP SIWX verification failed: ${message}`,
1270
- route: routeEntry.key
1271
- });
1272
- return fail(500, `MPP SIWX verification failed: ${message}`, meta, pluginCtx);
1273
- }
1274
- if (mppSiwxResult.valid) {
1275
- pluginCtx.setVerifiedWallet(mppSiwxResult.wallet);
1276
- firePluginHook(deps.plugin, "onAuthVerified", pluginCtx, {
1277
- authMode: "siwx",
1278
- wallet: mppSiwxResult.wallet,
1279
- route: routeEntry.key
1280
- });
1281
- const authResponse = await handleAuth(mppSiwxResult.wallet, void 0);
1282
- if (authResponse.status < 400) {
1283
- return mppSiwxResult.withReceipt(authResponse);
1284
- }
1285
- return authResponse;
1286
- }
1287
- }
1288
- if (!siwxHeader && routeEntry.authMode === "siwx") {
1289
- const url = new URL(request.url);
1290
- const nonce = crypto.randomUUID().replace(/-/g, "");
1291
- const supportedChains = getSupportedChains(deps.x402Accepts, deps.network);
1292
- const primaryChain = supportedChains[0];
1293
- const siwxInfo = {
1294
- domain: url.hostname,
1295
- uri: request.url,
1296
- version: "1",
1297
- chainId: primaryChain.chainId,
1298
- type: primaryChain.type,
1299
- nonce,
1300
- issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
1301
- expirationTime: new Date(Date.now() + SIWX_CHALLENGE_EXPIRY_MS).toISOString(),
1302
- statement: "Sign in to verify your wallet identity"
1303
- };
1304
- let siwxSchema;
1305
- try {
1306
- siwxSchema = await buildSIWXExtension();
1307
- } catch {
1308
- }
1309
- const paymentRequired = {
1310
- x402Version: 2,
1311
- error: "SIWX authentication required",
1312
- resource: {
1313
- url: request.url,
1314
- description: routeEntry.description ?? "SIWX-protected endpoint",
1315
- mimeType: "application/json"
1316
- },
1317
- accepts: [],
1318
- extensions: {
1319
- "sign-in-with-x": {
1320
- info: siwxInfo,
1321
- // supportedChains at top level required by MCP tools for chain detection
1322
- supportedChains,
1323
- ...siwxSchema ? { schema: siwxSchema } : {}
1324
- }
1325
- }
1326
- };
1327
- let encoded;
1328
- try {
1329
- const { encodePaymentRequiredHeader } = await import("@x402/core/http");
1330
- encoded = encodePaymentRequiredHeader(paymentRequired);
1331
- } catch (err) {
1332
- firePluginHook(deps.plugin, "onAlert", pluginCtx, {
1333
- level: "warn",
1334
- message: `SIWX challenge header encoding failed: ${err instanceof Error ? err.message : String(err)}`,
1335
- route: routeEntry.key
1336
- });
1337
- }
1338
- const response = new NextResponse2(JSON.stringify(paymentRequired), {
1339
- status: 402,
1340
- headers: { "Content-Type": "application/json", "Cache-Control": "no-store" }
1341
- });
1342
- if (encoded) response.headers.set("PAYMENT-REQUIRED", encoded);
1343
- if (deps.mppx) {
1344
- try {
1345
- const mppChallenge = await deps.mppx.charge({ amount: "0" })(request);
1346
- if (mppChallenge.status === 402) {
1347
- const wwwAuth = mppChallenge.challenge.headers.get("WWW-Authenticate");
1348
- if (wwwAuth) response.headers.set("WWW-Authenticate", wwwAuth);
1349
- }
1350
- } catch {
1351
- }
1352
- }
1353
- firePluginResponse(deps, pluginCtx, meta, response);
1354
- return response;
1355
- }
1356
- if (siwxHeader) {
1357
- const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
1358
- if (!siwx.valid) {
1359
- if (routeEntry.authMode === "siwx") {
1360
- const response = NextResponse2.json(
1361
- { error: siwx.code, message: SIWX_ERROR_MESSAGES[siwx.code] },
1362
- { status: 402 }
1363
- );
1364
- firePluginResponse(deps, pluginCtx, meta, response);
1365
- return response;
1366
- }
1367
- } else {
1368
- const wallet = normalizeWalletAddress(siwx.wallet);
1369
- pluginCtx.setVerifiedWallet(wallet);
1370
- if (routeEntry.authMode === "siwx") {
1371
- firePluginHook(deps.plugin, "onAuthVerified", pluginCtx, {
1372
- authMode: "siwx",
1373
- wallet,
1374
- route: routeEntry.key
1375
- });
1376
- return handleAuth(wallet, void 0);
1377
- }
1378
- if (routeEntry.siwxEnabled && routeEntry.pricing) {
1379
- const entitled = await deps.entitlementStore.has(routeEntry.key, wallet);
1380
- if (entitled) {
1381
- firePluginHook(deps.plugin, "onAuthVerified", pluginCtx, {
1382
- authMode: "siwx",
1383
- wallet,
1384
- route: routeEntry.key
1385
- });
1386
- return handleAuth(wallet, account);
1387
- }
1388
- }
1389
- }
1390
- }
1391
- }
1392
- if (!protocol || protocol === "siwx") {
1393
- if (routeEntry.pricing) {
1394
- const initErrors = routeEntry.protocols.map((p) => {
1395
- if (p === "x402" && deps.x402InitError) return `x402: ${deps.x402InitError}`;
1396
- if (p === "mpp" && deps.mppInitError) return `mpp: ${deps.mppInitError}`;
1397
- return null;
1398
- }).filter(Boolean);
1399
- if (initErrors.length > 0) {
1400
- return fail(
1401
- 500,
1402
- `Payment protocol initialization failed. ${initErrors.join("; ")}`,
1403
- meta,
1404
- pluginCtx
1405
- );
1406
- }
1407
- }
1408
- return await build402(request, routeEntry, deps, meta, pluginCtx, earlyBodyData);
1409
- }
1410
- const body = await parseBody(request, routeEntry);
1411
- if (!body.ok) {
1412
- firePluginResponse(deps, pluginCtx, meta, body.response);
1413
- return body.response;
1414
- }
1415
- if (routeEntry.validateFn) {
1454
+ return {
1455
+ scheme: accept.scheme,
1456
+ network: accept.network,
1457
+ amount: decimalToAtomicUnits(price, accept.decimals ?? 6),
1458
+ asset: accept.asset,
1459
+ payTo: accept.payTo,
1460
+ maxTimeoutSeconds: accept.maxTimeoutSeconds ?? 300,
1461
+ extra: accept.extra ?? {}
1462
+ };
1463
+ }
1464
+ function decimalToAtomicUnits(amount, decimals) {
1465
+ const match = /^(?<whole>\d+)(?:\.(?<fraction>\d+))?$/.exec(amount);
1466
+ if (!match?.groups) {
1467
+ throw new Error(`Invalid decimal amount '${amount}'`);
1468
+ }
1469
+ const whole = match.groups.whole;
1470
+ const fraction = match.groups.fraction ?? "";
1471
+ if (fraction.length > decimals) {
1472
+ throw new Error(`Amount '${amount}' exceeds ${decimals} decimal places`);
1473
+ }
1474
+ const normalized = `${whole}${fraction.padEnd(decimals, "0")}`.replace(/^0+(?=\d)/, "");
1475
+ return normalized === "" ? "0" : normalized;
1476
+ }
1477
+
1478
+ // src/protocols/x402/challenge.ts
1479
+ async function buildX402Challenge(opts) {
1480
+ const { server, routeEntry, request, price, accepts, facilitatorsByNetwork, extensions } = opts;
1481
+ const { encodePaymentRequiredHeader } = await import("@x402/core/http");
1482
+ const resource = buildChallengeResource(request, routeEntry);
1483
+ const requirements = await buildChallengeRequirements(
1484
+ server,
1485
+ request,
1486
+ price,
1487
+ accepts,
1488
+ resource,
1489
+ facilitatorsByNetwork
1490
+ );
1491
+ const paymentRequired = await server.createPaymentRequiredResponse(
1492
+ requirements,
1493
+ resource,
1494
+ void 0,
1495
+ extensions
1496
+ );
1497
+ const encoded = encodePaymentRequiredHeader(paymentRequired);
1498
+ return { encoded, requirements };
1499
+ }
1500
+ async function buildChallengeRequirements(server, request, price, accepts, resource, facilitatorsByNetwork) {
1501
+ const requirements = await buildExpectedRequirements(server, request, price, accepts);
1502
+ if (!needsFacilitatorEnrichment(accepts)) return requirements;
1503
+ return enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork);
1504
+ }
1505
+ function needsFacilitatorEnrichment(accepts) {
1506
+ return accepts.some((accept) => accept.scheme !== "exact") || hasSolanaAccepts(accepts);
1507
+ }
1508
+ async function enrichGroup(group, resource) {
1509
+ const accepted = await enrichRequirementsWithFacilitatorAccepts(
1510
+ group.facilitator,
1511
+ resource,
1512
+ group.items.map(({ requirement }) => requirement)
1513
+ );
1514
+ if (accepted.length !== group.items.length) {
1515
+ throw new Error(
1516
+ `Facilitator /accepts returned ${accepted.length} requirements for ${group.items.length} inputs on ${group.facilitator.url ?? group.facilitator.network}`
1517
+ );
1518
+ }
1519
+ return accepted;
1520
+ }
1521
+ async function enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork) {
1522
+ const groups = collectEnrichmentGroups(requirements, facilitatorsByNetwork);
1523
+ if (groups.length === 0) return requirements;
1524
+ const results = await Promise.all(
1525
+ groups.map(async (group) => {
1416
1526
  try {
1417
- await routeEntry.validateFn(body.data);
1527
+ return { success: true, group, accepted: await enrichGroup(group, resource) };
1418
1528
  } catch (err) {
1419
- const status = err.status ?? 400;
1420
- const message = err instanceof Error ? err.message : "Validation failed";
1421
- return fail(status, message, meta, pluginCtx, body.data);
1422
- }
1423
- }
1424
- const priceResult = await resolveDynamicPrice(body.data, routeEntry, deps, pluginCtx, meta);
1425
- if ("error" in priceResult) return priceResult.error;
1426
- const price = priceResult.price;
1427
- if (!routeEntry.protocols.includes(protocol)) {
1428
- const accepted = routeEntry.protocols.join(", ") || "none";
1429
- console.warn(
1430
- `[router] ${routeEntry.key}: received ${protocol} payment but route accepts [${accepted}]`
1431
- );
1432
- return fail(
1433
- 400,
1434
- `This route does not accept ${protocol} payments. Accepted protocols: ${accepted}`,
1435
- meta,
1436
- pluginCtx,
1437
- body.data
1438
- );
1439
- }
1440
- if (protocol === "x402") {
1441
- if (!deps.x402Server) {
1442
- 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";
1443
- console.error(`[router] ${routeEntry.key}: ${reason}`);
1444
- return fail(500, reason, meta, pluginCtx, body.data);
1445
- }
1446
- const accepts = await resolveX402Accepts(
1447
- request,
1448
- routeEntry,
1449
- deps.x402Accepts,
1450
- deps.payeeAddress,
1451
- body.data
1452
- );
1453
- const verify = await verifyX402Payment({
1454
- server: deps.x402Server,
1455
- request,
1456
- price,
1457
- accepts
1458
- });
1459
- if (!verify?.valid)
1460
- return await build402(request, routeEntry, deps, meta, pluginCtx, body.data);
1461
- const { payload: verifyPayload, requirements: verifyRequirements } = verify;
1462
- const matchedNetwork = getRequirementNetwork(verifyRequirements, deps.network);
1463
- const matchedRecipient = getRequirementRecipient(verifyRequirements);
1464
- const wallet = normalizeWalletAddress(verify.payer);
1465
- const payment = {
1466
- protocol: "x402",
1467
- status: "verified",
1468
- payer: wallet,
1469
- amount: price,
1470
- network: matchedNetwork,
1471
- ...matchedRecipient ? { recipient: matchedRecipient } : {}
1472
- };
1473
- pluginCtx.setVerifiedWallet(wallet);
1474
- firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
1475
- protocol: "x402",
1476
- payer: wallet,
1477
- amount: price,
1478
- network: matchedNetwork
1479
- });
1480
- const { response, rawResult, handlerError } = await invoke(
1481
- request,
1482
- meta,
1483
- pluginCtx,
1484
- wallet,
1485
- account,
1486
- body.data,
1487
- payment
1488
- );
1489
- const settleScope = {
1490
- request,
1491
- meta,
1492
- pluginCtx,
1493
- wallet,
1494
- account,
1495
- parsedBody: body.data,
1496
- payment,
1497
- response,
1498
- rawResult,
1499
- handlerError
1500
- };
1501
- if (response.status < 400) {
1502
- const validationFailure = await runBeforeSettle(settleScope);
1503
- if (validationFailure) return validationFailure;
1504
- try {
1505
- const settle = await settleX402Payment(
1506
- deps.x402Server,
1507
- verifyPayload,
1508
- verifyRequirements
1509
- );
1510
- if (!settle.result?.success) {
1511
- const reason = settle.result?.errorReason || "x402 settlement returned success=false";
1512
- const error = new Error(reason);
1513
- error.errorReason = reason;
1514
- throw error;
1515
- }
1516
- if (routeEntry.siwxEnabled) {
1517
- try {
1518
- await deps.entitlementStore.grant(routeEntry.key, wallet);
1519
- } catch (error) {
1520
- firePluginHook(deps.plugin, "onAlert", pluginCtx, {
1521
- level: "warn",
1522
- message: `Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`,
1523
- route: routeEntry.key
1524
- });
1525
- }
1526
- }
1527
- response.headers.set("PAYMENT-RESPONSE", settle.encoded);
1528
- response.headers.set("Cache-Control", "private");
1529
- const transaction = String(settle.result?.transaction ?? "");
1530
- const settledPayment = {
1531
- ...payment,
1532
- status: "settled",
1533
- ...transaction ? { transaction } : {}
1534
- };
1535
- firePluginHook(deps.plugin, "onPaymentSettled", pluginCtx, {
1536
- protocol: "x402",
1537
- payer: wallet,
1538
- transaction,
1539
- network: matchedNetwork
1540
- });
1541
- await runAfterSettle({ ...settleScope, payment: settledPayment });
1542
- } catch (err) {
1543
- await runSettlementError(settleScope, err, "settle");
1544
- const errObj = err;
1545
- console.error("Settlement failed", {
1546
- message: err instanceof Error ? err.message : String(err),
1547
- route: routeEntry.key,
1548
- network: matchedNetwork,
1549
- errorReason: errObj.errorReason,
1550
- facilitatorStatus: errObj.response?.status,
1551
- facilitatorBody: errObj.response?.data ?? errObj.response?.body
1552
- });
1553
- firePluginHook(deps.plugin, "onAlert", pluginCtx, {
1554
- level: "critical",
1555
- message: `Settlement failed: ${err instanceof Error ? err.message : String(err)}`,
1556
- route: routeEntry.key
1557
- });
1558
- return fail(500, "Settlement failed", meta, pluginCtx, body.data);
1559
- }
1560
- }
1561
- finalize(response, rawResult, meta, pluginCtx, body.data);
1562
- return response;
1563
- }
1564
- if (protocol === "mpp") {
1565
- if (!deps.mppx) {
1566
- 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";
1567
- console.error(`[router] ${routeEntry.key}: ${reason}`);
1568
- return fail(500, reason, meta, pluginCtx, body.data);
1569
- }
1570
- const mppCredential = Credential2.fromRequest(request);
1571
- const rawSource = mppCredential?.source ?? "";
1572
- const didParts = rawSource.split(":");
1573
- const lastPart = didParts[didParts.length - 1];
1574
- const wallet = normalizeWalletAddress(isAddress2(lastPart) ? getAddress2(lastPart) : rawSource);
1575
- const payloadType = mppCredential?.payload?.type;
1576
- if (payloadType === "transaction" && deps.tempoClient) {
1577
- try {
1578
- const serializedTx = mppCredential.payload.signature;
1579
- const transaction = TempoTransaction.deserialize(serializedTx);
1580
- await viemCall(deps.tempoClient, {
1581
- ...transaction,
1582
- account: transaction.from,
1583
- calls: transaction.calls ?? []
1584
- });
1585
- } catch (err) {
1586
- const message = err instanceof Error ? err.message : String(err);
1587
- console.warn(`[router] ${routeEntry.key}: MPP simulation failed \u2014 ${message}`);
1588
- firePluginHook(deps.plugin, "onAlert", pluginCtx, {
1589
- level: "warn",
1590
- message: `MPP simulation failed: ${message}`,
1591
- route: routeEntry.key
1592
- });
1593
- return await build402(request, routeEntry, deps, meta, pluginCtx, body.data);
1594
- }
1595
- pluginCtx.setVerifiedWallet(wallet);
1596
- firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
1597
- protocol: "mpp",
1598
- payer: wallet,
1599
- amount: price,
1600
- network: "tempo:4217"
1601
- });
1602
- const mppRecipient2 = deps.mppRecipient ?? deps.payeeAddress;
1603
- const payment2 = {
1604
- protocol: "mpp",
1605
- status: "verified",
1606
- payer: wallet,
1607
- amount: price,
1608
- network: "tempo:4217",
1609
- ...mppRecipient2 ? { recipient: mppRecipient2 } : {}
1610
- };
1611
- const { response: response2, rawResult: rawResult2, handlerError: handlerError2 } = await invoke(
1612
- request,
1613
- meta,
1614
- pluginCtx,
1615
- wallet,
1616
- account,
1617
- body.data,
1618
- payment2
1529
+ const label = group.facilitator.url ?? group.facilitator.network;
1530
+ const reason = err instanceof Error ? err.message : String(err);
1531
+ console.warn(
1532
+ `[router] ${label} /accepts failed, dropping ${group.items.length} requirement(s): ${reason}`
1619
1533
  );
1620
- const settleScope2 = {
1621
- request,
1622
- meta,
1623
- pluginCtx,
1624
- wallet,
1625
- account,
1626
- parsedBody: body.data,
1627
- payment: payment2,
1628
- response: response2,
1629
- rawResult: rawResult2,
1630
- handlerError: handlerError2
1631
- };
1632
- if (response2.status < 400) {
1633
- const validationFailure = await runBeforeSettle(settleScope2);
1634
- if (validationFailure) return validationFailure;
1635
- let mppResult2;
1636
- try {
1637
- mppResult2 = await deps.mppx.charge({ amount: price })(request);
1638
- } catch (err) {
1639
- await runSettlementError(settleScope2, err, "settle");
1640
- const message = err instanceof Error ? err.message : String(err);
1641
- console.error(
1642
- `[router] ${routeEntry.key}: MPP broadcast failed after handler: ${message}`
1643
- );
1644
- firePluginHook(deps.plugin, "onAlert", pluginCtx, {
1645
- level: "critical",
1646
- message: `MPP broadcast failed after handler: ${message}`,
1647
- route: routeEntry.key
1648
- });
1649
- return fail(
1650
- 500,
1651
- `MPP payment processing failed: ${message}`,
1652
- meta,
1653
- pluginCtx,
1654
- body.data
1655
- );
1656
- }
1657
- if (mppResult2.status === 402) {
1658
- let rejectReason = "";
1659
- try {
1660
- const problemBody = await mppResult2.challenge.clone().text();
1661
- if (problemBody) {
1662
- const problem = JSON.parse(problemBody);
1663
- rejectReason = problem.detail || problem.title || "";
1664
- }
1665
- } catch {
1666
- }
1667
- const detail = rejectReason || "transaction reverted on-chain after handler execution";
1668
- const settlementError = Object.assign(new Error(detail), {
1669
- status: 402,
1670
- detail,
1671
- mppResult: mppResult2,
1672
- challenge: mppResult2.challenge
1673
- });
1674
- await runSettlementError(settleScope2, settlementError, "settle");
1675
- console.error(
1676
- `[router] ${routeEntry.key}: MPP payment failed after handler \u2014 ${detail}`
1677
- );
1678
- firePluginHook(deps.plugin, "onAlert", pluginCtx, {
1679
- level: "critical",
1680
- message: `MPP payment failed after handler: ${detail}`,
1681
- route: routeEntry.key
1682
- });
1683
- return fail(500, `MPP payment failed: ${detail}`, meta, pluginCtx, body.data);
1684
- }
1685
- const receiptResponse = mppResult2.withReceipt(response2);
1686
- receiptResponse.headers.set("Cache-Control", "private");
1687
- let txHash2 = "";
1688
- const receiptHeader2 = receiptResponse.headers.get("Payment-Receipt");
1689
- if (receiptHeader2) {
1690
- try {
1691
- txHash2 = Receipt.deserialize(receiptHeader2).reference;
1692
- } catch {
1693
- }
1694
- }
1695
- if (routeEntry.siwxEnabled) {
1696
- try {
1697
- await deps.entitlementStore.grant(routeEntry.key, wallet);
1698
- } catch (error) {
1699
- firePluginHook(deps.plugin, "onAlert", pluginCtx, {
1700
- level: "warn",
1701
- message: `Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`,
1702
- route: routeEntry.key
1703
- });
1704
- }
1705
- }
1706
- firePluginHook(deps.plugin, "onPaymentSettled", pluginCtx, {
1707
- protocol: "mpp",
1708
- payer: wallet,
1709
- transaction: txHash2,
1710
- network: "tempo:4217"
1711
- });
1712
- const settledPayment = {
1713
- ...payment2,
1714
- status: "settled",
1715
- ...txHash2 ? { transaction: txHash2 } : {},
1716
- ...receiptHeader2 ? { receipt: receiptHeader2 } : {}
1717
- };
1718
- await runAfterSettle({
1719
- ...settleScope2,
1720
- payment: settledPayment,
1721
- response: receiptResponse
1722
- });
1723
- finalize(receiptResponse, rawResult2, meta, pluginCtx, body.data);
1724
- return receiptResponse;
1725
- }
1726
- finalize(response2, rawResult2, meta, pluginCtx, body.data);
1727
- return response2;
1728
- }
1729
- let mppResult;
1730
- try {
1731
- mppResult = await deps.mppx.charge({ amount: price })(request);
1732
- } catch (err) {
1733
- const message = err instanceof Error ? err.message : String(err);
1734
- console.error(`[router] ${routeEntry.key}: MPP charge failed: ${message}`);
1735
- firePluginHook(deps.plugin, "onAlert", pluginCtx, {
1736
- level: "critical",
1737
- message: `MPP charge failed: ${message}`,
1738
- route: routeEntry.key
1739
- });
1740
- return fail(500, `MPP payment processing failed: ${message}`, meta, pluginCtx, body.data);
1534
+ return { success: false, group };
1741
1535
  }
1742
- if (mppResult.status === 402) {
1743
- let rejectReason = "";
1744
- try {
1745
- const problemBody = await mppResult.challenge.clone().text();
1746
- if (problemBody) {
1747
- const problem = JSON.parse(problemBody);
1748
- rejectReason = problem.detail || problem.title || "";
1749
- }
1750
- } catch {
1751
- }
1752
- const detail = rejectReason || "credential may be invalid, or check TEMPO_RPC_URL configuration";
1753
- console.warn(`[router] ${routeEntry.key}: MPP credential rejected \u2014 ${detail}`);
1754
- firePluginHook(deps.plugin, "onAlert", pluginCtx, {
1755
- level: "warn",
1756
- message: `MPP payment rejected: ${detail}`,
1757
- route: routeEntry.key
1758
- });
1759
- return await build402(request, routeEntry, deps, meta, pluginCtx, body.data);
1536
+ })
1537
+ );
1538
+ const enriched = [...requirements];
1539
+ results.filter((r) => r.success).forEach(({ group, accepted }) => {
1540
+ accepted.forEach((req, offset) => {
1541
+ const index = group.items[offset]?.index;
1542
+ if (index !== void 0) enriched[index] = req;
1543
+ });
1544
+ });
1545
+ const failedIndices = new Set(
1546
+ results.filter((r) => !r.success).flatMap(({ group }) => group.items.map(({ index }) => index))
1547
+ );
1548
+ const remaining = enriched.filter((_, i) => !failedIndices.has(i));
1549
+ if (remaining.length === 0) {
1550
+ throw new Error(
1551
+ "All facilitator enrichments failed; no payment requirements remain for challenge"
1552
+ );
1553
+ }
1554
+ return remaining;
1555
+ }
1556
+ function collectEnrichmentGroups(requirements, facilitatorsByNetwork) {
1557
+ const groups = [];
1558
+ requirements.forEach((requirement, index) => {
1559
+ if (!requiresFacilitatorEnrichment(requirement)) return;
1560
+ const facilitator = getRequiredFacilitator(requirement, facilitatorsByNetwork);
1561
+ const existing = groups.find(
1562
+ (group) => sameResolvedX402Facilitator(group.facilitator, facilitator)
1563
+ );
1564
+ if (existing) {
1565
+ existing.items.push({ index, requirement });
1566
+ return;
1567
+ }
1568
+ groups.push({
1569
+ facilitator,
1570
+ items: [{ index, requirement }]
1571
+ });
1572
+ });
1573
+ return groups;
1574
+ }
1575
+ function getRequiredFacilitator(requirement, facilitatorsByNetwork) {
1576
+ const facilitator = getFacilitatorForRequirement(facilitatorsByNetwork, requirement);
1577
+ if (!facilitator) {
1578
+ throw new Error(
1579
+ `Missing x402 facilitator for ${requirement.scheme} requirement on ${requirement.network}`
1580
+ );
1581
+ }
1582
+ return facilitator;
1583
+ }
1584
+ function requiresFacilitatorEnrichment(requirement) {
1585
+ return requirement.scheme !== "exact" || isSolanaRequirement(requirement);
1586
+ }
1587
+ function buildChallengeResource(request, routeEntry) {
1588
+ return {
1589
+ url: request.url,
1590
+ method: routeEntry.method,
1591
+ description: routeEntry.description,
1592
+ mimeType: "application/json"
1593
+ };
1594
+ }
1595
+
1596
+ // src/protocols/x402/settle.ts
1597
+ async function settleX402Payment(server, payload, requirements) {
1598
+ const { encodePaymentResponseHeader } = await import("@x402/core/http");
1599
+ const result = await server.settlePayment(payload, requirements);
1600
+ const encoded = encodePaymentResponseHeader(result);
1601
+ return { encoded, result };
1602
+ }
1603
+
1604
+ // src/protocols/x402/verify.ts
1605
+ async function verifyX402Payment(opts) {
1606
+ const { server, request, price, accepts } = opts;
1607
+ const payload = await readPaymentPayload(request);
1608
+ if (!payload) return null;
1609
+ const requirements = await buildExpectedRequirements(server, request, price, accepts);
1610
+ const matching = findVerifiableRequirements(server, requirements, payload);
1611
+ if (!matching) {
1612
+ return invalidPaymentVerification();
1613
+ }
1614
+ let verify;
1615
+ try {
1616
+ verify = await server.verifyPayment(payload, matching);
1617
+ } catch (err) {
1618
+ const sc = err.statusCode;
1619
+ if (sc && sc >= 400 && sc < 500) return invalidPaymentVerification();
1620
+ throw err;
1621
+ }
1622
+ if (!verify.isValid) return invalidPaymentVerification();
1623
+ if (typeof verify.payer !== "string" || verify.payer.length === 0) {
1624
+ throw new Error("x402 verification succeeded without a payer address");
1625
+ }
1626
+ return {
1627
+ valid: true,
1628
+ payer: verify.payer,
1629
+ payload,
1630
+ requirements: matching
1631
+ };
1632
+ }
1633
+ function findVerifiableRequirements(server, requirements, payload) {
1634
+ const strictMatch = server.findMatchingRequirements(requirements, payload);
1635
+ if (strictMatch) {
1636
+ return payload.x402Version === 2 ? payload.accepted : strictMatch;
1637
+ }
1638
+ if (payload.x402Version !== 2) {
1639
+ return null;
1640
+ }
1641
+ const stableMatch = requirements.find(
1642
+ (requirement) => matchesStableFields(requirement, payload.accepted)
1643
+ );
1644
+ return stableMatch ? payload.accepted : null;
1645
+ }
1646
+ function matchesStableFields(requirement, accepted) {
1647
+ return requirement.scheme === accepted.scheme && requirement.network === accepted.network && requirement.payTo === accepted.payTo && requirement.asset === accepted.asset && requirement.amount === accepted.amount && requirement.maxTimeoutSeconds === accepted.maxTimeoutSeconds;
1648
+ }
1649
+ async function readPaymentPayload(request) {
1650
+ const paymentHeader = request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY);
1651
+ if (!paymentHeader) return null;
1652
+ const { decodePaymentSignatureHeader } = await import("@x402/core/http");
1653
+ return decodePaymentSignatureHeader(paymentHeader);
1654
+ }
1655
+ function invalidPaymentVerification() {
1656
+ return { valid: false, payload: null, requirements: null, payer: null };
1657
+ }
1658
+
1659
+ // src/protocols/x402/strategy.ts
1660
+ var x402Strategy = {
1661
+ protocol: "x402",
1662
+ detects(request) {
1663
+ return Boolean(
1664
+ request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)
1665
+ );
1666
+ },
1667
+ async verify(args) {
1668
+ const { request, body, price, routeEntry, deps } = args;
1669
+ if (!deps.x402Server) {
1670
+ const reason = deps.x402InitError ? `x402 facilitator initialization failed: ${deps.x402InitError}` : "x402 server not initialized \u2014 ensure @x402/core, @x402/evm, and @coinbase/x402 are installed";
1671
+ console.error(`[router] ${routeEntry.key}: ${reason}`);
1672
+ return { ok: false, kind: "config", message: reason };
1673
+ }
1674
+ const accepts = await resolveX402Accepts(
1675
+ request,
1676
+ routeEntry,
1677
+ deps.x402Accepts,
1678
+ deps.payeeAddress,
1679
+ body
1680
+ );
1681
+ const verifyResult = await verifyX402Payment({
1682
+ server: deps.x402Server,
1683
+ request,
1684
+ price,
1685
+ accepts
1686
+ });
1687
+ if (!verifyResult?.valid) return { ok: false, kind: "invalid" };
1688
+ const wallet = normalizeWalletAddress(verifyResult.payer);
1689
+ const matchedNetwork = getRequirementNetwork(verifyResult.requirements, deps.network);
1690
+ const matchedRecipient = getRequirementRecipient(verifyResult.requirements);
1691
+ const payment = {
1692
+ protocol: "x402",
1693
+ status: "verified",
1694
+ payer: wallet,
1695
+ amount: price,
1696
+ network: matchedNetwork,
1697
+ ...matchedRecipient ? { recipient: matchedRecipient } : {}
1698
+ };
1699
+ return {
1700
+ ok: true,
1701
+ wallet,
1702
+ payment,
1703
+ token: {
1704
+ payload: verifyResult.payload,
1705
+ requirements: verifyResult.requirements
1760
1706
  }
1761
- let txHash = "";
1762
- const receiptHeader = mppResult.withReceipt(new Response()).headers.get(
1763
- "Payment-Receipt"
1707
+ };
1708
+ },
1709
+ async settle(args) {
1710
+ const { response, payment, token, deps } = args;
1711
+ const x402Token = token;
1712
+ try {
1713
+ const settle = await settleX402Payment(
1714
+ deps.x402Server,
1715
+ x402Token.payload,
1716
+ x402Token.requirements
1764
1717
  );
1765
- if (receiptHeader) {
1766
- try {
1767
- txHash = Receipt.deserialize(receiptHeader).reference;
1768
- } catch {
1769
- }
1718
+ if (!settle.result?.success) {
1719
+ const reason = settle.result?.errorReason || "x402 settlement returned success=false";
1720
+ const error = new Error(reason);
1721
+ error.errorReason = reason;
1722
+ throw error;
1770
1723
  }
1771
- pluginCtx.setVerifiedWallet(wallet);
1772
- firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
1773
- protocol: "mpp",
1774
- payer: wallet,
1775
- amount: price,
1776
- network: "tempo:4217"
1777
- });
1778
- const mppRecipient = deps.mppRecipient ?? deps.payeeAddress;
1779
- const payment = {
1780
- protocol: "mpp",
1724
+ response.headers.set(HEADERS.X402_PAYMENT_RESPONSE, settle.encoded);
1725
+ response.headers.set("Cache-Control", "private");
1726
+ const transaction = String(settle.result?.transaction ?? "");
1727
+ const settledPayment = {
1728
+ ...payment,
1781
1729
  status: "settled",
1782
- payer: wallet,
1783
- amount: price,
1784
- network: "tempo:4217",
1785
- ...mppRecipient ? { recipient: mppRecipient } : {},
1786
- ...txHash ? { transaction: txHash } : {},
1787
- ...receiptHeader ? { receipt: receiptHeader } : {}
1788
- };
1789
- const { response, rawResult, handlerError } = await invoke(
1790
- request,
1791
- meta,
1792
- pluginCtx,
1793
- wallet,
1794
- account,
1795
- body.data,
1796
- payment
1797
- );
1798
- const settleScope = {
1799
- request,
1800
- meta,
1801
- pluginCtx,
1802
- wallet,
1803
- account,
1804
- parsedBody: body.data,
1805
- payment,
1806
- response,
1807
- rawResult,
1808
- handlerError
1730
+ ...transaction ? { transaction } : {}
1809
1731
  };
1810
- if (response.status < 400) {
1811
- if (routeEntry.siwxEnabled) {
1812
- try {
1813
- await deps.entitlementStore.grant(routeEntry.key, wallet);
1814
- } catch (error) {
1815
- firePluginHook(deps.plugin, "onAlert", pluginCtx, {
1816
- level: "warn",
1817
- message: `Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`,
1818
- route: routeEntry.key
1819
- });
1820
- }
1821
- }
1822
- const receiptResponse = mppResult.withReceipt(response);
1823
- receiptResponse.headers.set("Cache-Control", "private");
1824
- firePluginHook(deps.plugin, "onPaymentSettled", pluginCtx, {
1825
- protocol: "mpp",
1826
- payer: wallet,
1827
- transaction: txHash,
1828
- network: "tempo:4217"
1829
- });
1830
- await runAfterSettle({ ...settleScope, response: receiptResponse });
1831
- finalize(receiptResponse, rawResult, meta, pluginCtx, body.data);
1832
- return receiptResponse;
1833
- }
1834
- await runSettledHandlerError(settleScope);
1835
- finalize(response, rawResult, meta, pluginCtx, body.data);
1836
- return response;
1732
+ return { ok: true, response, settledPayment };
1733
+ } catch (err) {
1734
+ const errObj = err;
1735
+ console.error("Settlement failed", {
1736
+ message: err instanceof Error ? err.message : String(err),
1737
+ route: args.routeEntry.key,
1738
+ network: payment.network,
1739
+ errorReason: errObj.errorReason,
1740
+ facilitatorStatus: errObj.response?.status,
1741
+ facilitatorBody: errObj.response?.data ?? errObj.response?.body
1742
+ });
1743
+ return { ok: false, error: err, failMessage: "Settlement failed" };
1837
1744
  }
1838
- return await build402(request, routeEntry, deps, meta, pluginCtx, body.data);
1839
- };
1745
+ },
1746
+ async buildChallenge(args) {
1747
+ const { request, routeEntry, body, price, extensions, deps } = args;
1748
+ if (!deps.x402Server) return {};
1749
+ const accepts = await resolveX402Accepts(
1750
+ request,
1751
+ routeEntry,
1752
+ deps.x402Accepts,
1753
+ deps.payeeAddress,
1754
+ body
1755
+ );
1756
+ const { encoded } = await buildX402Challenge({
1757
+ server: deps.x402Server,
1758
+ routeEntry,
1759
+ request,
1760
+ price,
1761
+ accepts,
1762
+ facilitatorsByNetwork: deps.x402FacilitatorsByNetwork,
1763
+ extensions
1764
+ });
1765
+ return { headers: { [HEADERS.X402_PAYMENT_REQUIRED]: encoded } };
1766
+ }
1767
+ };
1768
+ function getRequirementNetwork(requirements, fallback) {
1769
+ const network = requirements?.network;
1770
+ return typeof network === "string" ? network : fallback;
1840
1771
  }
1841
- async function parseBody(request, routeEntry) {
1842
- if (!routeEntry.bodySchema) return { ok: true, data: void 0 };
1843
- const raw = await bufferBody(request);
1844
- const result = validateBody(raw, routeEntry.bodySchema);
1845
- if (result.success) return { ok: true, data: result.data };
1846
- return {
1847
- ok: false,
1848
- response: NextResponse2.json(
1849
- { success: false, error: result.error, issues: result.issues },
1850
- { status: 400 }
1851
- )
1852
- };
1772
+ function getRequirementRecipient(requirements) {
1773
+ const payTo = requirements?.payTo;
1774
+ return typeof payTo === "string" ? payTo : void 0;
1775
+ }
1776
+
1777
+ // src/protocols/detect.ts
1778
+ function detectProtocol(request) {
1779
+ if (request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)) {
1780
+ return "x402";
1781
+ }
1782
+ const auth = request.headers.get(HEADERS.AUTHORIZATION);
1783
+ if (auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT)) {
1784
+ return "mpp";
1785
+ }
1786
+ if (request.headers.get(HEADERS.SIWX)) {
1787
+ return "siwx";
1788
+ }
1789
+ return null;
1853
1790
  }
1854
- function buildMeta(request, routeEntry) {
1855
- return {
1856
- requestId: crypto.randomUUID(),
1857
- method: request.method,
1858
- route: routeEntry.key,
1859
- origin: request.headers.get("origin") ?? new URL(request.url).origin,
1860
- referer: request.headers.get("referer"),
1861
- walletAddress: request.headers.get("X-Wallet-Address"),
1862
- clientId: request.headers.get("X-Client-ID"),
1863
- sessionId: request.headers.get("X-Session-ID"),
1864
- contentType: request.headers.get("content-type"),
1865
- headers: Object.fromEntries(request.headers.entries()),
1866
- startTime: Date.now()
1867
- };
1791
+
1792
+ // src/protocols/index.ts
1793
+ var STRATEGIES = {
1794
+ x402: x402Strategy,
1795
+ mpp: mppStrategy
1796
+ };
1797
+ function selectIncomingStrategy(request, allowed) {
1798
+ for (const name of allowed) {
1799
+ const strategy = STRATEGIES[name];
1800
+ if (strategy.detects(request)) return strategy;
1801
+ }
1802
+ return null;
1868
1803
  }
1869
- function parseQuery(request, routeEntry) {
1870
- if (!routeEntry.querySchema) return void 0;
1871
- const params = Object.fromEntries(request.nextUrl.searchParams.entries());
1872
- const result = routeEntry.querySchema.safeParse(params);
1873
- return result.success ? result.data : params;
1804
+ function getAllowedStrategies(allowed) {
1805
+ return allowed.map((name) => STRATEGIES[name]);
1874
1806
  }
1875
- async function resolveDynamicPrice(bodyData, routeEntry, deps, pluginCtx, meta) {
1807
+
1808
+ // src/pipeline/challenge.ts
1809
+ import { NextResponse as NextResponse4 } from "next/server";
1810
+ async function build402(ctx, pricing, body) {
1811
+ let challengePrice;
1876
1812
  try {
1877
- let price = await resolvePrice(routeEntry.pricing, bodyData);
1878
- if (routeEntry.maxPrice) {
1879
- const calculated = parseFloat(price);
1880
- const max = parseFloat(routeEntry.maxPrice);
1881
- if (!Number.isFinite(calculated) || calculated > max) {
1882
- firePluginHook(deps.plugin, "onAlert", pluginCtx, {
1883
- level: "warn",
1884
- message: `Price ${price} exceeds maxPrice ${routeEntry.maxPrice}, capping`,
1885
- route: routeEntry.key,
1886
- meta: { calculated: price, maxPrice: routeEntry.maxPrice, body: bodyData }
1887
- });
1888
- price = routeEntry.maxPrice;
1889
- }
1890
- }
1891
- return { price };
1813
+ challengePrice = pricing ? await pricing.challengeQuote(body) : "0";
1892
1814
  } catch (err) {
1893
- firePluginHook(deps.plugin, "onAlert", pluginCtx, {
1894
- level: "error",
1895
- message: `Pricing function failed: ${err instanceof Error ? err.message : String(err)}`,
1896
- route: routeEntry.key,
1897
- meta: { error: err instanceof Error ? err.stack : String(err), body: bodyData }
1898
- });
1899
- if (routeEntry.maxPrice) {
1900
- firePluginHook(deps.plugin, "onAlert", pluginCtx, {
1901
- level: "warn",
1902
- message: `Using maxPrice ${routeEntry.maxPrice} as fallback after pricing error`,
1903
- route: routeEntry.key
1904
- });
1905
- return { price: routeEntry.maxPrice };
1906
- } else {
1907
- const message = errorMessage(err, "Price calculation failed");
1908
- const errorResponse = NextResponse2.json(
1909
- { success: false, error: message },
1910
- { status: errorStatus(err, 500) }
1911
- );
1912
- firePluginResponse(deps, pluginCtx, meta, errorResponse);
1913
- return { error: errorResponse };
1914
- }
1815
+ const message = errorMessage(err, "Price calculation failed");
1816
+ const errorResponse = NextResponse4.json(
1817
+ { success: false, error: message },
1818
+ { status: errorStatus(err, 500) }
1819
+ );
1820
+ firePluginResponse(ctx, errorResponse);
1821
+ return errorResponse;
1915
1822
  }
1916
- }
1917
- async function build402(request, routeEntry, deps, meta, pluginCtx, bodyData) {
1918
- const response = new NextResponse2(null, {
1823
+ const extensions = await buildChallengeExtensions(ctx);
1824
+ const response = new NextResponse4(null, {
1919
1825
  status: 402,
1920
1826
  headers: {
1921
1827
  "Content-Type": "application/json",
1922
1828
  "Cache-Control": "no-store"
1923
1829
  }
1924
1830
  });
1925
- let challengePrice;
1926
- if (bodyData !== void 0 && typeof routeEntry.pricing !== "string" && routeEntry.pricing != null) {
1927
- const result = await resolveDynamicPrice(bodyData, routeEntry, deps, pluginCtx, meta);
1928
- if ("error" in result) return result.error;
1929
- challengePrice = result.price;
1930
- } else if (routeEntry.maxPrice) {
1931
- challengePrice = routeEntry.maxPrice;
1932
- } else if (routeEntry.pricing) {
1831
+ for (const strategy of getAllowedStrategies(ctx.routeEntry.protocols)) {
1933
1832
  try {
1934
- challengePrice = resolveMaxPrice(routeEntry.pricing);
1935
- } catch {
1936
- challengePrice = "0";
1833
+ const contribution = await strategy.buildChallenge({
1834
+ request: ctx.request,
1835
+ routeEntry: ctx.routeEntry,
1836
+ body,
1837
+ price: challengePrice,
1838
+ extensions,
1839
+ deps: ctx.deps
1840
+ });
1841
+ if (contribution.headers) {
1842
+ for (const [name, value] of Object.entries(contribution.headers)) {
1843
+ response.headers.set(name, value);
1844
+ }
1845
+ }
1846
+ } catch (err) {
1847
+ const message = `${strategy.protocol} challenge build failed: ${errorMessage(err, String(err))}`;
1848
+ firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
1849
+ level: "critical",
1850
+ message,
1851
+ route: ctx.routeEntry.key
1852
+ });
1853
+ if (strategy.protocol === "x402") {
1854
+ const errorResponse = NextResponse4.json(
1855
+ { success: false, error: message },
1856
+ { status: 500 }
1857
+ );
1858
+ firePluginResponse(ctx, errorResponse);
1859
+ return errorResponse;
1860
+ }
1937
1861
  }
1938
- } else {
1939
- challengePrice = "0";
1940
1862
  }
1863
+ firePluginResponse(ctx, response);
1864
+ return response;
1865
+ }
1866
+ async function buildChallengeExtensions(ctx) {
1867
+ const { routeEntry } = ctx;
1941
1868
  let extensions;
1942
1869
  try {
1943
1870
  const { z } = await import("zod");
@@ -1963,7 +1890,7 @@ async function build402(request, routeEntry, deps, meta, pluginCtx, bodyData) {
1963
1890
  extensions = declareDiscoveryExtension(config);
1964
1891
  }
1965
1892
  } catch (err) {
1966
- firePluginHook(deps.plugin, "onAlert", pluginCtx, {
1893
+ firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
1967
1894
  level: "warn",
1968
1895
  message: `Bazaar schema generation failed: ${err instanceof Error ? err.message : String(err)}`,
1969
1896
  route: routeEntry.key
@@ -1981,101 +1908,390 @@ async function build402(request, routeEntry, deps, meta, pluginCtx, bodyData) {
1981
1908
  } catch {
1982
1909
  }
1983
1910
  }
1984
- if (routeEntry.protocols.includes("x402") && deps.x402Server) {
1985
- try {
1986
- const accepts = await resolveX402Accepts(
1987
- request,
1988
- routeEntry,
1989
- deps.x402Accepts,
1990
- deps.payeeAddress,
1991
- bodyData
1992
- );
1993
- const { encoded } = await buildX402Challenge({
1994
- server: deps.x402Server,
1995
- routeEntry,
1996
- request,
1997
- price: challengePrice,
1998
- accepts,
1999
- facilitatorsByNetwork: deps.x402FacilitatorsByNetwork,
2000
- extensions
2001
- });
2002
- response.headers.set("PAYMENT-REQUIRED", encoded);
2003
- } catch (err) {
2004
- const message = `x402 challenge build failed: ${err instanceof Error ? err.message : String(err)}`;
2005
- firePluginHook(deps.plugin, "onAlert", pluginCtx, {
1911
+ return extensions;
1912
+ }
1913
+
1914
+ // src/pipeline/flows/paid.ts
1915
+ async function runPaidFlow(ctx) {
1916
+ const { request, routeEntry, deps } = ctx;
1917
+ let account = void 0;
1918
+ if (routeEntry.apiKeyResolver) {
1919
+ const apiKeyResult = await verifyApiKey(request, routeEntry.apiKeyResolver);
1920
+ if (!apiKeyResult.valid) return fail(ctx, 401, "Invalid or missing API key");
1921
+ account = apiKeyResult.account;
1922
+ firePluginHook(deps.plugin, "onAuthVerified", ctx.pluginCtx, {
1923
+ authMode: "apiKey",
1924
+ wallet: null,
1925
+ route: routeEntry.key,
1926
+ account
1927
+ });
1928
+ }
1929
+ const alertFn = (level, message, meta) => {
1930
+ firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
1931
+ level,
1932
+ message,
1933
+ route: routeEntry.key,
1934
+ meta
1935
+ });
1936
+ };
1937
+ const pricing = selectPricing(routeEntry.pricing, {
1938
+ alert: alertFn,
1939
+ maxPrice: routeEntry.maxPrice,
1940
+ minPrice: routeEntry.minPrice,
1941
+ route: routeEntry.key
1942
+ });
1943
+ const incomingStrategy = selectIncomingStrategy(request, routeEntry.protocols);
1944
+ let earlyBody = void 0;
1945
+ if (shouldParseBodyEarly(incomingStrategy, routeEntry, pricing)) {
1946
+ const earlyClone = request.clone();
1947
+ const earlyResult = await parseBody(earlyClone, routeEntry);
1948
+ if (earlyResult.ok) {
1949
+ earlyBody = earlyResult.data;
1950
+ const validateErr2 = await runValidate(ctx, earlyBody);
1951
+ if (validateErr2) return validateErr2;
1952
+ } else {
1953
+ firePluginResponse(ctx, earlyResult.response);
1954
+ return earlyResult.response;
1955
+ }
1956
+ }
1957
+ const siwxFastPath = await trySiwxFastPath(ctx, account);
1958
+ if (siwxFastPath) return siwxFastPath;
1959
+ if (!incomingStrategy) {
1960
+ const initError = protocolInitError(routeEntry, deps);
1961
+ if (initError) return fail(ctx, 500, initError);
1962
+ return build402(ctx, pricing, earlyBody);
1963
+ }
1964
+ const body = await parseBody(request, routeEntry);
1965
+ if (!body.ok) {
1966
+ firePluginResponse(ctx, body.response);
1967
+ return body.response;
1968
+ }
1969
+ const validateErr = await runValidate(ctx, body.data);
1970
+ if (validateErr) return validateErr;
1971
+ if (!pricing) {
1972
+ return fail(ctx, 500, "Pricing not configured", body.data);
1973
+ }
1974
+ let price;
1975
+ try {
1976
+ price = await pricing.quote(body.data);
1977
+ } catch (err) {
1978
+ return fail(
1979
+ ctx,
1980
+ errorStatus(err, 500),
1981
+ errorMessage(err, "Price calculation failed"),
1982
+ body.data
1983
+ );
1984
+ }
1985
+ const verifyOutcome = await incomingStrategy.verify({
1986
+ request,
1987
+ body: body.data,
1988
+ price,
1989
+ routeEntry,
1990
+ deps
1991
+ });
1992
+ if (verifyOutcome.ok === false) {
1993
+ if (verifyOutcome.kind === "config") {
1994
+ return fail(ctx, 500, verifyOutcome.message, body.data);
1995
+ }
1996
+ return build402(ctx, pricing, body.data);
1997
+ }
1998
+ ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
1999
+ firePluginHook(deps.plugin, "onPaymentVerified", ctx.pluginCtx, {
2000
+ protocol: incomingStrategy.protocol,
2001
+ payer: verifyOutcome.wallet,
2002
+ amount: price,
2003
+ network: verifyOutcome.payment.network
2004
+ });
2005
+ const result = await invoke(ctx, verifyOutcome.wallet, account, body.data, verifyOutcome.payment);
2006
+ const settleScope = {
2007
+ wallet: verifyOutcome.wallet,
2008
+ account,
2009
+ body: body.data,
2010
+ payment: verifyOutcome.payment,
2011
+ response: result.response,
2012
+ rawResult: result.rawResult,
2013
+ handlerError: result.handlerError
2014
+ };
2015
+ if (verifyOutcome.alreadySettled) {
2016
+ if (result.response.status >= 400) {
2017
+ const settledScope = settleScope;
2018
+ await runSettledHandlerError(ctx, settledScope);
2019
+ return finalize(ctx, result.response, result.rawResult, body.data);
2020
+ }
2021
+ return settleAndFinalize({
2022
+ ctx,
2023
+ strategy: incomingStrategy,
2024
+ verifyOutcome,
2025
+ scope: settleScope,
2026
+ rawResult: result.rawResult,
2027
+ body: body.data
2028
+ });
2029
+ }
2030
+ if (result.response.status >= 400) {
2031
+ return finalize(ctx, result.response, result.rawResult, body.data);
2032
+ }
2033
+ const beforeErr = await runBeforeSettle(ctx, settleScope);
2034
+ if (beforeErr) return beforeErr;
2035
+ return settleAndFinalize({
2036
+ ctx,
2037
+ strategy: incomingStrategy,
2038
+ verifyOutcome,
2039
+ scope: settleScope,
2040
+ rawResult: result.rawResult,
2041
+ body: body.data,
2042
+ onSettleError: async (error, failMessage) => {
2043
+ await runSettlementError(ctx, settleScope, error, "settle");
2044
+ firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
2006
2045
  level: "critical",
2007
- message,
2046
+ message: `${incomingStrategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`,
2008
2047
  route: routeEntry.key
2009
2048
  });
2010
- const errorResponse = NextResponse2.json({ success: false, error: message }, { status: 500 });
2011
- firePluginResponse(deps, pluginCtx, meta, errorResponse);
2012
- return errorResponse;
2049
+ }
2050
+ });
2051
+ }
2052
+
2053
+ // src/pipeline/flows/siwx-only.ts
2054
+ import { NextResponse as NextResponse5 } from "next/server";
2055
+
2056
+ // src/auth/nonce.ts
2057
+ var SIWX_CHALLENGE_EXPIRY_MS = 5 * 60 * 1e3;
2058
+ var MemoryNonceStore = class {
2059
+ seen = /* @__PURE__ */ new Map();
2060
+ async check(nonce) {
2061
+ this.evict();
2062
+ if (this.seen.has(nonce)) return false;
2063
+ this.seen.set(nonce, Date.now() + SIWX_CHALLENGE_EXPIRY_MS);
2064
+ return true;
2065
+ }
2066
+ evict() {
2067
+ const now = Date.now();
2068
+ for (const [n, exp] of this.seen) {
2069
+ if (exp < now) this.seen.delete(n);
2013
2070
  }
2014
2071
  }
2015
- if (routeEntry.protocols.includes("mpp") && deps.mppx) {
2016
- try {
2017
- const result = await deps.mppx.charge({ amount: challengePrice })(request);
2018
- if (result.status === 402) {
2019
- const wwwAuth = result.challenge.headers.get("WWW-Authenticate");
2020
- if (wwwAuth) response.headers.set("WWW-Authenticate", wwwAuth);
2072
+ };
2073
+ function detectRedisClientType(client) {
2074
+ if (!client || typeof client !== "object") {
2075
+ throw new Error(
2076
+ "createRedisNonceStore requires a Redis client. Supported: @upstash/redis, ioredis. Pass your Redis client instance as the first argument."
2077
+ );
2078
+ }
2079
+ if ("options" in client && "status" in client) {
2080
+ return "ioredis";
2081
+ }
2082
+ const constructor = client.constructor?.name;
2083
+ if (constructor === "Redis" && "url" in client) {
2084
+ return "upstash";
2085
+ }
2086
+ if (typeof client.set === "function") {
2087
+ return "upstash";
2088
+ }
2089
+ throw new Error(
2090
+ "Unrecognized Redis client. Supported: @upstash/redis, ioredis. If using a different client, implement NonceStore interface directly."
2091
+ );
2092
+ }
2093
+ function createRedisNonceStore(client, opts) {
2094
+ const prefix = opts?.prefix ?? "siwx:nonce:";
2095
+ const ttlSeconds = Math.ceil((opts?.ttlMs ?? SIWX_CHALLENGE_EXPIRY_MS) / 1e3);
2096
+ const clientType = detectRedisClientType(client);
2097
+ return {
2098
+ async check(nonce) {
2099
+ const key = `${prefix}${nonce}`;
2100
+ if (clientType === "upstash") {
2101
+ const redis = client;
2102
+ const result = await redis.set(key, "1", { ex: ttlSeconds, nx: true });
2103
+ return result !== null;
2021
2104
  }
2105
+ if (clientType === "ioredis") {
2106
+ const redis = client;
2107
+ const result = await redis.set(key, "1", "EX", ttlSeconds, "NX");
2108
+ return result === "OK";
2109
+ }
2110
+ throw new Error("Unknown Redis client type");
2111
+ }
2112
+ };
2113
+ }
2114
+
2115
+ // src/protocols/mpp/siwx-mode.ts
2116
+ import { Credential as Credential2 } from "mppx";
2117
+ async function verifyMppSiwx(request, mppx) {
2118
+ const result = await mppx.charge({ amount: "0" })(request);
2119
+ if (result.status === 402) {
2120
+ return { valid: false, challenge: result.challenge };
2121
+ }
2122
+ const credential = Credential2.fromRequest(request);
2123
+ const rawSource = credential?.source ?? "";
2124
+ const wallet = walletFromDid(rawSource);
2125
+ return { valid: true, wallet, withReceipt: result.withReceipt };
2126
+ }
2127
+
2128
+ // src/pipeline/flows/siwx-only.ts
2129
+ async function runSiwxOnlyFlow(ctx) {
2130
+ const { request, routeEntry, deps } = ctx;
2131
+ if (routeEntry.validateFn && routeEntry.bodySchema && !request.headers.get(HEADERS.SIWX)) {
2132
+ const earlyClone = request.clone();
2133
+ const earlyBody = await parseBody(earlyClone, routeEntry);
2134
+ if (earlyBody.ok) {
2135
+ const validateErr = await runValidate(ctx, earlyBody.data);
2136
+ if (validateErr) return validateErr;
2137
+ } else {
2138
+ firePluginResponse(ctx, earlyBody.response);
2139
+ return earlyBody.response;
2140
+ }
2141
+ }
2142
+ const siwxHeader = request.headers.get(HEADERS.SIWX);
2143
+ const protocol = detectProtocol(request);
2144
+ if (!siwxHeader && protocol === "mpp" && deps.mppx) {
2145
+ let mppSiwxResult;
2146
+ try {
2147
+ mppSiwxResult = await verifyMppSiwx(request, deps.mppx);
2022
2148
  } catch (err) {
2023
- firePluginHook(deps.plugin, "onAlert", pluginCtx, {
2149
+ const message = err instanceof Error ? err.message : String(err);
2150
+ firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
2024
2151
  level: "critical",
2025
- message: `MPP challenge build failed: ${err instanceof Error ? err.message : String(err)}`,
2152
+ message: `MPP SIWX verification failed: ${message}`,
2153
+ route: routeEntry.key
2154
+ });
2155
+ return fail(ctx, 500, `MPP SIWX verification failed: ${message}`);
2156
+ }
2157
+ if (mppSiwxResult.valid) {
2158
+ ctx.pluginCtx.setVerifiedWallet(mppSiwxResult.wallet);
2159
+ firePluginHook(deps.plugin, "onAuthVerified", ctx.pluginCtx, {
2160
+ authMode: "siwx",
2161
+ wallet: mppSiwxResult.wallet,
2026
2162
  route: routeEntry.key
2027
2163
  });
2164
+ const authResponse = await runHandlerOnly(ctx, mppSiwxResult.wallet, void 0);
2165
+ if (authResponse.status < 400) {
2166
+ return mppSiwxResult.withReceipt(authResponse);
2167
+ }
2168
+ return authResponse;
2028
2169
  }
2029
2170
  }
2030
- firePluginResponse(deps, pluginCtx, meta, response);
2031
- return response;
2032
- }
2033
- function firePluginResponse(deps, pluginCtx, meta, response, requestBody, responseBody) {
2034
- firePluginHook(deps.plugin, "onResponse", pluginCtx, {
2035
- statusCode: response.status,
2036
- statusText: response.statusText,
2037
- duration: Date.now() - meta.startTime,
2038
- contentType: response.headers.get("content-type"),
2039
- headers: Object.fromEntries(response.headers.entries()),
2040
- requestBody,
2041
- responseBody
2171
+ if (!siwxHeader) {
2172
+ return buildSiwxChallenge(ctx);
2173
+ }
2174
+ const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
2175
+ if (!siwx.valid) {
2176
+ const response = NextResponse5.json(
2177
+ { error: siwx.code, message: SIWX_ERROR_MESSAGES[siwx.code] },
2178
+ { status: 402 }
2179
+ );
2180
+ firePluginResponse(ctx, response);
2181
+ return response;
2182
+ }
2183
+ const wallet = normalizeWalletAddress(siwx.wallet);
2184
+ ctx.pluginCtx.setVerifiedWallet(wallet);
2185
+ firePluginHook(deps.plugin, "onAuthVerified", ctx.pluginCtx, {
2186
+ authMode: "siwx",
2187
+ wallet,
2188
+ route: routeEntry.key
2042
2189
  });
2043
- if (response.status >= 400 && response.status !== 402) {
2044
- firePluginHook(deps.plugin, "onError", pluginCtx, {
2045
- status: response.status,
2046
- message: response.statusText || `HTTP ${response.status}`,
2047
- settled: false
2190
+ return runHandlerOnly(ctx, wallet, void 0);
2191
+ }
2192
+ async function buildSiwxChallenge(ctx) {
2193
+ const { request, routeEntry, deps } = ctx;
2194
+ const url = new URL(request.url);
2195
+ const nonce = crypto.randomUUID().replace(/-/g, "");
2196
+ const supportedChains = getSupportedChains(deps.x402Accepts, deps.network);
2197
+ const primaryChain = supportedChains[0];
2198
+ const siwxInfo = {
2199
+ domain: url.hostname,
2200
+ uri: request.url,
2201
+ version: "1",
2202
+ chainId: primaryChain.chainId,
2203
+ type: primaryChain.type,
2204
+ nonce,
2205
+ issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
2206
+ expirationTime: new Date(Date.now() + SIWX_CHALLENGE_EXPIRY_MS).toISOString(),
2207
+ statement: "Sign in to verify your wallet identity"
2208
+ };
2209
+ let siwxSchema;
2210
+ try {
2211
+ siwxSchema = await buildSIWXExtension();
2212
+ } catch {
2213
+ }
2214
+ const paymentRequired = {
2215
+ x402Version: 2,
2216
+ error: "SIWX authentication required",
2217
+ resource: {
2218
+ url: request.url,
2219
+ description: routeEntry.description ?? "SIWX-protected endpoint",
2220
+ mimeType: "application/json"
2221
+ },
2222
+ accepts: [],
2223
+ extensions: {
2224
+ "sign-in-with-x": {
2225
+ info: siwxInfo,
2226
+ // Required by MCP tools at the top level for chain detection.
2227
+ supportedChains,
2228
+ ...siwxSchema ? { schema: siwxSchema } : {}
2229
+ }
2230
+ }
2231
+ };
2232
+ let encoded;
2233
+ try {
2234
+ const { encodePaymentRequiredHeader } = await import("@x402/core/http");
2235
+ encoded = encodePaymentRequiredHeader(paymentRequired);
2236
+ } catch (err) {
2237
+ firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
2238
+ level: "warn",
2239
+ message: `SIWX challenge header encoding failed: ${err instanceof Error ? err.message : String(err)}`,
2240
+ route: routeEntry.key
2048
2241
  });
2049
2242
  }
2243
+ const response = new NextResponse5(JSON.stringify(paymentRequired), {
2244
+ status: 402,
2245
+ headers: { "Content-Type": "application/json", "Cache-Control": "no-store" }
2246
+ });
2247
+ if (encoded) response.headers.set(HEADERS.X402_PAYMENT_REQUIRED, encoded);
2248
+ if (deps.mppx) {
2249
+ try {
2250
+ const mppChallenge = await deps.mppx.charge({ amount: "0" })(request);
2251
+ if (mppChallenge.status === 402) {
2252
+ const wwwAuth = mppChallenge.challenge.headers.get(HEADERS.WWW_AUTHENTICATE);
2253
+ if (wwwAuth) response.headers.set(HEADERS.WWW_AUTHENTICATE, wwwAuth);
2254
+ }
2255
+ } catch {
2256
+ }
2257
+ }
2258
+ firePluginResponse(ctx, response);
2259
+ return response;
2050
2260
  }
2051
- function computeQuotaLevel(remaining, warn, critical) {
2052
- if (remaining === null) return "healthy";
2053
- if (critical !== void 0 && remaining <= critical) return "critical";
2054
- if (warn !== void 0 && remaining <= warn) return "warn";
2055
- return "healthy";
2261
+ function siwxSignatureType(network) {
2262
+ return network.startsWith("solana:") ? "ed25519" : "eip191";
2056
2263
  }
2057
- function fireProviderQuota(routeEntry, response, handlerResult, deps, pluginCtx) {
2058
- const { providerName, providerConfig } = routeEntry;
2059
- if (!providerName || !providerConfig?.extractQuota) return;
2060
- if (response.status >= 400) return;
2061
- try {
2062
- const quota = providerConfig.extractQuota(handlerResult, response.headers);
2063
- if (!quota) return;
2064
- const level = computeQuotaLevel(quota.remaining, providerConfig.warn, providerConfig.critical);
2065
- const overage = providerConfig.overage ?? "same-rate";
2066
- const event = {
2067
- provider: providerName,
2068
- route: routeEntry.key,
2069
- remaining: quota.remaining,
2070
- limit: quota.limit,
2071
- spend: quota.spend,
2072
- level,
2073
- overage,
2074
- message: quota.remaining !== null ? `${providerName}: ${quota.remaining}${quota.limit ? `/${quota.limit}` : ""} remaining` : `${providerName}: quota info unavailable`
2075
- };
2076
- firePluginHook(deps.plugin, "onProviderQuota", pluginCtx, event);
2077
- } catch {
2264
+ function getSupportedChains(x402Accepts, fallbackNetwork) {
2265
+ const seen = /* @__PURE__ */ new Set();
2266
+ const chains = [];
2267
+ for (const accept of x402Accepts) {
2268
+ if (accept.network && !seen.has(accept.network)) {
2269
+ seen.add(accept.network);
2270
+ chains.push({ chainId: accept.network, type: siwxSignatureType(accept.network) });
2271
+ }
2078
2272
  }
2273
+ if (chains.length === 0) {
2274
+ chains.push({ chainId: fallbackNetwork, type: siwxSignatureType(fallbackNetwork) });
2275
+ }
2276
+ return chains;
2277
+ }
2278
+
2279
+ // src/pipeline/flows/unprotected.ts
2280
+ async function runUnprotectedFlow(ctx) {
2281
+ return runHandlerOnly(ctx, null, void 0);
2282
+ }
2283
+
2284
+ // src/orchestrate.ts
2285
+ function createRequestHandler(routeEntry, handler, deps) {
2286
+ return async (request) => {
2287
+ await deps.initPromise;
2288
+ const ctx = preflight(routeEntry, handler, deps, request);
2289
+ if (routeEntry.authMode === "unprotected") return runUnprotectedFlow(ctx);
2290
+ if (routeEntry.authMode === "siwx") return runSiwxOnlyFlow(ctx);
2291
+ if (routeEntry.pricing) return runPaidFlow(ctx);
2292
+ if (routeEntry.apiKeyResolver) return runApiKeyOnlyFlow(ctx);
2293
+ return runUnprotectedFlow(ctx);
2294
+ };
2079
2295
  }
2080
2296
 
2081
2297
  // src/validate-examples.ts
@@ -2550,7 +2766,7 @@ function createRedisEntitlementStore(client, options) {
2550
2766
  }
2551
2767
 
2552
2768
  // src/discovery/well-known.ts
2553
- import { NextResponse as NextResponse3 } from "next/server";
2769
+ import { NextResponse as NextResponse6 } from "next/server";
2554
2770
 
2555
2771
  // src/discovery/utils/guidance.ts
2556
2772
  async function resolveGuidance(discovery) {
@@ -2594,7 +2810,7 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, discovery) {
2594
2810
  if (instructions) {
2595
2811
  body.instructions = instructions;
2596
2812
  }
2597
- return NextResponse3.json(body, {
2813
+ return NextResponse6.json(body, {
2598
2814
  headers: {
2599
2815
  "Access-Control-Allow-Origin": "*",
2600
2816
  "Access-Control-Allow-Methods": "GET",
@@ -2612,13 +2828,13 @@ function toDiscoveryResource(method, url, mode) {
2612
2828
 
2613
2829
  // src/discovery/openapi.ts
2614
2830
  init_constants();
2615
- import { NextResponse as NextResponse4 } from "next/server";
2831
+ import { NextResponse as NextResponse7 } from "next/server";
2616
2832
  function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
2617
2833
  const normalizedBase = baseUrl.replace(/\/+$/, "");
2618
2834
  let cached = null;
2619
2835
  let validated = false;
2620
2836
  return async (_request) => {
2621
- if (cached) return NextResponse4.json(cached);
2837
+ if (cached) return NextResponse7.json(cached);
2622
2838
  if (!validated && pricesKeys) {
2623
2839
  registry.validate(pricesKeys);
2624
2840
  validated = true;
@@ -2643,14 +2859,14 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
2643
2859
  securitySchemes.siwx = {
2644
2860
  type: "apiKey",
2645
2861
  in: "header",
2646
- name: "SIGN-IN-WITH-X"
2862
+ name: HEADERS.SIWX
2647
2863
  };
2648
2864
  }
2649
2865
  if (requiresApiKeyScheme) {
2650
2866
  securitySchemes.apiKey = {
2651
2867
  type: "apiKey",
2652
2868
  in: "header",
2653
- name: "X-API-Key"
2869
+ name: HEADERS.API_KEY
2654
2870
  };
2655
2871
  }
2656
2872
  const discoveryMetadata = {};
@@ -2681,7 +2897,7 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
2681
2897
  paths
2682
2898
  };
2683
2899
  cached = createDocument(openApiDocument);
2684
- return NextResponse4.json(cached);
2900
+ return NextResponse7.json(cached);
2685
2901
  };
2686
2902
  }
2687
2903
  function deriveTag(routeKey) {
@@ -2804,11 +3020,11 @@ function buildPricingInfo(entry) {
2804
3020
  }
2805
3021
 
2806
3022
  // src/discovery/llms-txt.ts
2807
- import { NextResponse as NextResponse5 } from "next/server";
3023
+ import { NextResponse as NextResponse8 } from "next/server";
2808
3024
  function createLlmsTxtHandler(discovery) {
2809
3025
  return async (_request) => {
2810
3026
  const guidance = await resolveGuidance(discovery) ?? "";
2811
- return new NextResponse5(guidance, {
3027
+ return new NextResponse8(guidance, {
2812
3028
  headers: {
2813
3029
  "Content-Type": "text/plain; charset=utf-8",
2814
3030
  "Access-Control-Allow-Origin": "*"
@@ -2818,14 +3034,14 @@ function createLlmsTxtHandler(discovery) {
2818
3034
  }
2819
3035
 
2820
3036
  // src/index.ts
2821
- init_x402_config();
3037
+ init_accepts();
2822
3038
  init_constants();
2823
3039
 
2824
3040
  // src/config.ts
2825
3041
  init_constants();
2826
3042
  init_evm();
2827
3043
  init_solana();
2828
- init_x402_config();
3044
+ init_accepts();
2829
3045
  var RouterConfigError = class extends Error {
2830
3046
  issues;
2831
3047
  constructor(issues) {