mppx 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/cli/cli.js +137 -131
  3. package/dist/cli/plugins/index.js +11 -11
  4. package/dist/cli/validate/payment.d.ts +12 -0
  5. package/dist/cli/validate/payment.js +113 -14
  6. package/dist/client/Mppx.d.ts +13 -3
  7. package/dist/client/Mppx.js +47 -22
  8. package/dist/client/internal/Fetch.d.ts +11 -0
  9. package/dist/client/internal/Fetch.js +46 -8
  10. package/dist/client/internal/ProofRealm.d.ts +21 -0
  11. package/dist/client/internal/ProofRealm.js +55 -0
  12. package/dist/{attestation → extensions/identity}/Constants.d.ts +2 -2
  13. package/dist/{attestation → extensions/identity}/Constants.js +2 -2
  14. package/dist/extensions/identity/Types.d.ts +21 -0
  15. package/dist/extensions/identity/index.d.ts +7 -0
  16. package/dist/extensions/identity/index.js +6 -0
  17. package/dist/{attestation → extensions/identity}/internal/HttpMessageSignature.d.ts +3 -3
  18. package/dist/{attestation → extensions/identity}/internal/HttpMessageSignature.js +10 -10
  19. package/dist/extensions/identity/internal/SigningContext.d.ts +6 -0
  20. package/dist/{attestation → extensions/identity}/internal/SigningContext.js +3 -3
  21. package/dist/extensions/identity/internal/Verification.d.ts +9 -0
  22. package/dist/extensions/identity/internal/Verification.js +10 -0
  23. package/dist/{tap → extensions/identity/tap}/Constants.js +1 -1
  24. package/dist/extensions/identity/tap/client/index.d.ts +32 -0
  25. package/dist/{tap → extensions/identity/tap}/client/index.js +13 -9
  26. package/dist/extensions/identity/tap/server/index.d.ts +27 -0
  27. package/dist/{tap → extensions/identity/tap}/server/index.js +12 -4
  28. package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Constants.js +1 -1
  29. package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/client/index.d.ts +6 -5
  30. package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/client/index.js +5 -5
  31. package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/server/index.d.ts +6 -5
  32. package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/server/index.js +6 -4
  33. package/dist/identity/Client.d.ts +34 -0
  34. package/dist/identity/Client.js +133 -0
  35. package/dist/identity/Server.d.ts +46 -0
  36. package/dist/identity/Server.js +145 -0
  37. package/dist/identity/Types.d.ts +90 -0
  38. package/dist/identity/Types.js +2 -0
  39. package/dist/{attestation → identity}/index.d.ts +2 -2
  40. package/dist/{attestation → identity}/index.js +1 -2
  41. package/dist/identity/internal/SigningContext.d.ts +6 -0
  42. package/dist/identity/internal/SigningContext.js +11 -0
  43. package/dist/integrations/x402/Evm.d.ts +1 -0
  44. package/dist/internal/version.d.ts +2 -2
  45. package/dist/internal/version.js +1 -1
  46. package/dist/mcp/client/McpClient.d.ts +5 -0
  47. package/dist/mcp/client/McpClient.js +2 -0
  48. package/dist/proxy/Proxy.js +10 -5
  49. package/dist/proxy/Service.d.ts +42 -4
  50. package/dist/proxy/Service.js +21 -5
  51. package/dist/server/Mppx.d.ts +27 -8
  52. package/dist/server/Mppx.js +136 -51
  53. package/dist/stripe/server/internal/html.gen.js +1 -1
  54. package/dist/tempo/internal/charge.js +2 -1
  55. package/dist/tempo/internal/fee-payer.d.ts +0 -2
  56. package/dist/tempo/internal/fee-payer.js +0 -4
  57. package/dist/tempo/server/Subscription.js +0 -1
  58. package/dist/tempo/server/internal/html.gen.js +1 -1
  59. package/dist/tempo/session/client/Runtime.d.ts +2 -0
  60. package/dist/tempo/session/client/Session.js +1 -1
  61. package/dist/tempo/session/client/SessionManager.d.ts +6 -0
  62. package/dist/tempo/session/client/SessionManager.js +14 -7
  63. package/dist/tempo/session/client/Transports.d.ts +12 -3
  64. package/dist/tempo/session/client/Transports.js +37 -11
  65. package/dist/tempo/session/precompile/Chain.js +0 -3
  66. package/dist/tempo/subscription/Store.js +10 -9
  67. package/package.json +15 -11
  68. package/dist/attestation/Client.d.ts +0 -17
  69. package/dist/attestation/Client.js +0 -28
  70. package/dist/attestation/Server.d.ts +0 -4
  71. package/dist/attestation/Server.js +0 -8
  72. package/dist/attestation/Types.d.ts +0 -76
  73. package/dist/attestation/internal/SigningContext.d.ts +0 -4
  74. package/dist/tap/client/index.d.ts +0 -26
  75. package/dist/tap/server/index.d.ts +0 -20
  76. /package/dist/{attestation → extensions/identity}/Types.js +0 -0
  77. /package/dist/{tap → extensions/identity/tap}/Constants.d.ts +0 -0
  78. /package/dist/{tap → extensions/identity/tap}/Types.d.ts +0 -0
  79. /package/dist/{tap → extensions/identity/tap}/Types.js +0 -0
  80. /package/dist/{tap → extensions/identity/tap}/index.d.ts +0 -0
  81. /package/dist/{tap → extensions/identity/tap}/index.js +0 -0
  82. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Constants.d.ts +0 -0
  83. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Types.d.ts +0 -0
  84. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Types.js +0 -0
  85. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/index.d.ts +0 -0
  86. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/index.js +0 -0
  87. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/JwkThumbprint.d.ts +0 -0
  88. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/JwkThumbprint.js +0 -0
  89. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/SignatureAgent.d.ts +0 -0
  90. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/SignatureAgent.js +0 -0
@@ -1,8 +1,8 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
- import type * as Attestation from '../attestation/Types.js';
3
2
  import * as Challenge from '../Challenge.js';
4
3
  import * as Credential from '../Credential.js';
5
4
  import * as Errors from '../Errors.js';
5
+ import type * as Identity from '../identity/Types.js';
6
6
  import type { DeepReadonly, MaybePromise } from '../internal/types.js';
7
7
  import * as Method from '../Method.js';
8
8
  import type * as Receipt from '../Receipt.js';
@@ -277,8 +277,25 @@ type IntentHandlers<methods extends readonly Method.AnyServer[], transport exten
277
277
  intent: intent;
278
278
  }>, transport>;
279
279
  };
280
+ /**
281
+ * Route identity option. HTTP routes accept identity verifiers, all of which must be valid;
282
+ * use `Identity.anyOf` to accept one of several protocols. Other transports reject it.
283
+ */
284
+ type RouteIdentity<transport extends Transport.AnyTransport> = Transport.InputOf<transport> extends Request ? Transport.ChallengeOutputOf<transport> extends Response ? {
285
+ identity?: readonly Identity.Verifier[] | undefined;
286
+ } : {
287
+ identity?: never;
288
+ } : {
289
+ identity?: never;
290
+ };
280
291
  /** Handler for an intent with multiple methods - takes the intersection of all methods' options. */
281
- type MultiMethodFn<methods extends Method.AnyServer, transport extends Transport.AnyTransport> = (options: UnionToIntersection<MultiMethodFnDistribute<methods>>) => (input: Transport.InputOf<transport>) => Promise<MethodFn.Response<transport>>;
292
+ type MultiMethodFn<methods extends Method.AnyServer, transport extends Transport.AnyTransport> = (options: MultiMethodOptions<methods> & RouteIdentity<transport>) => (input: Transport.InputOf<transport>) => Promise<MethodFn.Response<transport>>;
293
+ /**
294
+ * Intersected options of every method for an intent. When a method's options can't be inferred,
295
+ * the intersection is `unknown`; fall back to any object (interfaces included) so `RouteIdentity`
296
+ * doesn't turn it into an object that rejects every option but `identity`.
297
+ */
298
+ type MultiMethodOptions<methods extends Method.AnyServer> = UnionToIntersection<MultiMethodFnDistribute<methods>> extends infer options ? unknown extends options ? Record<string, any> : options : never;
282
299
  type MultiMethodFnDistribute<M> = M extends Method.Server<infer method, infer defaults, any, any, any> ? MethodFn.Options<method, NonNullable<defaults>> : never;
283
300
  type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
284
301
  /** Nested handlers: `mppx.tempo.charge(...)`, grouped by method name then intent. */
@@ -334,8 +351,6 @@ type ChallengeFn<method extends Method.Method, defaults extends Record<string, u
334
351
  export declare function create<const methods extends Methods, const transport extends Transport.AnyTransport = Transport.Http>(config: create.Config<methods, transport>): Mppx<methods, transport>;
335
352
  export declare namespace create {
336
353
  type Config<methods extends Methods = Methods, transport extends Transport.AnyTransport = Transport.Http> = {
337
- /** Request-attestation verifiers required before handling an HTTP payment request. */
338
- attestation?: transport extends Transport.Http ? Attestation.VerifierMap | undefined : never;
339
354
  /** Array of configured methods. @example [tempo()] */
340
355
  methods: methods;
341
356
  /** Uses `Payment-Authorization` for Payment credentials so `Authorization` remains available for application authentication. */
@@ -357,7 +372,7 @@ export declare class MissingReceiptResponseError extends Error {
357
372
  }
358
373
  /** Returns true when an error is the typed `withReceipt()` no-response sentinel. */
359
374
  export declare function isMissingReceiptResponseError(error: unknown): error is MissingReceiptResponseError;
360
- export type MethodFn<method extends Method.Method, transport extends Transport.AnyTransport, defaults extends Record<string, unknown>> = (options: MethodFn.Options<method, defaults>) => (input: Transport.InputOf<transport>) => Promise<MethodFn.Response<transport>>;
375
+ export type MethodFn<method extends Method.Method, transport extends Transport.AnyTransport, defaults extends Record<string, unknown>> = (options: MethodFn.Options<method, defaults> & RouteIdentity<transport>) => (input: Transport.InputOf<transport>) => Promise<MethodFn.Response<transport>>;
361
376
  /** @internal */
362
377
  export type AnyMethodFn = (options: any) => (input: any) => Promise<any>;
363
378
  /** @internal */
@@ -379,6 +394,8 @@ declare namespace MethodFn {
379
394
  status: 200;
380
395
  /** Canonical challenge resolved from the selected route's terms, when available. */
381
396
  challenge?: Challenge.Challenge | undefined;
397
+ /** Verified identity values for the route's verifiers. */
398
+ identity?: Identity.Results | undefined;
382
399
  withReceipt: Transport.WithReceipt<transport>;
383
400
  };
384
401
  }
@@ -395,6 +412,8 @@ type ConfiguredHandler<transport extends Transport.AnyTransport = Transport.Http
395
412
  scope?: string | undefined;
396
413
  _canonicalRequest: Record<string, unknown>;
397
414
  _stableBinding?: Method.StableBindingFn<Method.Method> | undefined;
415
+ /** Whether the offer requires payment-stage identity. */
416
+ _paymentIdentity?: boolean | undefined;
398
417
  _transport: Transport.AnyTransport;
399
418
  };
400
419
  };
@@ -409,19 +428,19 @@ type ComposedHandler<transport extends Transport.AnyTransport = Transport.Http>
409
428
  type ComposeEntry<methods extends readonly Method.AnyServer[]> = {
410
429
  [i in keyof methods]: readonly [
411
430
  methods[i],
412
- MethodFn.Options<methods[i], NonNullable<methods[i]['defaults']>>
431
+ MethodFn.Options<methods[i], NonNullable<methods[i]['defaults']>> & RouteIdentity<Transport.Http>
413
432
  ];
414
433
  }[number] | {
415
434
  [i in keyof methods]: readonly [
416
435
  `${methods[i]['name']}/${methods[i]['intent']}`,
417
- MethodFn.Options<methods[i], NonNullable<methods[i]['defaults']>>
436
+ MethodFn.Options<methods[i], NonNullable<methods[i]['defaults']>> & RouteIdentity<Transport.Http>
418
437
  ];
419
438
  }[number] | {
420
439
  [i in keyof methods]: readonly [
421
440
  MethodFn<methods[i], any, any> & {
422
441
  _method: methods[i];
423
442
  },
424
- MethodFn.Options<methods[i], NonNullable<methods[i]['defaults']>>
443
+ MethodFn.Options<methods[i], NonNullable<methods[i]['defaults']>> & RouteIdentity<Transport.Http>
425
444
  ];
426
445
  }[number];
427
446
  export declare function compose(...args: readonly unknown[]): ComposedHandler;
@@ -1,10 +1,10 @@
1
1
  import { isDeepStrictEqual } from 'node:util';
2
- import * as AttestationServer from '../attestation/Server.js';
3
2
  import * as Challenge from '../Challenge.js';
4
3
  import * as Constants from '../Constants.js';
5
4
  import * as Credential from '../Credential.js';
6
5
  import * as Errors from '../Errors.js';
7
6
  import * as Expires from '../Expires.js';
7
+ import * as IdentityServer from '../identity/Server.js';
8
8
  import * as AcceptPayment from '../internal/AcceptPayment.js';
9
9
  import * as Env from '../internal/env.js';
10
10
  import { warnOnce } from '../internal/warnOnce.js';
@@ -59,17 +59,19 @@ export const reservedMppxKeys = new Set(reservedMppxKeyValues);
59
59
  * ```
60
60
  */
61
61
  export function create(config) {
62
- const { attestation, requiresAuth, realm = Env.get('realm'), selectOffers, secretKey = Env.get('secretKey'), transport = Transport.http({ requiresAuth }), } = config;
62
+ const { requiresAuth, realm = Env.get('realm'), selectOffers, secretKey = Env.get('secretKey'), transport = Transport.http({ requiresAuth }), } = config;
63
63
  const credentialHeader = requiresAuth ? Constants.Headers.paymentAuthorization : undefined;
64
+ if ('attestation' in config)
65
+ throw new TypeError('Mppx server `attestation` was replaced by per-route `identity`: pass `identity: [verifier]` to each route.');
66
+ // Identity is configured per route; a top-level option would otherwise be ignored (fail open).
67
+ if ('identity' in config)
68
+ throw new TypeError('Mppx server identity is configured per route: pass `identity: [verifier]` to each route.');
64
69
  if (!secretKey) {
65
70
  throw new Error('Missing secret key. Set the MPP_SECRET_KEY environment variable or pass `secretKey` to Mppx.create().');
66
71
  }
67
72
  SecretKey.assert(secretKey);
68
73
  const methods = config.methods.flat();
69
74
  const serverEvents = createServerEventDispatcher();
70
- const verifyAttestation = attestation
71
- ? createAttestationGuard(attestation)
72
- : undefined;
73
75
  const handlers = {};
74
76
  const methodHandlers = new Map();
75
77
  const intentCount = {};
@@ -87,7 +89,6 @@ export function create(config) {
87
89
  assertNoReservedMppxKeys(methods);
88
90
  for (const mi of methods) {
89
91
  const fn = createMethodFn({
90
- ...(verifyAttestation && { attest: verifyAttestation }),
91
92
  authorize: mi.authorize,
92
93
  credentialHeader,
93
94
  defaults: mi.defaults,
@@ -436,8 +437,19 @@ export function create(config) {
436
437
  }
437
438
  // biome-ignore lint/correctness/noUnusedVariables: _
438
439
  function createMethodFn(parameters) {
439
- const { attest, authorize, credentialHeader, defaults, events, method, preflight, realm, respond, secretKey, broadcast, stableBinding, transport, validate, verify, } = parameters;
440
- return (options) => {
440
+ const { authorize, credentialHeader, defaults, events, method, preflight, realm, respond, secretKey, broadcast, stableBinding, transport, validate, verify, } = parameters;
441
+ return (routeOptions) => {
442
+ const { identity = [], ...options } = routeOptions;
443
+ const requestVerifiers = identity.filter((verifier) => IdentityServer.stageOf(verifier) === 'request');
444
+ const paymentVerifiers = identity.filter((verifier) => IdentityServer.stageOf(verifier) === 'payment');
445
+ // Preflight hooks (e.g. session bootstrap) issue and settle their own challenges, which
446
+ // payment-stage verifiers cannot see; reject the combination instead of skipping identity.
447
+ if (paymentVerifiers.length > 0 && preflight)
448
+ throw new TypeError(`Payment-stage identity is not supported on ${method.name}/${method.intent} routes with a preflight hook.`);
449
+ // The HTML checkout cannot answer identity challenges, and it pays before resubmitting, so
450
+ // a browser would pay and then be rejected.
451
+ if (paymentVerifiers.length > 0 && method.html)
452
+ throw new TypeError(`Payment-stage identity is not supported on ${method.name}/${method.intent} routes with the HTML checkout.`);
441
453
  const { description, meta, scope, ...rest } = options;
442
454
  const staticMeta = Scope.merge({ meta, scope });
443
455
  const internal = {
@@ -455,20 +467,26 @@ function createMethodFn(parameters) {
455
467
  ]),
456
468
  ],
457
469
  _canonicalRequest: PaymentRequest.fromMethod(method, { ...defaults, ...rest }),
470
+ _paymentIdentity: paymentVerifiers.length > 0,
458
471
  _stableBinding: stableBinding,
459
472
  _transport: transport,
460
473
  };
461
474
  Object.defineProperty(internal, '_method', { value: method });
462
475
  const handler = async (input) => {
463
- if (attest && input instanceof globalThis.Request) {
464
- const response = await attest(input);
465
- if (response)
466
- return {
467
- status: 200,
468
- withReceipt() {
469
- return response;
470
- },
471
- };
476
+ const identityValues = new Map();
477
+ if (identity.length > 0) {
478
+ if (!(input instanceof globalThis.Request))
479
+ throw new TypeError('Route identity requires the HTTP transport.');
480
+ const outcome = await IdentityServer.verify(input, requestVerifiers);
481
+ if (outcome.status !== 'valid') {
482
+ // Returned on the non-200 branch so callers never run paid work: like an early
483
+ // rejection, `challenge` holds the identity response (401/403) to send as is.
484
+ const rejection = IdentityServer.toResponse(outcome);
485
+ identityRejections.add(rejection);
486
+ return { challenge: rejection, status: 402 };
487
+ }
488
+ for (const [verifier, value] of outcome.values)
489
+ identityValues.set(verifier, value);
472
490
  }
473
491
  if (method.html && isServiceWorkerRequest(input))
474
492
  return {
@@ -507,6 +525,7 @@ function createMethodFn(parameters) {
507
525
  return { challenge: response, status: 402 };
508
526
  return {
509
527
  status: 200,
528
+ identity: IdentityServer.results(identityValues),
510
529
  withReceipt() {
511
530
  return response;
512
531
  },
@@ -514,12 +533,35 @@ function createMethodFn(parameters) {
514
533
  }
515
534
  }
516
535
  const emitChallenge = async (parameters) => {
517
- const response = await transport.respondChallenge({
536
+ let response = await transport.respondChallenge({
518
537
  challenge: parameters.challenge,
519
538
  input,
520
539
  ...(parameters.error && { error: parameters.error }),
521
540
  ...(parameters.html && { html: parameters.html }),
522
541
  });
542
+ // Payment-stage verifiers add their own challenge headers to the 402.
543
+ if (paymentVerifiers.length > 0 &&
544
+ input instanceof globalThis.Request &&
545
+ response instanceof globalThis.Response) {
546
+ const outcome = await IdentityServer.verify(input, paymentVerifiers, {
547
+ challenge: parameters.challenge,
548
+ });
549
+ const identityHeaders = new Headers();
550
+ if (outcome.status !== 'valid')
551
+ for (const [name, value] of outcome.headers)
552
+ identityHeaders.append(name, value);
553
+ if ([...identityHeaders].length > 0) {
554
+ const headers = new Headers(response.headers);
555
+ for (const [name, value] of identityHeaders)
556
+ headers.append(name, value);
557
+ response = new globalThis.Response(response.body, {
558
+ headers,
559
+ status: response.status,
560
+ statusText: response.statusText,
561
+ });
562
+ identityChallenges.set(response, identityHeaders);
563
+ }
564
+ }
523
565
  if (isIssuedChallengeResponse(response))
524
566
  await events.emit('challenge.created', createChallengeContext({
525
567
  capturedRequest,
@@ -637,6 +679,7 @@ function createMethodFn(parameters) {
637
679
  const result = {
638
680
  status: 200,
639
681
  challenge,
682
+ identity: IdentityServer.results(identityValues),
640
683
  withReceipt(response) {
641
684
  if (managementResponse) {
642
685
  return transport.respondReceipt({
@@ -666,7 +709,17 @@ function createMethodFn(parameters) {
666
709
  };
667
710
  // No credential provided—issue challenge
668
711
  if (!credential) {
669
- if (authorize && input instanceof globalThis.Request) {
712
+ // Authorization without a payment credential still requires payment-stage identity;
713
+ // a verifier that needs the credential reports missing, so the route issues its 402.
714
+ const authorizeIdentity = authorize && input instanceof globalThis.Request && paymentVerifiers.length > 0
715
+ ? await IdentityServer.verify(input, paymentVerifiers, { challenge })
716
+ : undefined;
717
+ if (authorize &&
718
+ input instanceof globalThis.Request &&
719
+ (!authorizeIdentity || authorizeIdentity.status === 'valid')) {
720
+ if (authorizeIdentity?.status === 'valid')
721
+ for (const [verifier, value] of authorizeIdentity.values)
722
+ identityValues.set(verifier, value);
670
723
  try {
671
724
  const authorized = await authorize({
672
725
  challenge,
@@ -839,6 +892,33 @@ function createMethodFn(parameters) {
839
892
  credential: parsedCredential,
840
893
  request: parsedRequest,
841
894
  });
895
+ // Payment-stage identity is checked against the answered challenge before settlement.
896
+ if (paymentVerifiers.length > 0) {
897
+ const outcome = await IdentityServer.verify(input, paymentVerifiers, {
898
+ challenge: credential.challenge,
899
+ credential: parsedCredential,
900
+ });
901
+ if (outcome.status !== 'valid') {
902
+ const error = new Errors.VerificationFailedError({ reason: outcome.reason });
903
+ await emitPaymentFailed({
904
+ challenge,
905
+ credential: parsedCredential,
906
+ error,
907
+ request: parsedRequest,
908
+ retryChallenge: challenge,
909
+ submittedChallenge: credential.challenge,
910
+ });
911
+ // The client may already have pushed this payment, so a fresh 402 would invite
912
+ // it to pay again. Reject without a challenge, like a replayed credential.
913
+ // Returned on the non-200 branch so callers never run paid work: like an early
914
+ // rejection, `challenge` holds the identity response (401/403) to send as is.
915
+ const rejection = IdentityServer.toResponse(outcome);
916
+ identityRejections.add(rejection);
917
+ return { challenge: rejection, status: 402 };
918
+ }
919
+ for (const [verifier, value] of outcome.values)
920
+ identityValues.set(verifier, value);
921
+ }
842
922
  // User-provided verification (e.g., check signature, submit tx, verify payment).
843
923
  // If verification fails, re-issue the challenge so the client can retry.
844
924
  let receiptData;
@@ -922,7 +1002,7 @@ function createMethodFn(parameters) {
922
1002
  function createChallengeFn(parameters) {
923
1003
  const { credentialHeader, defaults, method, realm, secretKey } = parameters;
924
1004
  return async (options) => {
925
- const { description, meta, scope, ...rest } = options;
1005
+ const { description, identity: _, meta, scope, ...rest } = options;
926
1006
  const effectiveMeta = Scope.merge({ meta, scope });
927
1007
  const expires = 'expires' in options
928
1008
  ? normalizeExpires(options.expires)
@@ -941,33 +1021,6 @@ function createChallengeFn(parameters) {
941
1021
  }).then((resolved) => resolved.challenge);
942
1022
  };
943
1023
  }
944
- function createAttestationGuard(verifiers) {
945
- const entries = Object.entries(verifiers);
946
- if (entries.length === 0)
947
- throw new TypeError('Mppx server attestation must configure at least one verifier.');
948
- const cache = new WeakMap();
949
- return (request) => {
950
- const cached = cache.get(request);
951
- if (cached)
952
- return cached;
953
- const verification = AttestationServer.verify(request, verifiers).then((outcomes) => {
954
- for (const [name, outcome] of Object.entries(outcomes))
955
- if (outcome.status === 'invalid')
956
- return new globalThis.Response(`Request attestation "${name}" is invalid: ${outcome.reason}`, { status: 401 });
957
- for (const [name, outcome] of Object.entries(outcomes)) {
958
- if (outcome.status === 'verified')
959
- continue;
960
- const reason = outcome.status === 'unverified'
961
- ? `Request attestation "${name}" could not be verified: ${outcome.reason}`
962
- : `Request attestation "${name}" is required.`;
963
- return new globalThis.Response(reason, { status: 403 });
964
- }
965
- return undefined;
966
- });
967
- cache.set(request, verification);
968
- return verification;
969
- };
970
- }
971
1024
  /** Dispatches instance events and scopes success hooks to the supplied configured method. */
972
1025
  function createServerEventDispatcher() {
973
1026
  const handlers = {
@@ -1531,6 +1584,16 @@ function withParsedCredentialPayload(credential, payload) {
1531
1584
  };
1532
1585
  }
1533
1586
  const paymentAuthChallengeHeader = Constants.Headers.wwwAuthenticate;
1587
+ /** Payment-stage identity headers added to issued 402 responses, keyed by response. @internal */
1588
+ const identityChallenges = new WeakMap();
1589
+ /** Identity rejections (401/403) returned on a route's non-success branch. @internal */
1590
+ const identityRejections = new WeakSet();
1591
+ /** Records a merged 402's identity challenge so an outer composer can forward it. @internal */
1592
+ function withIdentityChallenge(response, headers) {
1593
+ if (headers)
1594
+ identityChallenges.set(response, headers);
1595
+ return response;
1596
+ }
1534
1597
  const challengeHeaderMerges = [
1535
1598
  {
1536
1599
  name: paymentAuthChallengeHeader,
@@ -1576,6 +1639,9 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
1576
1639
  });
1577
1640
  if (handlers.length === 0)
1578
1641
  throw new Error('compose() requires at least one handler');
1642
+ // A 402 carries one identity challenge, bound to one payment challenge.
1643
+ if (handlers.flatMap(getConfiguredOffers).filter((offer) => offer._paymentIdentity).length > 1)
1644
+ throw new TypeError('A composed route can include at most one offer with payment-stage identity.');
1579
1645
  const offerSelector = selectOffers;
1580
1646
  const offerEntries = handlers.map((handler) => {
1581
1647
  const internal = handler._internal;
@@ -1684,21 +1750,30 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
1684
1750
  return transportCredentialHandler(input);
1685
1751
  // No credential — evaluate handlers sequentially so authorize()/renewal hooks
1686
1752
  // can safely claim the request without racing each other.
1753
+ // An identity rejection from one offer does not hide the others: it is returned only
1754
+ // when no offer issued a payment challenge.
1687
1755
  const results = [];
1688
1756
  for (const handler of selectedHandlers) {
1689
1757
  const result = transportResults.get(handler) ?? (await handler(input));
1690
1758
  if (result.status === 200)
1691
1759
  return result;
1692
- if (result.challenge.status !== 402)
1760
+ const rejected = identityRejections.has(result.challenge);
1761
+ if (result.challenge.status !== 402 && !rejected)
1693
1762
  return result;
1694
1763
  results.push(result);
1695
1764
  }
1765
+ const offers = results.filter((result) => result.status === 402 && !identityRejections.has(result.challenge));
1766
+ if (offers.length === 0 && results[0])
1767
+ return results[0];
1696
1768
  const challengeEntries = (() => {
1697
1769
  const entries = [];
1698
1770
  for (let i = 0; i < selectedHandlers.length; i++) {
1699
1771
  const result = results[i];
1700
1772
  if (result?.status !== 402)
1701
1773
  continue;
1774
+ // Identity rejections may carry non-Payment challenges (e.g. Bearer); they are not offers.
1775
+ if (identityRejections.has(result.challenge))
1776
+ continue;
1702
1777
  const response = result.challenge;
1703
1778
  const wwwAuth = response.headers.get(paymentAuthChallengeHeader);
1704
1779
  if (!wwwAuth)
@@ -1723,7 +1798,7 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
1723
1798
  return entries;
1724
1799
  }
1725
1800
  })();
1726
- const challengeResponses = results.flatMap((result) => result.status === 402 ? [result.challenge] : []);
1801
+ const challengeResponses = offers.map((result) => result.challenge);
1727
1802
  const unnegotiatedX402Responses = input.headers.has(Constants.Headers.acceptPayment) || challengeEntries.length === 0
1728
1803
  ? []
1729
1804
  : challengeResponses.filter((response) => response.headers.has(x402_Types.paymentRequiredHeader) &&
@@ -1734,6 +1809,11 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
1734
1809
  ...unnegotiatedX402Responses,
1735
1810
  ]
1736
1811
  : challengeResponses;
1812
+ // At most one offer carries an identity challenge (enforced when composing); forward it
1813
+ // only if its payment challenge is still advertised after negotiation.
1814
+ const identityHeaders = negotiatedChallengeResponses
1815
+ .map((response) => identityChallenges.get(response))
1816
+ .find((headers) => headers !== undefined);
1737
1817
  // Merge challenge headers from the negotiated 402 responses.
1738
1818
  const mergedHeaders = new Headers();
1739
1819
  mergedHeaders.set('Cache-Control', 'no-store');
@@ -1746,6 +1826,11 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
1746
1826
  mergedHeaders.append(header.name, value);
1747
1827
  }
1748
1828
  }
1829
+ // Identity WWW-Authenticate values already arrive in the merged offer value above.
1830
+ if (identityHeaders)
1831
+ for (const [name, value] of identityHeaders)
1832
+ if (name !== paymentAuthChallengeHeader.toLowerCase())
1833
+ mergedHeaders.append(name, value);
1749
1834
  // Collect html-enabled handlers and their challenges
1750
1835
  const htmlEntries = challengeEntries.filter((entry) => entry.handler._internal?.html);
1751
1836
  const wantsHtml = input.headers.get('Accept')?.includes('text/html');
@@ -1782,7 +1867,7 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
1782
1867
  });
1783
1868
  return {
1784
1869
  status: 402,
1785
- challenge: new Response(body, { status: 402, headers: mergedHeaders }),
1870
+ challenge: withIdentityChallenge(new Response(body, { status: 402, headers: mergedHeaders }), identityHeaders),
1786
1871
  };
1787
1872
  }
1788
1873
  // Non-HTML fallback: prefer the first Payment-auth body, otherwise use
@@ -1802,7 +1887,7 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
1802
1887
  }
1803
1888
  return {
1804
1889
  status: 402,
1805
- challenge: new Response(body, { status: 402, headers: mergedHeaders }),
1890
+ challenge: withIdentityChallenge(new Response(body, { status: 402, headers: mergedHeaders }), identityHeaders),
1806
1891
  };
1807
1892
  };
1808
1893
  const offers = handlers.flatMap(getConfiguredOffers);