@agentcash/router 1.10.5 → 1.10.7

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/README.md CHANGED
@@ -150,6 +150,21 @@ The barrel forces every route module to load before the discovery handler walks
150
150
 
151
151
  The `openapi.json` should be hosted at `GET <origin>/openapi.json`.
152
152
 
153
+ ### 4. Unmatched route fallback
154
+
155
+ ```typescript
156
+ // app/api/[[...path]]/route.ts
157
+ import { router } from '@/lib/router';
158
+
159
+ export const GET = router.notFound();
160
+ export const POST = router.notFound();
161
+ export const DELETE = router.notFound();
162
+ export const PUT = router.notFound();
163
+ export const PATCH = router.notFound();
164
+ ```
165
+
166
+ This catches stale agent calls to API paths that no longer exist and returns a JSON 404 telling the client to rediscover the origin.
167
+
153
168
  ## Auth modes
154
169
 
155
170
  | Method | Purpose |
package/dist/index.cjs CHANGED
@@ -542,7 +542,8 @@ var HEADERS = {
542
542
  X402_PAYMENT_REQUIRED: "PAYMENT-REQUIRED",
543
543
  X402_PAYMENT_RESPONSE: "PAYMENT-RESPONSE",
544
544
  MPP_PAYMENT_RECEIPT: "Payment-Receipt",
545
- REQUEST_ID: "X-Request-ID"
545
+ REQUEST_ID: "X-Request-ID",
546
+ AGENT_IDENTITY: "X-Agent-Identity"
546
547
  };
547
548
  var AUTH_SCHEME = {
548
549
  BEARER: "Bearer ",
@@ -677,6 +678,7 @@ function firePaymentSettled(ctx, event) {
677
678
  }
678
679
  function firePluginResponse(ctx, response, requestBody, responseBody, failure) {
679
680
  attachRequestId(response, ctx.meta.requestId);
681
+ const error = response.status >= 400 && response.status !== 402 ? buildErrorEvent(ctx, response, failure) : void 0;
680
682
  firePluginHook(ctx.deps.plugin, "onResponse", ctx.pluginCtx, {
681
683
  statusCode: response.status,
682
684
  statusText: response.statusText,
@@ -684,10 +686,10 @@ function firePluginResponse(ctx, response, requestBody, responseBody, failure) {
684
686
  contentType: response.headers.get("content-type"),
685
687
  headers: Object.fromEntries(response.headers.entries()),
686
688
  requestBody,
687
- responseBody
689
+ responseBody,
690
+ error
688
691
  });
689
- if (response.status >= 400 && response.status !== 402) {
690
- const error = buildErrorEvent(ctx, response, failure);
692
+ if (error) {
691
693
  if (response.status >= 500) logRouterFailure(error);
692
694
  firePluginHook(ctx.deps.plugin, "onError", ctx.pluginCtx, error);
693
695
  }
@@ -860,6 +862,9 @@ async function runValidate(ctx, body) {
860
862
  // src/pipeline/flows/static/static-invoke.ts
861
863
  var import_server4 = require("next/server");
862
864
 
865
+ // src/auth/agent-identity.ts
866
+ var import_did_auth_challenge = require("did-auth-challenge");
867
+
863
868
  // src/types.ts
864
869
  var HttpError = class extends Error {
865
870
  constructor(message, status) {
@@ -869,18 +874,87 @@ var HttpError = class extends Error {
869
874
  }
870
875
  };
871
876
 
872
- // src/pipeline/flows/static/static-invoke.ts
873
- function invokePaidStatic(ctx, wallet, account, body, payment) {
874
- return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, payment));
877
+ // src/auth/agent-identity.ts
878
+ var PROTOCOL_VERSION = 1;
879
+ function base64urlEncode(value) {
880
+ return Buffer.from(JSON.stringify(value)).toString("base64url");
881
+ }
882
+ function base64urlDecode(value) {
883
+ return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
884
+ }
885
+ function challengeBindings(request) {
886
+ const url = new URL(request.url);
887
+ return { domain: url.hostname, route: url.pathname };
888
+ }
889
+ async function buildAgentIdentityChallengeHeader(request, nonceStore) {
890
+ const bindings = challengeBindings(request);
891
+ const challenge = await (0, import_did_auth_challenge.createChallenge)(bindings, { nonceStore });
892
+ return base64urlEncode({
893
+ v: PROTOCOL_VERSION,
894
+ challenge
895
+ });
875
896
  }
876
- function invokeUnauthed(ctx, wallet, account, body) {
877
- const base = buildHandlerCtx(ctx, wallet, account, body, null);
897
+ async function attachAgentIdentityChallenge(response, request, nonceStore) {
898
+ response.headers.set(
899
+ HEADERS.AGENT_IDENTITY,
900
+ await buildAgentIdentityChallengeHeader(request, nonceStore)
901
+ );
902
+ }
903
+ async function resolveActor(request, nonceStore) {
904
+ const header = request.headers.get(HEADERS.AGENT_IDENTITY);
905
+ if (!header) return null;
906
+ let envelope;
907
+ try {
908
+ envelope = base64urlDecode(header);
909
+ } catch {
910
+ throw new HttpError("Malformed X-Agent-Identity header", 401);
911
+ }
912
+ if (envelope.v !== PROTOCOL_VERSION || !envelope.challenge || !envelope.did || !envelope.signature) {
913
+ throw new HttpError("Invalid X-Agent-Identity proof", 401);
914
+ }
915
+ const bindings = challengeBindings(request);
916
+ try {
917
+ await (0, import_did_auth_challenge.verifySignature)({
918
+ did: envelope.did,
919
+ authenticationKey: envelope.authenticationKey,
920
+ challenge: envelope.challenge,
921
+ signature: envelope.signature,
922
+ options: {
923
+ nonceStore,
924
+ expected: bindings
925
+ }
926
+ });
927
+ } catch (err) {
928
+ const message = (0, import_did_auth_challenge.isIdentityError)(err) ? err.code : "Invalid agent identity";
929
+ throw new HttpError(message, 401);
930
+ }
931
+ return envelope.did;
932
+ }
933
+
934
+ // src/pipeline/flows/static/static-invoke.ts
935
+ async function invokePaidStatic(ctx, wallet, account, body, payment) {
936
+ const actorResult = await resolveActorForHandler(ctx);
937
+ if (actorResult.kind === "error") return actorResult.result;
938
+ return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, payment, actorResult.actor));
939
+ }
940
+ async function invokeUnauthed(ctx, wallet, account, body) {
941
+ const actorResult = await resolveActorForHandler(ctx);
942
+ if (actorResult.kind === "error") return actorResult.result;
943
+ const base = buildHandlerCtx(ctx, wallet, account, body, null, actorResult.actor);
878
944
  if (ctx.routeEntry.billing !== "upto") return runHandler(ctx, base);
879
945
  const uptoCtx = { ...base, charge: async () => {
880
946
  } };
881
947
  return runHandler(ctx, uptoCtx);
882
948
  }
883
- function buildHandlerCtx(ctx, wallet, account, body, payment) {
949
+ async function resolveActorForHandler(ctx) {
950
+ try {
951
+ const actor = await resolveActor(ctx.request, ctx.deps.agentIdentityNonceStore);
952
+ return { kind: "ok", actor };
953
+ } catch (error) {
954
+ return { kind: "error", result: errorResult(error) };
955
+ }
956
+ }
957
+ function buildHandlerCtx(ctx, wallet, account, body, payment, actor) {
884
958
  return {
885
959
  body,
886
960
  query: ctx.query,
@@ -888,6 +962,7 @@ function buildHandlerCtx(ctx, wallet, account, body, payment) {
888
962
  requestId: ctx.meta.requestId,
889
963
  route: ctx.routeEntry.key,
890
964
  wallet,
965
+ actor,
891
966
  payment,
892
967
  account,
893
968
  alert: ctx.report,
@@ -1207,7 +1282,7 @@ async function trySiwxFastPath(ctx, account) {
1207
1282
  function shouldParseBodyEarly(incomingStrategy, routeEntry, pricing) {
1208
1283
  if (incomingStrategy) return false;
1209
1284
  if (!routeEntry.bodySchema) return false;
1210
- return (pricing?.needsBody ?? false) || !!routeEntry.validateFn;
1285
+ return (pricing?.needsBody ?? false) || !!routeEntry.validateFn || !!routeEntry.checkoutSession;
1211
1286
  }
1212
1287
 
1213
1288
  // src/pipeline/steps/resolve-early-body.ts
@@ -2012,7 +2087,7 @@ function tagBareDecimalAsDollars(amount) {
2012
2087
  }
2013
2088
 
2014
2089
  // src/protocols/x402/verify.ts
2015
- var import_types2 = require("@x402/core/types");
2090
+ var import_types3 = require("@x402/core/types");
2016
2091
  async function verifyX402Payment(opts) {
2017
2092
  const { server, request, price, accepts, report } = opts;
2018
2093
  const payment = await readPaymentPayload(request);
@@ -2038,7 +2113,7 @@ async function verifyX402Payment(opts) {
2038
2113
  try {
2039
2114
  verify = await server.verifyPayment(payload, matching);
2040
2115
  } catch (err) {
2041
- if (err instanceof import_types2.VerifyError && err.statusCode >= 400 && err.statusCode < 500) {
2116
+ if (err instanceof import_types3.VerifyError && err.statusCode >= 400 && err.statusCode < 500) {
2042
2117
  return invalidPaymentVerification({
2043
2118
  reason: err.invalidReason ?? "verify_error",
2044
2119
  ...err.invalidMessage ? { message: err.invalidMessage } : {},
@@ -2549,30 +2624,37 @@ async function buildChallengeResponse(ctx, pricing, body, failure) {
2549
2624
  return errorResponse;
2550
2625
  }
2551
2626
  const extensions = await buildChallengeExtensions(ctx);
2552
- const responseBody = failure ? JSON.stringify({ error: failure.message ?? null, reason: failure.reason }) : null;
2553
- const response = new import_server5.NextResponse(responseBody, {
2554
- status: 402,
2555
- headers: {
2556
- "Content-Type": "application/json",
2557
- "Cache-Control": "no-store"
2558
- }
2559
- });
2560
- for (const strategy of getAllowedStrategies(ctx.routeEntry.protocols)) {
2627
+ let checkoutSession;
2628
+ if (ctx.routeEntry.checkoutSession) {
2561
2629
  try {
2562
- const contribution = await strategy.buildChallenge({
2630
+ checkoutSession = await ctx.routeEntry.checkoutSession({
2563
2631
  request: ctx.request,
2564
- routeEntry: ctx.routeEntry,
2632
+ route: ctx.routeEntry.key,
2565
2633
  body,
2566
- price: challengePrice,
2567
- extensions,
2568
- deps: ctx.deps,
2569
- report: ctx.report
2634
+ price: challengePrice
2570
2635
  });
2571
- if (contribution.headers) {
2572
- for (const [name, value] of Object.entries(contribution.headers)) {
2573
- response.headers.set(name, value);
2574
- }
2575
- }
2636
+ } catch (err) {
2637
+ const message = errorMessage(err, "Checkout session build failed");
2638
+ const responseBody2 = { success: false, error: message };
2639
+ const errorResponse = import_server5.NextResponse.json(responseBody2, { status: errorStatus(err, 500) });
2640
+ firePluginResponse(ctx, errorResponse, body, responseBody2, { message, cause: err });
2641
+ return errorResponse;
2642
+ }
2643
+ }
2644
+ const contributions = [];
2645
+ for (const strategy of getAllowedStrategies(ctx.routeEntry.protocols)) {
2646
+ try {
2647
+ contributions.push(
2648
+ await strategy.buildChallenge({
2649
+ request: ctx.request,
2650
+ routeEntry: ctx.routeEntry,
2651
+ body,
2652
+ price: challengePrice,
2653
+ extensions,
2654
+ deps: ctx.deps,
2655
+ report: ctx.report
2656
+ })
2657
+ );
2576
2658
  } catch (err) {
2577
2659
  const message = `${strategy.protocol} challenge build failed: ${errorMessage(err, String(err))}`;
2578
2660
  ctx.report("critical", message);
@@ -2584,6 +2666,27 @@ async function buildChallengeResponse(ctx, pricing, body, failure) {
2584
2666
  }
2585
2667
  }
2586
2668
  }
2669
+ const responsePayload = {
2670
+ ...Object.assign({}, ...contributions.map((contribution) => contribution.body ?? {})),
2671
+ ...failure && { error: failure.message ?? null, reason: failure.reason },
2672
+ ...checkoutSession && { checkout_session: checkoutSession }
2673
+ };
2674
+ const responseBody = Object.keys(responsePayload).length ? JSON.stringify(responsePayload) : null;
2675
+ const response = new import_server5.NextResponse(responseBody, {
2676
+ status: 402,
2677
+ headers: {
2678
+ "Content-Type": "application/json",
2679
+ "Cache-Control": "no-store"
2680
+ }
2681
+ });
2682
+ for (const contribution of contributions) {
2683
+ if (contribution.headers) {
2684
+ for (const [name, value] of Object.entries(contribution.headers)) {
2685
+ response.headers.set(name, value);
2686
+ }
2687
+ }
2688
+ }
2689
+ await attachAgentIdentityChallenge(response, ctx.request, ctx.deps.agentIdentityNonceStore);
2587
2690
  firePluginResponse(ctx, response);
2588
2691
  return response;
2589
2692
  }
@@ -2718,7 +2821,7 @@ function createChargeContext(args) {
2718
2821
 
2719
2822
  // src/pipeline/flows/dynamic/dynamic-invoke/shared.ts
2720
2823
  var import_server7 = require("next/server");
2721
- function buildBaseHandlerCtx(ctx, wallet, account, body, payment) {
2824
+ function buildBaseHandlerCtx(ctx, wallet, account, body, payment, actor = null) {
2722
2825
  return {
2723
2826
  body,
2724
2827
  query: ctx.query,
@@ -2726,12 +2829,21 @@ function buildBaseHandlerCtx(ctx, wallet, account, body, payment) {
2726
2829
  requestId: ctx.meta.requestId,
2727
2830
  route: ctx.routeEntry.key,
2728
2831
  wallet,
2832
+ actor,
2729
2833
  payment,
2730
2834
  account,
2731
2835
  alert: ctx.report,
2732
2836
  setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
2733
2837
  };
2734
2838
  }
2839
+ async function resolveActorOrError(ctx) {
2840
+ try {
2841
+ const actor = await resolveActor(ctx.request, ctx.deps.agentIdentityNonceStore);
2842
+ return { actor };
2843
+ } catch (error) {
2844
+ return errorResult2(error);
2845
+ }
2846
+ }
2735
2847
  function toResponse(rawResult) {
2736
2848
  return rawResult instanceof Response ? rawResult : import_server7.NextResponse.json(rawResult);
2737
2849
  }
@@ -2755,12 +2867,21 @@ function isThenable2(value) {
2755
2867
 
2756
2868
  // src/pipeline/flows/dynamic/dynamic-invoke/metered-invoke.ts
2757
2869
  async function invokeMetered(ctx, wallet, account, body, payment) {
2870
+ const actorResult = await resolveActorOrError(ctx);
2871
+ if ("response" in actorResult) return actorResult;
2758
2872
  const chargeContext = ctx.routeEntry.streaming ? createChargeContext({
2759
2873
  tickCost: ctx.routeEntry.tickCost,
2760
2874
  maxPrice: ctx.routeEntry.maxPrice,
2761
2875
  route: ctx.routeEntry.key
2762
2876
  }) : null;
2763
- const baseHandlerCtx = buildBaseHandlerCtx(ctx, wallet, account, body, payment);
2877
+ const baseHandlerCtx = buildBaseHandlerCtx(
2878
+ ctx,
2879
+ wallet,
2880
+ account,
2881
+ body,
2882
+ payment,
2883
+ actorResult.actor
2884
+ );
2764
2885
  const handlerCtx = chargeContext !== null ? { ...baseHandlerCtx, charge: chargeContext.charge } : baseHandlerCtx;
2765
2886
  let returned;
2766
2887
  try {
@@ -2819,12 +2940,14 @@ function createUptoChargeContext(args) {
2819
2940
 
2820
2941
  // src/pipeline/flows/dynamic/dynamic-invoke/upto-invoke.ts
2821
2942
  async function invokeUpto(ctx, wallet, account, body, payment) {
2943
+ const actorResult = await resolveActorOrError(ctx);
2944
+ if ("response" in actorResult) return actorResult;
2822
2945
  const uptoCtx = createUptoChargeContext({
2823
2946
  maxPrice: ctx.routeEntry.maxPrice,
2824
2947
  route: ctx.routeEntry.key
2825
2948
  });
2826
2949
  const handlerCtx = {
2827
- ...buildBaseHandlerCtx(ctx, wallet, account, body, payment),
2950
+ ...buildBaseHandlerCtx(ctx, wallet, account, body, payment, actorResult.actor),
2828
2951
  charge: uptoCtx.charge
2829
2952
  };
2830
2953
  let returned;
@@ -3357,6 +3480,45 @@ async function createKvMppStore(kv, options) {
3357
3480
  });
3358
3481
  }
3359
3482
 
3483
+ // src/kv-store/agent-identity-nonce.ts
3484
+ var import_did_auth_challenge2 = require("did-auth-challenge");
3485
+ function createKvAgentIdentityNonceStore(kv, options) {
3486
+ const prefix = options?.prefix ?? "actor:nonce:";
3487
+ return {
3488
+ async add(nonce, expiresAt, challengeJson) {
3489
+ const ttl = Math.max(1, Math.ceil((expiresAt.getTime() - Date.now()) / 1e3));
3490
+ const record = { challengeJson, expiresAt: expiresAt.getTime() };
3491
+ await kv.setNxEx(`${prefix}${nonce}`, record, ttl);
3492
+ },
3493
+ async get(nonce) {
3494
+ const raw = await kv.get(`${prefix}${nonce}`);
3495
+ if (!raw) return null;
3496
+ const record = raw;
3497
+ if (Date.now() > record.expiresAt) {
3498
+ await kv.del(`${prefix}${nonce}`);
3499
+ return null;
3500
+ }
3501
+ return record;
3502
+ },
3503
+ async has(nonce) {
3504
+ return await this.get(nonce) !== null;
3505
+ },
3506
+ async consume(nonce) {
3507
+ return kv.update(`${prefix}${nonce}`, (current) => {
3508
+ if (!current) return { op: "noop", result: null };
3509
+ const record = current;
3510
+ if (Date.now() > record.expiresAt) {
3511
+ return { op: "delete", result: null };
3512
+ }
3513
+ return { op: "delete", result: record };
3514
+ });
3515
+ }
3516
+ };
3517
+ }
3518
+ function createAgentIdentityNonceStore(kv) {
3519
+ return kv ? createKvAgentIdentityNonceStore(kv) : new import_did_auth_challenge2.NonceStore();
3520
+ }
3521
+
3360
3522
  // src/protocols/mpp/siwx-mode.ts
3361
3523
  var import_mppx3 = require("mppx");
3362
3524
  async function verifyMppSiwx(request, mppx) {
@@ -3483,6 +3645,7 @@ async function buildSiwxChallenge(ctx) {
3483
3645
  } catch {
3484
3646
  }
3485
3647
  }
3648
+ await attachAgentIdentityChallenge(response, request, deps.agentIdentityNonceStore);
3486
3649
  firePluginResponse(ctx, response);
3487
3650
  return response;
3488
3651
  }
@@ -3602,7 +3765,8 @@ var RouteBuilder = class _RouteBuilder {
3602
3765
  validateFn: void 0,
3603
3766
  settlement: void 0,
3604
3767
  mppInfo: void 0,
3605
- hasCheckout: false
3768
+ hasCheckout: false,
3769
+ checkoutSession: void 0
3606
3770
  };
3607
3771
  }
3608
3772
  fork() {
@@ -3693,7 +3857,8 @@ var RouteBuilder = class _RouteBuilder {
3693
3857
  if (resolvedOptions.minPrice) next.#s.minPrice = resolvedOptions.minPrice;
3694
3858
  if (resolvedOptions.payTo) next.#s.payTo = resolvedOptions.payTo;
3695
3859
  if (resolvedOptions.mpp) next.#s.mppInfo = resolvedOptions.mpp;
3696
- if (resolvedOptions.checkout) next.#s.hasCheckout = true;
3860
+ if (resolvedOptions.checkout || resolvedOptions.checkoutSession) next.#s.hasCheckout = true;
3861
+ if (resolvedOptions.checkoutSession) next.#s.checkoutSession = resolvedOptions.checkoutSession;
3697
3862
  next.#s.billing = billing;
3698
3863
  if (tickCost) next.#s.tickCost = tickCost;
3699
3864
  if (unitType) next.#s.unitType = unitType;
@@ -4128,6 +4293,7 @@ var RouteBuilder = class _RouteBuilder {
4128
4293
  settlement: this.#s.settlement,
4129
4294
  mppInfo: this.#s.mppInfo,
4130
4295
  hasCheckout: this.#s.hasCheckout ? true : void 0,
4296
+ checkoutSession: this.#s.checkoutSession,
4131
4297
  tickCost: this.#s.tickCost,
4132
4298
  unitType: this.#s.unitType
4133
4299
  };
@@ -4475,6 +4641,33 @@ function createLlmsTxtHandler(discovery) {
4475
4641
  };
4476
4642
  }
4477
4643
 
4644
+ // src/discovery/not-found.ts
4645
+ var import_server12 = require("next/server");
4646
+ function createNotFoundHandler(baseUrl) {
4647
+ const normalizedBase = baseUrl.replace(/\/+$/, "");
4648
+ return async (request) => import_server12.NextResponse.json(
4649
+ {
4650
+ success: false,
4651
+ code: "route_not_found",
4652
+ error: "Route not found. Rediscover this origin and retry with the current discovery document.",
4653
+ requestedUrl: request.url,
4654
+ discovery: {
4655
+ openapi: `${normalizedBase}/openapi.json`,
4656
+ wellKnown: `${normalizedBase}/.well-known/x402`,
4657
+ llmsTxt: `${normalizedBase}/llms.txt`
4658
+ }
4659
+ },
4660
+ {
4661
+ status: 404,
4662
+ headers: {
4663
+ "Access-Control-Allow-Origin": "*",
4664
+ "Access-Control-Allow-Methods": "GET, POST, DELETE, PUT, PATCH, OPTIONS",
4665
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key, SIGN-IN-WITH-X, X-Agent-Identity"
4666
+ }
4667
+ }
4668
+ );
4669
+ }
4670
+
4478
4671
  // src/index.ts
4479
4672
  init_accepts();
4480
4673
  init_constants();
@@ -5083,6 +5276,7 @@ function createRouter(config) {
5083
5276
  const registry = new RouteRegistry();
5084
5277
  const kvStore = resolveKvStore(config.kvStore);
5085
5278
  const nonceStore = kvStore ? createKvNonceStore(kvStore) : new MemoryNonceStore();
5279
+ const agentIdentityNonceStore = createAgentIdentityNonceStore(kvStore);
5086
5280
  const entitlementStore = kvStore ? createKvEntitlementStore(kvStore) : new MemoryEntitlementStore();
5087
5281
  const network = config.network ?? BASE_MAINNET_NETWORK;
5088
5282
  const x402Accepts = getConfiguredX402Accepts(config);
@@ -5117,6 +5311,7 @@ function createRouter(config) {
5117
5311
  initPromise: Promise.resolve(),
5118
5312
  plugin: config.plugin,
5119
5313
  nonceStore,
5314
+ agentIdentityNonceStore,
5120
5315
  entitlementStore,
5121
5316
  payeeAddress: config.payeeAddress ?? "",
5122
5317
  mppRecipient: config.mpp?.recipient ?? config.payeeAddress,
@@ -5178,6 +5373,9 @@ function createRouter(config) {
5178
5373
  llmsTxt() {
5179
5374
  return createLlmsTxtHandler(config.discovery);
5180
5375
  },
5376
+ notFound() {
5377
+ return createNotFoundHandler(resolvedBaseUrl);
5378
+ },
5181
5379
  monitors() {
5182
5380
  const result = [];
5183
5381
  for (const [, entry] of registry.entries()) {
package/dist/index.d.cts CHANGED
@@ -4,6 +4,7 @@ import { ZodType } from 'zod';
4
4
  import { PaymentRequirements, PaymentRequired, VerifyResponse, SettleResponse, Network } from '@x402/core/types';
5
5
  import * as viem from 'viem';
6
6
  import { Transport } from 'mppx/server';
7
+ import { NonceStoreInterface } from 'did-auth-challenge';
7
8
 
8
9
  interface KvStore {
9
10
  get(key: string): Promise<unknown>;
@@ -79,6 +80,8 @@ interface ResponseMeta {
79
80
  requestBody?: unknown;
80
81
  /** Handler return value or structured router-generated error body. */
81
82
  responseBody?: unknown;
83
+ /** Rich error details for non-402 failure responses. */
84
+ error?: ErrorEvent;
82
85
  }
83
86
  interface ErrorEvent {
84
87
  status: number;
@@ -219,6 +222,15 @@ interface MppProtocolInfo {
219
222
  intent?: string;
220
223
  currency?: string;
221
224
  }
225
+ type CheckoutSessionResponse = Record<string, unknown>;
226
+ interface CheckoutSessionContext<TBody = unknown> {
227
+ request: NextRequest;
228
+ route: string;
229
+ body: TBody | undefined;
230
+ /** Decimal-dollar price quoted for this 402 challenge. */
231
+ price: string;
232
+ }
233
+ type CheckoutSessionFn<TBody = unknown> = (ctx: CheckoutSessionContext<TBody>) => CheckoutSessionResponse | null | undefined | Promise<CheckoutSessionResponse | null | undefined>;
222
234
  interface PaidOptions {
223
235
  protocols?: ProtocolType[];
224
236
  maxPrice?: string;
@@ -229,6 +241,14 @@ interface PaidOptions {
229
241
  mpp?: MppProtocolInfo;
230
242
  /** Signal in discovery that clients should use an explicit checkout flow before payment. */
231
243
  checkout?: boolean;
244
+ /**
245
+ * Build dynamic checkout review metadata for the router-owned 402 response body.
246
+ *
247
+ * The returned object is emitted as `{ "checkout_session": ... }` on the unpaid
248
+ * payment challenge response. Payment terms remain authoritative in the x402/MPP
249
+ * challenge headers.
250
+ */
251
+ checkoutSession?: CheckoutSessionFn;
232
252
  }
233
253
  type PaidArg = (PaidOptions & {
234
254
  price: string;
@@ -303,6 +323,8 @@ interface HandlerContext<TBody = undefined, TQuery = undefined> {
303
323
  requestId: string;
304
324
  route: string;
305
325
  wallet: string | null;
326
+ /** Optional DID from `X-Agent-Identity` proof. Null when the client omits identity. */
327
+ actor: string | null;
306
328
  payment: HandlerPaymentContext | null;
307
329
  account: unknown;
308
330
  alert: AlertFn;
@@ -374,6 +396,7 @@ interface RouteEntry {
374
396
  settlement?: SettlementLifecycle;
375
397
  mppInfo?: MppProtocolInfo;
376
398
  hasCheckout?: boolean;
399
+ checkoutSession?: CheckoutSessionFn;
377
400
  /** Per-tick cost (decimal-dollar). Required when `metered` is true. */
378
401
  tickCost?: string;
379
402
  /** Cosmetic unit label for 402 challenges and client UIs. */
@@ -480,6 +503,7 @@ interface RouterDeps {
480
503
  mppInitError?: string;
481
504
  plugin?: RouterPlugin;
482
505
  nonceStore: NonceStore;
506
+ agentIdentityNonceStore: NonceStoreInterface;
483
507
  entitlementStore: EntitlementStore;
484
508
  payeeAddress: string;
485
509
  mppRecipient?: string;
@@ -557,8 +581,8 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, TOutput = unde
557
581
  * - `{ price }` — fixed price (object form of the string sugar).
558
582
  * - `{ field, tiers, default? }` — pick a tier from `body[field]`.
559
583
  *
560
- * Common knobs (`protocols`, `maxPrice`, `minPrice`, `payTo`, `mpp`, `checkout`) live
561
- * alongside the pricing shape. For handler-computed billing use `.upTo()`;
584
+ * Common knobs (`protocols`, `maxPrice`, `minPrice`, `payTo`, `mpp`, `checkout`,
585
+ * `checkoutSession`) live alongside the pricing shape. For handler-computed billing use `.upTo()`;
562
586
  * for per-tick billing use `.metered()`.
563
587
  *
564
588
  * @example
@@ -900,6 +924,7 @@ interface ServiceRouter<TPriceKeys extends string = never> {
900
924
  wellKnown(): (request: NextRequest) => Promise<NextResponse>;
901
925
  openapi(): (request: NextRequest) => Promise<NextResponse>;
902
926
  llmsTxt(): (request: NextRequest) => Promise<NextResponse>;
927
+ notFound(): (request: NextRequest) => Promise<NextResponse>;
903
928
  monitors(): MonitorEntry[];
904
929
  registry: RouteRegistry;
905
930
  }
@@ -928,4 +953,4 @@ declare function createRouter<const P extends Record<string, string> = Record<ne
928
953
  */
929
954
  declare function createRouterFromEnv<const P extends Record<string, string> = Record<never, string>>(options: CreateRouterFromEnvOptions<P>): ServiceRouter<Extract<keyof P, string>>;
930
955
 
931
- export { BASE_MAINNET_NETWORK, BASE_USDC_ADDRESS, BASE_USDC_DECIMALS, type CreateRouterFromEnvOptions, DEFAULT_SOLANA_FACILITATOR_URL, DEFAULT_TEMPO_RPC_URL, type DiscoveryConfig, type HandlerContext, HttpError, type KvStore, type PaidOptions, type ProtocolType, type RouterConfig, RouterConfigError, type RouterConfigIssue, type RouterConfigIssueCode, type RouterConfigIssueSeverity, type RouterPlugin, SOLANA_MAINNET_NETWORK, type ServiceRouter, type SettlementErrorContext, type SettlementLifecycleContext, type SettlementSettledContext, TEMPO_USDC_ADDRESS, TEMPO_USDC_DECIMALS, type X402FacilitatorsConfig, ZERO_EVM_ADDRESS, createRouter, createRouterFromEnv, routerConfigFromEnv };
956
+ export { BASE_MAINNET_NETWORK, BASE_USDC_ADDRESS, BASE_USDC_DECIMALS, type CheckoutSessionContext, type CheckoutSessionFn, type CheckoutSessionResponse, type CreateRouterFromEnvOptions, DEFAULT_SOLANA_FACILITATOR_URL, DEFAULT_TEMPO_RPC_URL, type DiscoveryConfig, type HandlerContext, HttpError, type KvStore, type PaidOptions, type ProtocolType, type RouterConfig, RouterConfigError, type RouterConfigIssue, type RouterConfigIssueCode, type RouterConfigIssueSeverity, type RouterPlugin, SOLANA_MAINNET_NETWORK, type ServiceRouter, type SettlementErrorContext, type SettlementLifecycleContext, type SettlementSettledContext, TEMPO_USDC_ADDRESS, TEMPO_USDC_DECIMALS, type X402FacilitatorsConfig, ZERO_EVM_ADDRESS, createRouter, createRouterFromEnv, routerConfigFromEnv };
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ import { ZodType } from 'zod';
4
4
  import { PaymentRequirements, PaymentRequired, VerifyResponse, SettleResponse, Network } from '@x402/core/types';
5
5
  import * as viem from 'viem';
6
6
  import { Transport } from 'mppx/server';
7
+ import { NonceStoreInterface } from 'did-auth-challenge';
7
8
 
8
9
  interface KvStore {
9
10
  get(key: string): Promise<unknown>;
@@ -79,6 +80,8 @@ interface ResponseMeta {
79
80
  requestBody?: unknown;
80
81
  /** Handler return value or structured router-generated error body. */
81
82
  responseBody?: unknown;
83
+ /** Rich error details for non-402 failure responses. */
84
+ error?: ErrorEvent;
82
85
  }
83
86
  interface ErrorEvent {
84
87
  status: number;
@@ -219,6 +222,15 @@ interface MppProtocolInfo {
219
222
  intent?: string;
220
223
  currency?: string;
221
224
  }
225
+ type CheckoutSessionResponse = Record<string, unknown>;
226
+ interface CheckoutSessionContext<TBody = unknown> {
227
+ request: NextRequest;
228
+ route: string;
229
+ body: TBody | undefined;
230
+ /** Decimal-dollar price quoted for this 402 challenge. */
231
+ price: string;
232
+ }
233
+ type CheckoutSessionFn<TBody = unknown> = (ctx: CheckoutSessionContext<TBody>) => CheckoutSessionResponse | null | undefined | Promise<CheckoutSessionResponse | null | undefined>;
222
234
  interface PaidOptions {
223
235
  protocols?: ProtocolType[];
224
236
  maxPrice?: string;
@@ -229,6 +241,14 @@ interface PaidOptions {
229
241
  mpp?: MppProtocolInfo;
230
242
  /** Signal in discovery that clients should use an explicit checkout flow before payment. */
231
243
  checkout?: boolean;
244
+ /**
245
+ * Build dynamic checkout review metadata for the router-owned 402 response body.
246
+ *
247
+ * The returned object is emitted as `{ "checkout_session": ... }` on the unpaid
248
+ * payment challenge response. Payment terms remain authoritative in the x402/MPP
249
+ * challenge headers.
250
+ */
251
+ checkoutSession?: CheckoutSessionFn;
232
252
  }
233
253
  type PaidArg = (PaidOptions & {
234
254
  price: string;
@@ -303,6 +323,8 @@ interface HandlerContext<TBody = undefined, TQuery = undefined> {
303
323
  requestId: string;
304
324
  route: string;
305
325
  wallet: string | null;
326
+ /** Optional DID from `X-Agent-Identity` proof. Null when the client omits identity. */
327
+ actor: string | null;
306
328
  payment: HandlerPaymentContext | null;
307
329
  account: unknown;
308
330
  alert: AlertFn;
@@ -374,6 +396,7 @@ interface RouteEntry {
374
396
  settlement?: SettlementLifecycle;
375
397
  mppInfo?: MppProtocolInfo;
376
398
  hasCheckout?: boolean;
399
+ checkoutSession?: CheckoutSessionFn;
377
400
  /** Per-tick cost (decimal-dollar). Required when `metered` is true. */
378
401
  tickCost?: string;
379
402
  /** Cosmetic unit label for 402 challenges and client UIs. */
@@ -480,6 +503,7 @@ interface RouterDeps {
480
503
  mppInitError?: string;
481
504
  plugin?: RouterPlugin;
482
505
  nonceStore: NonceStore;
506
+ agentIdentityNonceStore: NonceStoreInterface;
483
507
  entitlementStore: EntitlementStore;
484
508
  payeeAddress: string;
485
509
  mppRecipient?: string;
@@ -557,8 +581,8 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, TOutput = unde
557
581
  * - `{ price }` — fixed price (object form of the string sugar).
558
582
  * - `{ field, tiers, default? }` — pick a tier from `body[field]`.
559
583
  *
560
- * Common knobs (`protocols`, `maxPrice`, `minPrice`, `payTo`, `mpp`, `checkout`) live
561
- * alongside the pricing shape. For handler-computed billing use `.upTo()`;
584
+ * Common knobs (`protocols`, `maxPrice`, `minPrice`, `payTo`, `mpp`, `checkout`,
585
+ * `checkoutSession`) live alongside the pricing shape. For handler-computed billing use `.upTo()`;
562
586
  * for per-tick billing use `.metered()`.
563
587
  *
564
588
  * @example
@@ -900,6 +924,7 @@ interface ServiceRouter<TPriceKeys extends string = never> {
900
924
  wellKnown(): (request: NextRequest) => Promise<NextResponse>;
901
925
  openapi(): (request: NextRequest) => Promise<NextResponse>;
902
926
  llmsTxt(): (request: NextRequest) => Promise<NextResponse>;
927
+ notFound(): (request: NextRequest) => Promise<NextResponse>;
903
928
  monitors(): MonitorEntry[];
904
929
  registry: RouteRegistry;
905
930
  }
@@ -928,4 +953,4 @@ declare function createRouter<const P extends Record<string, string> = Record<ne
928
953
  */
929
954
  declare function createRouterFromEnv<const P extends Record<string, string> = Record<never, string>>(options: CreateRouterFromEnvOptions<P>): ServiceRouter<Extract<keyof P, string>>;
930
955
 
931
- export { BASE_MAINNET_NETWORK, BASE_USDC_ADDRESS, BASE_USDC_DECIMALS, type CreateRouterFromEnvOptions, DEFAULT_SOLANA_FACILITATOR_URL, DEFAULT_TEMPO_RPC_URL, type DiscoveryConfig, type HandlerContext, HttpError, type KvStore, type PaidOptions, type ProtocolType, type RouterConfig, RouterConfigError, type RouterConfigIssue, type RouterConfigIssueCode, type RouterConfigIssueSeverity, type RouterPlugin, SOLANA_MAINNET_NETWORK, type ServiceRouter, type SettlementErrorContext, type SettlementLifecycleContext, type SettlementSettledContext, TEMPO_USDC_ADDRESS, TEMPO_USDC_DECIMALS, type X402FacilitatorsConfig, ZERO_EVM_ADDRESS, createRouter, createRouterFromEnv, routerConfigFromEnv };
956
+ export { BASE_MAINNET_NETWORK, BASE_USDC_ADDRESS, BASE_USDC_DECIMALS, type CheckoutSessionContext, type CheckoutSessionFn, type CheckoutSessionResponse, type CreateRouterFromEnvOptions, DEFAULT_SOLANA_FACILITATOR_URL, DEFAULT_TEMPO_RPC_URL, type DiscoveryConfig, type HandlerContext, HttpError, type KvStore, type PaidOptions, type ProtocolType, type RouterConfig, RouterConfigError, type RouterConfigIssue, type RouterConfigIssueCode, type RouterConfigIssueSeverity, type RouterPlugin, SOLANA_MAINNET_NETWORK, type ServiceRouter, type SettlementErrorContext, type SettlementLifecycleContext, type SettlementSettledContext, TEMPO_USDC_ADDRESS, TEMPO_USDC_DECIMALS, type X402FacilitatorsConfig, ZERO_EVM_ADDRESS, createRouter, createRouterFromEnv, routerConfigFromEnv };
package/dist/index.js CHANGED
@@ -500,7 +500,8 @@ var HEADERS = {
500
500
  X402_PAYMENT_REQUIRED: "PAYMENT-REQUIRED",
501
501
  X402_PAYMENT_RESPONSE: "PAYMENT-RESPONSE",
502
502
  MPP_PAYMENT_RECEIPT: "Payment-Receipt",
503
- REQUEST_ID: "X-Request-ID"
503
+ REQUEST_ID: "X-Request-ID",
504
+ AGENT_IDENTITY: "X-Agent-Identity"
504
505
  };
505
506
  var AUTH_SCHEME = {
506
507
  BEARER: "Bearer ",
@@ -635,6 +636,7 @@ function firePaymentSettled(ctx, event) {
635
636
  }
636
637
  function firePluginResponse(ctx, response, requestBody, responseBody, failure) {
637
638
  attachRequestId(response, ctx.meta.requestId);
639
+ const error = response.status >= 400 && response.status !== 402 ? buildErrorEvent(ctx, response, failure) : void 0;
638
640
  firePluginHook(ctx.deps.plugin, "onResponse", ctx.pluginCtx, {
639
641
  statusCode: response.status,
640
642
  statusText: response.statusText,
@@ -642,10 +644,10 @@ function firePluginResponse(ctx, response, requestBody, responseBody, failure) {
642
644
  contentType: response.headers.get("content-type"),
643
645
  headers: Object.fromEntries(response.headers.entries()),
644
646
  requestBody,
645
- responseBody
647
+ responseBody,
648
+ error
646
649
  });
647
- if (response.status >= 400 && response.status !== 402) {
648
- const error = buildErrorEvent(ctx, response, failure);
650
+ if (error) {
649
651
  if (response.status >= 500) logRouterFailure(error);
650
652
  firePluginHook(ctx.deps.plugin, "onError", ctx.pluginCtx, error);
651
653
  }
@@ -818,6 +820,13 @@ async function runValidate(ctx, body) {
818
820
  // src/pipeline/flows/static/static-invoke.ts
819
821
  import { NextResponse as NextResponse4 } from "next/server";
820
822
 
823
+ // src/auth/agent-identity.ts
824
+ import {
825
+ createChallenge,
826
+ isIdentityError,
827
+ verifySignature
828
+ } from "did-auth-challenge";
829
+
821
830
  // src/types.ts
822
831
  var HttpError = class extends Error {
823
832
  constructor(message, status) {
@@ -827,18 +836,87 @@ var HttpError = class extends Error {
827
836
  }
828
837
  };
829
838
 
830
- // src/pipeline/flows/static/static-invoke.ts
831
- function invokePaidStatic(ctx, wallet, account, body, payment) {
832
- return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, payment));
839
+ // src/auth/agent-identity.ts
840
+ var PROTOCOL_VERSION = 1;
841
+ function base64urlEncode(value) {
842
+ return Buffer.from(JSON.stringify(value)).toString("base64url");
843
+ }
844
+ function base64urlDecode(value) {
845
+ return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
846
+ }
847
+ function challengeBindings(request) {
848
+ const url = new URL(request.url);
849
+ return { domain: url.hostname, route: url.pathname };
850
+ }
851
+ async function buildAgentIdentityChallengeHeader(request, nonceStore) {
852
+ const bindings = challengeBindings(request);
853
+ const challenge = await createChallenge(bindings, { nonceStore });
854
+ return base64urlEncode({
855
+ v: PROTOCOL_VERSION,
856
+ challenge
857
+ });
833
858
  }
834
- function invokeUnauthed(ctx, wallet, account, body) {
835
- const base = buildHandlerCtx(ctx, wallet, account, body, null);
859
+ async function attachAgentIdentityChallenge(response, request, nonceStore) {
860
+ response.headers.set(
861
+ HEADERS.AGENT_IDENTITY,
862
+ await buildAgentIdentityChallengeHeader(request, nonceStore)
863
+ );
864
+ }
865
+ async function resolveActor(request, nonceStore) {
866
+ const header = request.headers.get(HEADERS.AGENT_IDENTITY);
867
+ if (!header) return null;
868
+ let envelope;
869
+ try {
870
+ envelope = base64urlDecode(header);
871
+ } catch {
872
+ throw new HttpError("Malformed X-Agent-Identity header", 401);
873
+ }
874
+ if (envelope.v !== PROTOCOL_VERSION || !envelope.challenge || !envelope.did || !envelope.signature) {
875
+ throw new HttpError("Invalid X-Agent-Identity proof", 401);
876
+ }
877
+ const bindings = challengeBindings(request);
878
+ try {
879
+ await verifySignature({
880
+ did: envelope.did,
881
+ authenticationKey: envelope.authenticationKey,
882
+ challenge: envelope.challenge,
883
+ signature: envelope.signature,
884
+ options: {
885
+ nonceStore,
886
+ expected: bindings
887
+ }
888
+ });
889
+ } catch (err) {
890
+ const message = isIdentityError(err) ? err.code : "Invalid agent identity";
891
+ throw new HttpError(message, 401);
892
+ }
893
+ return envelope.did;
894
+ }
895
+
896
+ // src/pipeline/flows/static/static-invoke.ts
897
+ async function invokePaidStatic(ctx, wallet, account, body, payment) {
898
+ const actorResult = await resolveActorForHandler(ctx);
899
+ if (actorResult.kind === "error") return actorResult.result;
900
+ return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, payment, actorResult.actor));
901
+ }
902
+ async function invokeUnauthed(ctx, wallet, account, body) {
903
+ const actorResult = await resolveActorForHandler(ctx);
904
+ if (actorResult.kind === "error") return actorResult.result;
905
+ const base = buildHandlerCtx(ctx, wallet, account, body, null, actorResult.actor);
836
906
  if (ctx.routeEntry.billing !== "upto") return runHandler(ctx, base);
837
907
  const uptoCtx = { ...base, charge: async () => {
838
908
  } };
839
909
  return runHandler(ctx, uptoCtx);
840
910
  }
841
- function buildHandlerCtx(ctx, wallet, account, body, payment) {
911
+ async function resolveActorForHandler(ctx) {
912
+ try {
913
+ const actor = await resolveActor(ctx.request, ctx.deps.agentIdentityNonceStore);
914
+ return { kind: "ok", actor };
915
+ } catch (error) {
916
+ return { kind: "error", result: errorResult(error) };
917
+ }
918
+ }
919
+ function buildHandlerCtx(ctx, wallet, account, body, payment, actor) {
842
920
  return {
843
921
  body,
844
922
  query: ctx.query,
@@ -846,6 +924,7 @@ function buildHandlerCtx(ctx, wallet, account, body, payment) {
846
924
  requestId: ctx.meta.requestId,
847
925
  route: ctx.routeEntry.key,
848
926
  wallet,
927
+ actor,
849
928
  payment,
850
929
  account,
851
930
  alert: ctx.report,
@@ -1165,7 +1244,7 @@ async function trySiwxFastPath(ctx, account) {
1165
1244
  function shouldParseBodyEarly(incomingStrategy, routeEntry, pricing) {
1166
1245
  if (incomingStrategy) return false;
1167
1246
  if (!routeEntry.bodySchema) return false;
1168
- return (pricing?.needsBody ?? false) || !!routeEntry.validateFn;
1247
+ return (pricing?.needsBody ?? false) || !!routeEntry.validateFn || !!routeEntry.checkoutSession;
1169
1248
  }
1170
1249
 
1171
1250
  // src/pipeline/steps/resolve-early-body.ts
@@ -2507,30 +2586,37 @@ async function buildChallengeResponse(ctx, pricing, body, failure) {
2507
2586
  return errorResponse;
2508
2587
  }
2509
2588
  const extensions = await buildChallengeExtensions(ctx);
2510
- const responseBody = failure ? JSON.stringify({ error: failure.message ?? null, reason: failure.reason }) : null;
2511
- const response = new NextResponse5(responseBody, {
2512
- status: 402,
2513
- headers: {
2514
- "Content-Type": "application/json",
2515
- "Cache-Control": "no-store"
2516
- }
2517
- });
2518
- for (const strategy of getAllowedStrategies(ctx.routeEntry.protocols)) {
2589
+ let checkoutSession;
2590
+ if (ctx.routeEntry.checkoutSession) {
2519
2591
  try {
2520
- const contribution = await strategy.buildChallenge({
2592
+ checkoutSession = await ctx.routeEntry.checkoutSession({
2521
2593
  request: ctx.request,
2522
- routeEntry: ctx.routeEntry,
2594
+ route: ctx.routeEntry.key,
2523
2595
  body,
2524
- price: challengePrice,
2525
- extensions,
2526
- deps: ctx.deps,
2527
- report: ctx.report
2596
+ price: challengePrice
2528
2597
  });
2529
- if (contribution.headers) {
2530
- for (const [name, value] of Object.entries(contribution.headers)) {
2531
- response.headers.set(name, value);
2532
- }
2533
- }
2598
+ } catch (err) {
2599
+ const message = errorMessage(err, "Checkout session build failed");
2600
+ const responseBody2 = { success: false, error: message };
2601
+ const errorResponse = NextResponse5.json(responseBody2, { status: errorStatus(err, 500) });
2602
+ firePluginResponse(ctx, errorResponse, body, responseBody2, { message, cause: err });
2603
+ return errorResponse;
2604
+ }
2605
+ }
2606
+ const contributions = [];
2607
+ for (const strategy of getAllowedStrategies(ctx.routeEntry.protocols)) {
2608
+ try {
2609
+ contributions.push(
2610
+ await strategy.buildChallenge({
2611
+ request: ctx.request,
2612
+ routeEntry: ctx.routeEntry,
2613
+ body,
2614
+ price: challengePrice,
2615
+ extensions,
2616
+ deps: ctx.deps,
2617
+ report: ctx.report
2618
+ })
2619
+ );
2534
2620
  } catch (err) {
2535
2621
  const message = `${strategy.protocol} challenge build failed: ${errorMessage(err, String(err))}`;
2536
2622
  ctx.report("critical", message);
@@ -2542,6 +2628,27 @@ async function buildChallengeResponse(ctx, pricing, body, failure) {
2542
2628
  }
2543
2629
  }
2544
2630
  }
2631
+ const responsePayload = {
2632
+ ...Object.assign({}, ...contributions.map((contribution) => contribution.body ?? {})),
2633
+ ...failure && { error: failure.message ?? null, reason: failure.reason },
2634
+ ...checkoutSession && { checkout_session: checkoutSession }
2635
+ };
2636
+ const responseBody = Object.keys(responsePayload).length ? JSON.stringify(responsePayload) : null;
2637
+ const response = new NextResponse5(responseBody, {
2638
+ status: 402,
2639
+ headers: {
2640
+ "Content-Type": "application/json",
2641
+ "Cache-Control": "no-store"
2642
+ }
2643
+ });
2644
+ for (const contribution of contributions) {
2645
+ if (contribution.headers) {
2646
+ for (const [name, value] of Object.entries(contribution.headers)) {
2647
+ response.headers.set(name, value);
2648
+ }
2649
+ }
2650
+ }
2651
+ await attachAgentIdentityChallenge(response, ctx.request, ctx.deps.agentIdentityNonceStore);
2545
2652
  firePluginResponse(ctx, response);
2546
2653
  return response;
2547
2654
  }
@@ -2676,7 +2783,7 @@ function createChargeContext(args) {
2676
2783
 
2677
2784
  // src/pipeline/flows/dynamic/dynamic-invoke/shared.ts
2678
2785
  import { NextResponse as NextResponse7 } from "next/server";
2679
- function buildBaseHandlerCtx(ctx, wallet, account, body, payment) {
2786
+ function buildBaseHandlerCtx(ctx, wallet, account, body, payment, actor = null) {
2680
2787
  return {
2681
2788
  body,
2682
2789
  query: ctx.query,
@@ -2684,12 +2791,21 @@ function buildBaseHandlerCtx(ctx, wallet, account, body, payment) {
2684
2791
  requestId: ctx.meta.requestId,
2685
2792
  route: ctx.routeEntry.key,
2686
2793
  wallet,
2794
+ actor,
2687
2795
  payment,
2688
2796
  account,
2689
2797
  alert: ctx.report,
2690
2798
  setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
2691
2799
  };
2692
2800
  }
2801
+ async function resolveActorOrError(ctx) {
2802
+ try {
2803
+ const actor = await resolveActor(ctx.request, ctx.deps.agentIdentityNonceStore);
2804
+ return { actor };
2805
+ } catch (error) {
2806
+ return errorResult2(error);
2807
+ }
2808
+ }
2693
2809
  function toResponse(rawResult) {
2694
2810
  return rawResult instanceof Response ? rawResult : NextResponse7.json(rawResult);
2695
2811
  }
@@ -2713,12 +2829,21 @@ function isThenable2(value) {
2713
2829
 
2714
2830
  // src/pipeline/flows/dynamic/dynamic-invoke/metered-invoke.ts
2715
2831
  async function invokeMetered(ctx, wallet, account, body, payment) {
2832
+ const actorResult = await resolveActorOrError(ctx);
2833
+ if ("response" in actorResult) return actorResult;
2716
2834
  const chargeContext = ctx.routeEntry.streaming ? createChargeContext({
2717
2835
  tickCost: ctx.routeEntry.tickCost,
2718
2836
  maxPrice: ctx.routeEntry.maxPrice,
2719
2837
  route: ctx.routeEntry.key
2720
2838
  }) : null;
2721
- const baseHandlerCtx = buildBaseHandlerCtx(ctx, wallet, account, body, payment);
2839
+ const baseHandlerCtx = buildBaseHandlerCtx(
2840
+ ctx,
2841
+ wallet,
2842
+ account,
2843
+ body,
2844
+ payment,
2845
+ actorResult.actor
2846
+ );
2722
2847
  const handlerCtx = chargeContext !== null ? { ...baseHandlerCtx, charge: chargeContext.charge } : baseHandlerCtx;
2723
2848
  let returned;
2724
2849
  try {
@@ -2777,12 +2902,14 @@ function createUptoChargeContext(args) {
2777
2902
 
2778
2903
  // src/pipeline/flows/dynamic/dynamic-invoke/upto-invoke.ts
2779
2904
  async function invokeUpto(ctx, wallet, account, body, payment) {
2905
+ const actorResult = await resolveActorOrError(ctx);
2906
+ if ("response" in actorResult) return actorResult;
2780
2907
  const uptoCtx = createUptoChargeContext({
2781
2908
  maxPrice: ctx.routeEntry.maxPrice,
2782
2909
  route: ctx.routeEntry.key
2783
2910
  });
2784
2911
  const handlerCtx = {
2785
- ...buildBaseHandlerCtx(ctx, wallet, account, body, payment),
2912
+ ...buildBaseHandlerCtx(ctx, wallet, account, body, payment, actorResult.actor),
2786
2913
  charge: uptoCtx.charge
2787
2914
  };
2788
2915
  let returned;
@@ -3315,6 +3442,45 @@ async function createKvMppStore(kv, options) {
3315
3442
  });
3316
3443
  }
3317
3444
 
3445
+ // src/kv-store/agent-identity-nonce.ts
3446
+ import { NonceStore } from "did-auth-challenge";
3447
+ function createKvAgentIdentityNonceStore(kv, options) {
3448
+ const prefix = options?.prefix ?? "actor:nonce:";
3449
+ return {
3450
+ async add(nonce, expiresAt, challengeJson) {
3451
+ const ttl = Math.max(1, Math.ceil((expiresAt.getTime() - Date.now()) / 1e3));
3452
+ const record = { challengeJson, expiresAt: expiresAt.getTime() };
3453
+ await kv.setNxEx(`${prefix}${nonce}`, record, ttl);
3454
+ },
3455
+ async get(nonce) {
3456
+ const raw = await kv.get(`${prefix}${nonce}`);
3457
+ if (!raw) return null;
3458
+ const record = raw;
3459
+ if (Date.now() > record.expiresAt) {
3460
+ await kv.del(`${prefix}${nonce}`);
3461
+ return null;
3462
+ }
3463
+ return record;
3464
+ },
3465
+ async has(nonce) {
3466
+ return await this.get(nonce) !== null;
3467
+ },
3468
+ async consume(nonce) {
3469
+ return kv.update(`${prefix}${nonce}`, (current) => {
3470
+ if (!current) return { op: "noop", result: null };
3471
+ const record = current;
3472
+ if (Date.now() > record.expiresAt) {
3473
+ return { op: "delete", result: null };
3474
+ }
3475
+ return { op: "delete", result: record };
3476
+ });
3477
+ }
3478
+ };
3479
+ }
3480
+ function createAgentIdentityNonceStore(kv) {
3481
+ return kv ? createKvAgentIdentityNonceStore(kv) : new NonceStore();
3482
+ }
3483
+
3318
3484
  // src/protocols/mpp/siwx-mode.ts
3319
3485
  import { Credential as Credential2 } from "mppx";
3320
3486
  async function verifyMppSiwx(request, mppx) {
@@ -3441,6 +3607,7 @@ async function buildSiwxChallenge(ctx) {
3441
3607
  } catch {
3442
3608
  }
3443
3609
  }
3610
+ await attachAgentIdentityChallenge(response, request, deps.agentIdentityNonceStore);
3444
3611
  firePluginResponse(ctx, response);
3445
3612
  return response;
3446
3613
  }
@@ -3560,7 +3727,8 @@ var RouteBuilder = class _RouteBuilder {
3560
3727
  validateFn: void 0,
3561
3728
  settlement: void 0,
3562
3729
  mppInfo: void 0,
3563
- hasCheckout: false
3730
+ hasCheckout: false,
3731
+ checkoutSession: void 0
3564
3732
  };
3565
3733
  }
3566
3734
  fork() {
@@ -3651,7 +3819,8 @@ var RouteBuilder = class _RouteBuilder {
3651
3819
  if (resolvedOptions.minPrice) next.#s.minPrice = resolvedOptions.minPrice;
3652
3820
  if (resolvedOptions.payTo) next.#s.payTo = resolvedOptions.payTo;
3653
3821
  if (resolvedOptions.mpp) next.#s.mppInfo = resolvedOptions.mpp;
3654
- if (resolvedOptions.checkout) next.#s.hasCheckout = true;
3822
+ if (resolvedOptions.checkout || resolvedOptions.checkoutSession) next.#s.hasCheckout = true;
3823
+ if (resolvedOptions.checkoutSession) next.#s.checkoutSession = resolvedOptions.checkoutSession;
3655
3824
  next.#s.billing = billing;
3656
3825
  if (tickCost) next.#s.tickCost = tickCost;
3657
3826
  if (unitType) next.#s.unitType = unitType;
@@ -4086,6 +4255,7 @@ var RouteBuilder = class _RouteBuilder {
4086
4255
  settlement: this.#s.settlement,
4087
4256
  mppInfo: this.#s.mppInfo,
4088
4257
  hasCheckout: this.#s.hasCheckout ? true : void 0,
4258
+ checkoutSession: this.#s.checkoutSession,
4089
4259
  tickCost: this.#s.tickCost,
4090
4260
  unitType: this.#s.unitType
4091
4261
  };
@@ -4433,6 +4603,33 @@ function createLlmsTxtHandler(discovery) {
4433
4603
  };
4434
4604
  }
4435
4605
 
4606
+ // src/discovery/not-found.ts
4607
+ import { NextResponse as NextResponse12 } from "next/server";
4608
+ function createNotFoundHandler(baseUrl) {
4609
+ const normalizedBase = baseUrl.replace(/\/+$/, "");
4610
+ return async (request) => NextResponse12.json(
4611
+ {
4612
+ success: false,
4613
+ code: "route_not_found",
4614
+ error: "Route not found. Rediscover this origin and retry with the current discovery document.",
4615
+ requestedUrl: request.url,
4616
+ discovery: {
4617
+ openapi: `${normalizedBase}/openapi.json`,
4618
+ wellKnown: `${normalizedBase}/.well-known/x402`,
4619
+ llmsTxt: `${normalizedBase}/llms.txt`
4620
+ }
4621
+ },
4622
+ {
4623
+ status: 404,
4624
+ headers: {
4625
+ "Access-Control-Allow-Origin": "*",
4626
+ "Access-Control-Allow-Methods": "GET, POST, DELETE, PUT, PATCH, OPTIONS",
4627
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key, SIGN-IN-WITH-X, X-Agent-Identity"
4628
+ }
4629
+ }
4630
+ );
4631
+ }
4632
+
4436
4633
  // src/index.ts
4437
4634
  init_accepts();
4438
4635
  init_constants();
@@ -5041,6 +5238,7 @@ function createRouter(config) {
5041
5238
  const registry = new RouteRegistry();
5042
5239
  const kvStore = resolveKvStore(config.kvStore);
5043
5240
  const nonceStore = kvStore ? createKvNonceStore(kvStore) : new MemoryNonceStore();
5241
+ const agentIdentityNonceStore = createAgentIdentityNonceStore(kvStore);
5044
5242
  const entitlementStore = kvStore ? createKvEntitlementStore(kvStore) : new MemoryEntitlementStore();
5045
5243
  const network = config.network ?? BASE_MAINNET_NETWORK;
5046
5244
  const x402Accepts = getConfiguredX402Accepts(config);
@@ -5075,6 +5273,7 @@ function createRouter(config) {
5075
5273
  initPromise: Promise.resolve(),
5076
5274
  plugin: config.plugin,
5077
5275
  nonceStore,
5276
+ agentIdentityNonceStore,
5078
5277
  entitlementStore,
5079
5278
  payeeAddress: config.payeeAddress ?? "",
5080
5279
  mppRecipient: config.mpp?.recipient ?? config.payeeAddress,
@@ -5136,6 +5335,9 @@ function createRouter(config) {
5136
5335
  llmsTxt() {
5137
5336
  return createLlmsTxtHandler(config.discovery);
5138
5337
  },
5338
+ notFound() {
5339
+ return createNotFoundHandler(resolvedBaseUrl);
5340
+ },
5139
5341
  monitors() {
5140
5342
  const result = [];
5141
5343
  for (const [, entry] of registry.entries()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentcash/router",
3
- "version": "1.10.5",
3
+ "version": "1.10.7",
4
4
  "description": "Unified route builder for Next.js App Router APIs with x402, MPP, SIWX, and API key auth",
5
5
  "type": "module",
6
6
  "exports": {
@@ -32,6 +32,7 @@
32
32
  "@x402/evm": "^2.13.0",
33
33
  "@x402/extensions": "^2.13.0",
34
34
  "@x402/svm": "^2.13.0",
35
+ "did-auth-challenge": "^0.0.1",
35
36
  "mppx": "^0.6.16",
36
37
  "viem": "^2.47.6",
37
38
  "zod-openapi": "^5.0.0"