mppx 0.8.18 → 0.9.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 (112) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +0 -6
  3. package/dist/Challenge.d.ts +7 -0
  4. package/dist/Challenge.js +32 -6
  5. package/dist/Constants.d.ts +1 -0
  6. package/dist/Constants.js +1 -0
  7. package/dist/Credential.d.ts +11 -5
  8. package/dist/Credential.js +8 -8
  9. package/dist/Html.js +1 -1
  10. package/dist/cli/THIRD-PARTY-LICENSES.txt +1 -1
  11. package/dist/cli/cli.d.ts +2 -2
  12. package/dist/cli/cli.js +134 -134
  13. package/dist/cli/plugins/index.js +13 -13
  14. package/dist/cli/sessions/Manager.js +4 -6
  15. package/dist/cli/sessions/commands.d.ts +1 -1
  16. package/dist/cli/sessions/store.js +13 -19
  17. package/dist/cli/validate/index.d.ts +2 -2
  18. package/dist/client/Mppx.d.ts +39 -2
  19. package/dist/client/Mppx.js +102 -46
  20. package/dist/client/Transport.js +1 -1
  21. package/dist/client/internal/protocols/Mpp.d.ts +1 -1
  22. package/dist/client/internal/protocols/Mpp.js +5 -3
  23. package/dist/client/internal/protocols/Protocol.d.ts +3 -1
  24. package/dist/client/internal/protocols/Shared.js +5 -1
  25. package/dist/integrations/x402/Evm.d.ts +29 -0
  26. package/dist/integrations/x402/Evm.js +150 -0
  27. package/dist/integrations/x402/Negotiator.d.ts +36 -0
  28. package/dist/integrations/x402/Negotiator.js +165 -0
  29. package/dist/mcp/internal/Credential.d.ts +9 -0
  30. package/dist/mcp/internal/Credential.js +16 -0
  31. package/dist/mcp/server/Transport.js +2 -4
  32. package/dist/middlewares/express.js +5 -12
  33. package/dist/middlewares/internal/express.d.ts +8 -0
  34. package/dist/middlewares/internal/express.js +24 -0
  35. package/dist/middlewares/internal/node.d.ts +10 -0
  36. package/dist/middlewares/internal/node.js +34 -0
  37. package/dist/proxy/Proxy.js +15 -9
  38. package/dist/server/Mppx.d.ts +2 -0
  39. package/dist/server/Mppx.js +17 -14
  40. package/dist/server/Transport.d.ts +8 -2
  41. package/dist/server/Transport.js +6 -3
  42. package/dist/server/internal/SecretKey.d.ts +3 -0
  43. package/dist/server/internal/SecretKey.js +9 -0
  44. package/dist/server/internal/html/serviceWorker.client.d.ts +1 -1
  45. package/dist/server/internal/html/serviceWorker.client.js +2 -2
  46. package/dist/server/internal/html/serviceWorker.gen.d.ts +1 -1
  47. package/dist/server/internal/html/serviceWorker.gen.js +1 -1
  48. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  49. package/dist/stripe/server/internal/html.gen.js +1 -1
  50. package/dist/tempo/Methods.d.ts +0 -10
  51. package/dist/tempo/Methods.js +3 -12
  52. package/dist/tempo/client/Charge.d.ts +0 -3
  53. package/dist/tempo/client/Charge.js +3 -13
  54. package/dist/tempo/client/Methods.d.ts +0 -10
  55. package/dist/tempo/internal/account.d.ts +15 -6
  56. package/dist/tempo/internal/account.js +17 -26
  57. package/dist/tempo/internal/defaults.d.ts +0 -11
  58. package/dist/tempo/internal/defaults.js +0 -11
  59. package/dist/tempo/internal/fee-payer.d.ts +21 -3
  60. package/dist/tempo/internal/fee-payer.js +10 -19
  61. package/dist/tempo/internal/remote-fee-payer.d.ts +12 -0
  62. package/dist/tempo/internal/remote-fee-payer.js +17 -0
  63. package/dist/tempo/legacy/client/Session.d.ts +0 -7
  64. package/dist/tempo/server/Charge.d.ts +0 -3
  65. package/dist/tempo/server/Charge.js +26 -68
  66. package/dist/tempo/server/Methods.d.ts +4 -18
  67. package/dist/tempo/server/Methods.js +4 -8
  68. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  69. package/dist/tempo/server/internal/html.gen.js +1 -1
  70. package/dist/tempo/session/client/ChannelOps.d.ts +0 -14
  71. package/dist/tempo/session/client/ChannelOps.js +0 -22
  72. package/dist/tempo/session/client/ChannelStore.d.ts +5 -0
  73. package/dist/tempo/session/client/ChannelStore.js +2 -2
  74. package/dist/tempo/session/client/CredentialState.d.ts +2 -12
  75. package/dist/tempo/session/client/CredentialState.js +75 -73
  76. package/dist/tempo/session/client/Session.d.ts +2 -9
  77. package/dist/tempo/session/client/Session.js +71 -176
  78. package/dist/tempo/session/client/SessionManager.js +9 -28
  79. package/dist/tempo/session/precompile/Chain.d.ts +0 -4
  80. package/dist/tempo/session/precompile/Chain.js +2 -29
  81. package/dist/tempo/session/precompile/Protocol.d.ts +0 -13
  82. package/dist/tempo/session/precompile/Voucher.d.ts +1 -3
  83. package/dist/tempo/session/precompile/Voucher.js +12 -6
  84. package/dist/tempo/session/server/ChannelStore.d.ts +0 -6
  85. package/dist/tempo/session/server/ChannelStore.js +4 -11
  86. package/dist/tempo/session/server/CredentialVerification.d.ts +7 -9
  87. package/dist/tempo/session/server/CredentialVerification.js +118 -184
  88. package/dist/tempo/session/server/RequestState.d.ts +0 -17
  89. package/dist/tempo/session/server/RequestState.js +15 -59
  90. package/dist/tempo/session/server/Session.d.ts +0 -9
  91. package/dist/tempo/session/server/Session.js +8 -37
  92. package/dist/tempo/session/server/Settlement.d.ts +0 -7
  93. package/dist/tempo/session/server/Settlement.js +16 -50
  94. package/dist/viem/Client.d.ts +7 -2
  95. package/dist/viem/Client.js +17 -5
  96. package/dist/x402/Types.d.ts +2 -0
  97. package/dist/x402/Types.js +2 -0
  98. package/dist/x402/mcp.d.ts +20 -0
  99. package/dist/x402/mcp.js +153 -0
  100. package/dist/x402/middlewares/express.d.ts +23 -0
  101. package/dist/x402/middlewares/express.js +49 -0
  102. package/dist/x402/middlewares/hono.d.ts +13 -0
  103. package/dist/x402/middlewares/hono.js +36 -0
  104. package/dist/x402/middlewares/next.d.ts +17 -0
  105. package/dist/x402/middlewares/next.js +58 -0
  106. package/package.json +41 -1
  107. package/dist/tempo/internal/machine-token-charge.d.ts +0 -51
  108. package/dist/tempo/internal/machine-token-charge.js +0 -113
  109. package/dist/tempo/internal/machine-token-session.d.ts +0 -88
  110. package/dist/tempo/internal/machine-token-session.js +0 -173
  111. package/dist/tempo/internal/signature-envelope.d.ts +0 -14
  112. package/dist/tempo/internal/signature-envelope.js +0 -28
@@ -10,16 +10,14 @@ function assertCloseChallengeScope(challenge, channel) {
10
10
  const chainId = challenge.request.methodDetails?.chainId;
11
11
  if (chainId !== undefined && chainId !== channel.chainId)
12
12
  throw new Error('Close challenge changed the session chain.');
13
- if (resolveEscrow(challenge, channel.escrow).toLowerCase() !== channel.escrow.toLowerCase())
14
- throw new Error('Close challenge changed the session escrow.');
15
- const payee = channel.paymentScope?.payee ?? channel.descriptor.payee;
16
- const token = channel.paymentScope?.token ?? channel.descriptor.token;
17
13
  if (typeof challenge.request.recipient !== 'string' ||
18
- challenge.request.recipient.toLowerCase() !== payee.toLowerCase())
14
+ challenge.request.recipient.toLowerCase() !== channel.descriptor.payee.toLowerCase())
19
15
  throw new Error('Close challenge changed the session payee.');
20
16
  if (typeof challenge.request.currency !== 'string' ||
21
- challenge.request.currency.toLowerCase() !== token.toLowerCase())
17
+ challenge.request.currency.toLowerCase() !== channel.descriptor.token.toLowerCase())
22
18
  throw new Error('Close challenge changed the session token.');
19
+ if (resolveEscrow(challenge, channel.escrow).toLowerCase() !== channel.escrow.toLowerCase())
20
+ throw new Error('Close challenge changed the session escrow.');
23
21
  const snapshot = getSessionSnapshot(challenge);
24
22
  if (snapshot && snapshot.channelId.toLowerCase() !== channel.channelId.toLowerCase())
25
23
  throw new Error('Close challenge changed the session channel.');
@@ -31,6 +31,6 @@ declare const sessions: Cli.Cli<{
31
31
  yes: boolean;
32
32
  };
33
33
  };
34
- }, undefined, undefined, undefined>;
34
+ }, undefined, undefined, undefined, "sessions">;
35
35
  export default sessions;
36
36
  //# sourceMappingURL=commands.d.ts.map
@@ -48,10 +48,6 @@ const storedChannelSchema = z.object({
48
48
  escrow: z.address(),
49
49
  chainId: z.number(),
50
50
  opened: z.boolean(),
51
- paymentScope: z.optional(z.object({
52
- payee: z.address(),
53
- token: z.address(),
54
- })),
55
51
  });
56
52
  const accountSchema = z.object({
57
53
  name: z.optional(z.string()),
@@ -118,8 +114,8 @@ export function sessionScopeKey(scope) {
118
114
  export function sessionScope(channel) {
119
115
  return {
120
116
  payer: channel.descriptor.payer,
121
- payee: channel.paymentScope?.payee ?? channel.descriptor.payee,
122
- token: channel.paymentScope?.token ?? channel.descriptor.token,
117
+ payee: channel.descriptor.payee,
118
+ token: channel.descriptor.token,
123
119
  escrow: channel.escrow,
124
120
  chainId: channel.chainId,
125
121
  };
@@ -185,9 +181,9 @@ export function createSessionRegistry(options = {}) {
185
181
  const file = channelFile(paths, normalizedId);
186
182
  const previousValue = await readJson(file);
187
183
  const previous = previousValue === undefined ? undefined : parseStoredSession(previousValue, file);
188
- const challenge = parseSessionChallenge(input.challenge, file);
189
184
  if (previous)
190
185
  assertSameSession(previous, input, file);
186
+ const challenge = parseSessionChallenge(input.challenge, file);
191
187
  assertChallengeMatchesChannel(challenge, channel, file);
192
188
  const receipt = input.receipt
193
189
  ? sanitizeReceipt(input.receipt, channel.channelId, file)
@@ -536,12 +532,6 @@ function channelIdentity(channel) {
536
532
  ])),
537
533
  escrow: channel.escrow.toLowerCase(),
538
534
  chainId: channel.chainId,
539
- paymentScope: channel.paymentScope
540
- ? {
541
- payee: channel.paymentScope.payee.toLowerCase(),
542
- token: channel.paymentScope.token.toLowerCase(),
543
- }
544
- : undefined,
545
535
  };
546
536
  }
547
537
  function assertChallengeMatchesChannel(challenge, channel, file) {
@@ -554,7 +544,10 @@ function assertChallengeMatchesChannel(challenge, channel, file) {
554
544
  throw stateError(file, 'Session channel ID does not match its descriptor.');
555
545
  const payee = normalizeAddress(challenge.request.recipient, 'challenge recipient', file);
556
546
  const token = normalizeAddress(challenge.request.currency, 'challenge currency', file);
557
- const scope = normalizeScope(sessionScope(channel));
547
+ if (payee !== channel.descriptor.payee.toLowerCase())
548
+ throw stateError(file, 'Session challenge payee does not match the channel.');
549
+ if (token !== channel.descriptor.token.toLowerCase())
550
+ throw stateError(file, 'Session challenge token does not match the channel.');
558
551
  if (resolveEscrow(challenge, channel.escrow).toLowerCase() !== channel.escrow.toLowerCase())
559
552
  throw stateError(file, 'Session challenge escrow does not match the channel.');
560
553
  if (isObject(challenge.request.methodDetails)) {
@@ -562,13 +555,14 @@ function assertChallengeMatchesChannel(challenge, channel, file) {
562
555
  if (methodDetails.chainId !== undefined && methodDetails.chainId !== channel.chainId)
563
556
  throw stateError(file, 'Session challenge chain does not match the channel.');
564
557
  }
565
- if (payee !== scope.payee)
566
- throw stateError(file, 'Session challenge payee does not match the channel.');
567
- if (token !== scope.token)
568
- throw stateError(file, 'Session challenge token does not match the channel.');
569
558
  }
570
559
  function assertChannelScope(channel, scope, file) {
571
- if (sessionScopeKey(sessionScope(channel)) !== sessionScopeKey(scope))
560
+ const normalized = normalizeScope(scope);
561
+ if (channel.descriptor.payer.toLowerCase() !== normalized.payer ||
562
+ channel.descriptor.payee.toLowerCase() !== normalized.payee ||
563
+ channel.descriptor.token.toLowerCase() !== normalized.token ||
564
+ channel.escrow.toLowerCase() !== normalized.escrow ||
565
+ channel.chainId !== normalized.chainId)
572
566
  throw stateError(file, 'Session channel does not match the selected payment scope.');
573
567
  }
574
568
  function selectLatestReceipt(previous, next) {
@@ -1,6 +1,6 @@
1
1
  import { Cli } from 'incur';
2
2
  declare const validate: Cli.Cli<{
3
- [x: string]: {
3
+ validate: {
4
4
  args: {
5
5
  url: string;
6
6
  };
@@ -14,6 +14,6 @@ declare const validate: Cli.Cli<{
14
14
  outputJson: boolean;
15
15
  };
16
16
  };
17
- }, undefined, undefined, undefined>;
17
+ }, undefined, undefined, undefined, "validate">;
18
18
  export default validate;
19
19
  //# sourceMappingURL=index.d.ts.map
@@ -7,6 +7,23 @@ import * as Fetch from './internal/Fetch.js';
7
7
  import * as Transport from './Transport.js';
8
8
  export type Methods = readonly (Method.AnyClient | readonly Method.AnyClient[])[];
9
9
  type EventResponseOf<transport extends Transport.AnyTransport> = Response | Transport.ResponseOf<transport>;
10
+ /**
11
+ * A selected payment that can be inspected before its credential is created.
12
+ *
13
+ * Prepared payments retain transport-local protocol state and are not serializable.
14
+ */
15
+ export type PreparedPayment<methods extends readonly Method.AnyClient[] = readonly Method.AnyClient[], transport extends Transport.AnyTransport = Transport.Transport> = Readonly<{
16
+ /** The selected payment challenge. */
17
+ challenge: Challenge.Challenge;
18
+ /** All supported payment challenges extracted from the response. */
19
+ challenges: readonly Challenge.Challenge[];
20
+ /** Creates the selected challenge's credential at most once. */
21
+ createCredential: (context?: AnyContextFor<methods> | undefined) => Promise<string>;
22
+ /** The configured client method selected for the challenge. */
23
+ method: methods[number];
24
+ /** Attaches a credential using the protocol that produced the selected challenge. */
25
+ setCredential: (request: Transport.RequestOf<transport>, credential: string) => Transport.RequestOf<transport>;
26
+ }>;
10
27
  /**
11
28
  * Client-side payment handler.
12
29
  */
@@ -19,8 +36,22 @@ export type Mppx<methods extends Methods = Methods, transport extends Transport.
19
36
  methods: FlattenMethods<methods>;
20
37
  /** The transport used. */
21
38
  transport: transport;
39
+ /**
40
+ * Selects a payment challenge without creating its credential.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * const request = { method: 'POST' }
45
+ * const response = await mppx.rawFetch('/resource', request)
46
+ * const payment = await mppx.preparePayment(response, { request })
47
+ * inspect(payment.challenge)
48
+ * const credential = await payment.createCredential()
49
+ * const init = payment.setCredential(request, credential)
50
+ * ```
51
+ */
52
+ preparePayment: (response: Transport.ResponseOf<transport>, options?: preparePayment.Options<FlattenMethods<methods>, transport> | undefined) => Promise<PreparedPayment<FlattenMethods<methods>, transport>>;
22
53
  /** Creates a credential from a payment-required response by routing to the correct method. */
23
- createCredential: (response: Transport.ResponseOf<transport>, context?: AnyContextFor<FlattenMethods<methods>> | undefined, options?: createCredential.Options<FlattenMethods<methods>> | undefined) => Promise<string>;
54
+ createCredential: (response: Transport.ResponseOf<transport>, context?: AnyContextFor<FlattenMethods<methods>> | undefined, options?: createCredential.Options<FlattenMethods<methods>, transport> | undefined) => Promise<string>;
24
55
  /** Register a client event handler by canonical event name. */
25
56
  on<name extends Fetch.ClientEventName<FlattenMethods<methods>, EventResponseOf<transport>>>(name: name, handler: Fetch.ClientEventHandler<FlattenMethods<methods>, name, EventResponseOf<transport>>): Fetch.Unsubscribe;
26
57
  /** Register a handler for received payment challenges. */
@@ -54,12 +85,18 @@ export type Mppx<methods extends Methods = Methods, transport extends Transport.
54
85
  * ```
55
86
  */
56
87
  export declare function create<const methods extends Methods, const transport extends Transport.Transport<any, any> = Transport.Transport<RequestInit, Response>>(config: create.Config<methods, transport>): Mppx<methods, transport>;
88
+ export declare namespace preparePayment {
89
+ /** Options for selecting a payment without creating its credential. */
90
+ type Options<methods extends readonly Method.AnyClient[] = readonly Method.AnyClient[], transport extends Transport.AnyTransport = Transport.Transport> = createCredential.Options<methods, transport>;
91
+ }
57
92
  export declare namespace createCredential {
58
- type Options<methods extends readonly Method.AnyClient[] = readonly Method.AnyClient[]> = {
93
+ type Options<methods extends readonly Method.AnyClient[] = readonly Method.AnyClient[], transport extends Transport.AnyTransport = Transport.Transport> = {
59
94
  /** Request-local Accept-Payment override for manual rawFetch + createCredential flows. */
60
95
  acceptPayment?: string | readonly AcceptPayment.Entry[] | undefined;
61
96
  /** Request-local challenge filtering and sorting. */
62
97
  orderChallenges?: AcceptPayment.OrderChallenges<methods> | undefined;
98
+ /** Request that produced the response, when challenge extraction depends on both values. */
99
+ request?: Transport.RequestOf<transport> | undefined;
63
100
  };
64
101
  }
65
102
  /**
@@ -62,6 +62,83 @@ export function create(config) {
62
62
  function onPaymentResponse(handler) {
63
63
  return events.on('payment.response', handler);
64
64
  }
65
+ async function preparePayment(response, options) {
66
+ const eventResponse = snapshotResponse(response);
67
+ const challenges = await transport.getChallenges(response, options?.request);
68
+ const challengeSnapshots = Object.freeze(challenges.map((candidate) => snapshotValue(candidate)));
69
+ const preferences = resolveChallengePreferences(acceptPayment.entries, options?.acceptPayment);
70
+ let challenge;
71
+ let mi;
72
+ try {
73
+ const candidates = AcceptPayment.selectChallengeCandidates(challenges, methods, preferences);
74
+ const orderedCandidates = await resolveChallengeOrder(candidates, options?.orderChallenges ?? orderChallenges);
75
+ const selected = orderedCandidates[0];
76
+ if (!selected)
77
+ throw new Error(`No method found for challenges: ${challenges.map((challenge) => `${challenge.method}.${challenge.intent}`).join(', ')}. Available: ${methods.map((m) => `${m.name}.${m.intent}`).join(', ')}`);
78
+ // Signing must use what the caller inspects, while attachment needs the adapter's original
79
+ // object identity to retain protocol provenance.
80
+ const transportChallenge = selected.challenge;
81
+ challenge = challengeSnapshots[selected.index];
82
+ mi = selected.method;
83
+ if (challenge.expires)
84
+ Expires.assert(challenge.expires, challenge.id);
85
+ const selectedChallenge = challenge;
86
+ const selectedMethod = mi;
87
+ const createPreparedCredential = memoizeCreateCredential(async (context) => {
88
+ try {
89
+ if (selectedChallenge.expires)
90
+ Expires.assert(selectedChallenge.expires, selectedChallenge.id);
91
+ const createCredential = memoizeCreateCredential((overrideContext) => createCredentialForMethod(selectedChallenge, selectedMethod, overrideContext ?? context));
92
+ const eventCredential = await events.emit('challenge.received', createChallengeReceivedPayload({
93
+ challenge: selectedChallenge,
94
+ challenges,
95
+ createCredential,
96
+ method: selectedMethod,
97
+ response: eventResponse,
98
+ }));
99
+ const credential = eventCredential ?? (await createCredential());
100
+ Fetch.validateCredentialHeaderValue(credential);
101
+ await events.emit('credential.created', createCredentialCreatedPayload({
102
+ challenge: selectedChallenge,
103
+ credential,
104
+ method: selectedMethod,
105
+ response: eventResponse,
106
+ }));
107
+ return credential;
108
+ }
109
+ catch (error) {
110
+ await events.emit('payment.failed', createPaymentFailedPayload({
111
+ challenge: selectedChallenge,
112
+ challenges,
113
+ error,
114
+ method: selectedMethod,
115
+ response: eventResponse,
116
+ }));
117
+ throw error;
118
+ }
119
+ });
120
+ return Object.freeze({
121
+ challenge: selectedChallenge,
122
+ challenges: challengeSnapshots,
123
+ createCredential: createPreparedCredential,
124
+ method: snapshotMethod(selectedMethod),
125
+ setCredential(request, credential) {
126
+ Fetch.validateCredentialHeaderValue(credential);
127
+ return transport.setCredential(request, credential, { challenge: transportChallenge });
128
+ },
129
+ });
130
+ }
131
+ catch (error) {
132
+ await events.emit('payment.failed', createPaymentFailedPayload({
133
+ challenge,
134
+ challenges,
135
+ error,
136
+ method: mi,
137
+ response: eventResponse,
138
+ }));
139
+ throw error;
140
+ }
141
+ }
65
142
  return {
66
143
  fetch,
67
144
  rawFetch,
@@ -72,50 +149,10 @@ export function create(config) {
72
149
  onCredentialCreated,
73
150
  onPaymentFailed,
74
151
  onPaymentResponse,
152
+ preparePayment,
75
153
  async createCredential(response, context, options) {
76
- const challenges = await transport.getChallenges(response);
77
- const preferences = resolveChallengePreferences(acceptPayment.entries, options?.acceptPayment);
78
- let challenge;
79
- let mi;
80
- try {
81
- const candidates = AcceptPayment.selectChallengeCandidates(challenges, methods, preferences);
82
- const orderedCandidates = await resolveChallengeOrder(candidates, options?.orderChallenges ?? orderChallenges);
83
- const selected = orderedCandidates[0];
84
- if (!selected)
85
- throw new Error(`No method found for challenges: ${challenges.map((challenge) => `${challenge.method}.${challenge.intent}`).join(', ')}. Available: ${methods.map((m) => `${m.name}.${m.intent}`).join(', ')}`);
86
- const selectedChallenge = selected.challenge;
87
- challenge = selectedChallenge;
88
- mi = selected.method;
89
- if (challenge.expires)
90
- Expires.assert(challenge.expires, challenge.id);
91
- const createCredential = memoizeCreateCredential((overrideContext) => createCredentialForMethod(selectedChallenge, mi, overrideContext ?? context));
92
- const eventCredential = await events.emit('challenge.received', createChallengeReceivedPayload({
93
- challenge: selectedChallenge,
94
- challenges,
95
- createCredential,
96
- method: mi,
97
- response,
98
- }));
99
- const credential = eventCredential ?? (await createCredential());
100
- Fetch.validateCredentialHeaderValue(credential);
101
- await events.emit('credential.created', createCredentialCreatedPayload({
102
- challenge: selectedChallenge,
103
- credential,
104
- method: mi,
105
- response,
106
- }));
107
- return credential;
108
- }
109
- catch (error) {
110
- await events.emit('payment.failed', createPaymentFailedPayload({
111
- challenge,
112
- challenges,
113
- error,
114
- method: mi,
115
- response,
116
- }));
117
- throw error;
118
- }
154
+ const prepared = await preparePayment(response, options);
155
+ return prepared.createCredential(context);
119
156
  },
120
157
  };
121
158
  }
@@ -181,13 +218,32 @@ function snapshotMethod(method) {
181
218
  return freezeSnapshot(Object.assign({}, method));
182
219
  }
183
220
  function snapshotResponse(response) {
184
- if (response instanceof Response)
185
- return response.clone();
221
+ if (response instanceof Response) {
222
+ try {
223
+ return response.clone();
224
+ }
225
+ catch {
226
+ // A consumed body cannot be cloned, but status and headers are sufficient for event context.
227
+ return new Response(null, {
228
+ headers: response.headers,
229
+ status: response.status,
230
+ statusText: response.statusText,
231
+ });
232
+ }
233
+ }
186
234
  return snapshotValue(response);
187
235
  }
188
236
  function snapshotValue(value) {
189
237
  try {
190
- return deepFreeze(structuredClone(value));
238
+ const snapshot = structuredClone(value);
239
+ // Protocol adapters may attach non-enumerable symbol metadata to otherwise plain challenges.
240
+ if (value && snapshot && typeof value === 'object' && typeof snapshot === 'object')
241
+ for (const symbol of Object.getOwnPropertySymbols(value)) {
242
+ const descriptor = Object.getOwnPropertyDescriptor(value, symbol);
243
+ if (descriptor)
244
+ Object.defineProperty(snapshot, symbol, descriptor);
245
+ }
246
+ return deepFreeze(snapshot);
191
247
  }
192
248
  catch {
193
249
  return freezeSnapshot(value);
@@ -64,7 +64,7 @@ export function http() {
64
64
  const fallback = protocols[0];
65
65
  if (!protocol && !fallback)
66
66
  throw new Error('No protocol to attach the credential.');
67
- return (protocol ?? fallback).setCredential(request, credential);
67
+ return (protocol ?? fallback).setCredential(request, credential, options);
68
68
  },
69
69
  });
70
70
  }
@@ -1,4 +1,4 @@
1
1
  import type { Protocol } from './Protocol.js';
2
- /** MPP — the native HTTP scheme: a 402 carrying a `WWW-Authenticate` challenge, paid back in `Authorization`. */
2
+ /** MPP — native HTTP Payment authentication. */
3
3
  export declare function mpp(): Protocol;
4
4
  //# sourceMappingURL=Mpp.d.ts.map
@@ -1,7 +1,7 @@
1
1
  import * as Challenge from '../../../Challenge.js';
2
2
  import * as Constants from '../../../Constants.js';
3
3
  import { paymentRequiredStatus, setCredentialHeader } from './Shared.js';
4
- /** MPP — the native HTTP scheme: a 402 carrying a `WWW-Authenticate` challenge, paid back in `Authorization`. */
4
+ /** MPP — native HTTP Payment authentication. */
5
5
  export function mpp() {
6
6
  return {
7
7
  getChallenges(response) {
@@ -10,8 +10,10 @@ export function mpp() {
10
10
  return [];
11
11
  return Challenge.fromResponseList(response);
12
12
  },
13
- setCredential(request, credential) {
14
- return setCredentialHeader(request, Constants.Headers.authorization, credential);
13
+ setCredential(request, credential, options) {
14
+ return setCredentialHeader(request, options?.challenge
15
+ ? Challenge.credentialHeader(options.challenge)
16
+ : Constants.Headers.authorization, credential);
15
17
  },
16
18
  };
17
19
  }
@@ -5,6 +5,8 @@ export type Protocol = {
5
5
  /** This protocol's challenges from a response; `[]` when the response isn't its concern. */
6
6
  getChallenges: (response: Response, request?: RequestInit) => MaybePromise<Challenge.Challenge[]>;
7
7
  /** Attaches this protocol's credential to a retry request. */
8
- setCredential: (request: RequestInit, credential: string) => RequestInit;
8
+ setCredential: (request: RequestInit, credential: string, options?: {
9
+ challenge?: Challenge.Challenge | undefined;
10
+ }) => RequestInit;
9
11
  };
10
12
  //# sourceMappingURL=Protocol.d.ts.map
@@ -12,8 +12,12 @@ const credentialHeaders = [
12
12
  /** Attaches `credential` under `header`, clearing any stale credential headers first. */
13
13
  export function setCredentialHeader(request, header, credential) {
14
14
  const headers = new Headers(request.headers);
15
- for (const stale of credentialHeaders)
15
+ for (const stale of credentialHeaders) {
16
+ // Never erase ordinary application credentials from Authorization.
17
+ if (stale === Constants.Headers.authorization && !headers.get(stale)?.startsWith('Payment '))
18
+ continue;
16
19
  headers.delete(stale);
20
+ }
17
21
  headers.set(header, credential);
18
22
  return { ...request, headers };
19
23
  }
@@ -0,0 +1,29 @@
1
+ import type { HTTPRequestContext, x402ResourceServer } from '@x402/core/server';
2
+ import type { PaymentRequired, PaymentRequirements } from '@x402/core/types';
3
+ /** Configuration shared by the x402 compatibility integrations. */
4
+ export type Config = {
5
+ /** Realm used in MPP challenges. Defaults using the normal mppx realm resolution. */
6
+ realm?: string | undefined;
7
+ /** Secret used to bind MPP challenge IDs to their contents. Must contain at least 32 bytes. */
8
+ secretKey: string;
9
+ /** Existing x402 resource server used for requirement metadata, verification, and settlement. */
10
+ server: x402ResourceServer;
11
+ };
12
+ /** Creates a native MPP handler backed by a compatible x402 EVM requirement. */
13
+ export declare function createHandler(parameters: {
14
+ config: Config;
15
+ context: HTTPRequestContext;
16
+ paymentRequired: PaymentRequired;
17
+ }): ((request: Request) => Promise<{
18
+ payment: {
19
+ challenge: Response;
20
+ status: 402;
21
+ } | {
22
+ status: 200;
23
+ withReceipt: import("../../server/Transport.js").WithReceipt<import("../../server/Transport.js").Http>;
24
+ };
25
+ skipHandlerResponse: Response | undefined;
26
+ }>) | undefined;
27
+ /** Selects an extension-free EIP-3009 requirement that can be represented without semantic loss. */
28
+ export declare function selectRequirement(paymentRequired: PaymentRequired, server: x402ResourceServer): PaymentRequirements | undefined;
29
+ //# sourceMappingURL=Evm.d.ts.map
@@ -0,0 +1,150 @@
1
+ import { formatUnits, getAddress, isAddress } from 'viem';
2
+ import { VerificationFailedError } from '../../Errors.js';
3
+ import { evm } from '../../evm/server/Methods.js';
4
+ import { eip3009 } from '../../evm/Types.js';
5
+ import * as Expires from '../../Expires.js';
6
+ import * as Mppx from '../../server/Mppx.js';
7
+ import { tempo } from '../../tempo/server/Methods.js';
8
+ import * as Types from '../../x402/Types.js';
9
+ /** Creates a native MPP handler backed by a compatible x402 EVM requirement. */
10
+ export function createHandler(parameters) {
11
+ const requirement = selectRequirement(parameters.paymentRequired, parameters.config.server);
12
+ if (!requirement)
13
+ return undefined;
14
+ const chainId = parseChainId(requirement.network);
15
+ if (chainId === undefined)
16
+ return undefined;
17
+ let skipHandlerResponse;
18
+ const decimals = parameters.config.server.getAssetDecimalsForRequirements(requirement);
19
+ const evmMethod = evm.charge({
20
+ authorization: {
21
+ name: requirement.extra.name,
22
+ version: requirement.extra.version,
23
+ },
24
+ chainId,
25
+ currency: normalizeAddress(requirement.asset),
26
+ decimals,
27
+ recipient: normalizeAddress(requirement.payTo),
28
+ settle: async ({ payload }) => {
29
+ const result = await settle({
30
+ context: parameters.context,
31
+ payload,
32
+ paymentRequired: parameters.paymentRequired,
33
+ requirement,
34
+ server: parameters.config.server,
35
+ });
36
+ skipHandlerResponse = result.skipHandlerResponse;
37
+ return { reference: result.reference };
38
+ },
39
+ });
40
+ const payment = Mppx.create({
41
+ methods: [tempo.charge({ recipient: normalizeAddress(requirement.payTo) }), evmMethod],
42
+ ...(parameters.config.realm ? { realm: parameters.config.realm } : {}),
43
+ secretKey: parameters.config.secretKey,
44
+ });
45
+ const options = {
46
+ amount: formatUnits(BigInt(requirement.amount), decimals),
47
+ ...(parameters.paymentRequired.resource.description
48
+ ? { description: parameters.paymentRequired.resource.description }
49
+ : {}),
50
+ expires: Expires.seconds(requirement.maxTimeoutSeconds),
51
+ scope: parameters.paymentRequired.resource.url,
52
+ };
53
+ const handler = payment.compose([payment.tempo.charge, options], [payment.evm.charge, options]);
54
+ return async (request) => ({
55
+ payment: await handler(request),
56
+ skipHandlerResponse,
57
+ });
58
+ }
59
+ /** Selects an extension-free EIP-3009 requirement that can be represented without semantic loss. */
60
+ export function selectRequirement(paymentRequired, server) {
61
+ if (paymentRequired.extensions && Object.keys(paymentRequired.extensions).length > 0)
62
+ return undefined;
63
+ return paymentRequired.accepts.find((requirement) => {
64
+ if (requirement.scheme !== 'exact')
65
+ return false;
66
+ if (parseChainId(requirement.network) === undefined)
67
+ return false;
68
+ const assetTransferMethod = requirement.extra?.assetTransferMethod ??
69
+ server.getRegisteredScheme(requirement.network, requirement.scheme)
70
+ ?.defaultAssetTransferMethod;
71
+ if (assetTransferMethod !== eip3009)
72
+ return false;
73
+ if (!isAddress(requirement.asset, { strict: false }))
74
+ return false;
75
+ if (!isAddress(requirement.payTo, { strict: false }))
76
+ return false;
77
+ return (typeof requirement.extra?.name === 'string' &&
78
+ typeof requirement.extra?.version === 'string' &&
79
+ /^\d+$/.test(requirement.amount));
80
+ });
81
+ }
82
+ function normalizeAddress(address) {
83
+ return getAddress(address.toLowerCase());
84
+ }
85
+ /** Verifies and settles an MPP authorization through the official x402 resource server. */
86
+ async function settle(parameters) {
87
+ const paymentPayload = toPaymentPayload(parameters);
88
+ const transportContext = { request: parameters.context };
89
+ const verified = await parameters.server.verifyPayment(paymentPayload, parameters.requirement, parameters.paymentRequired.extensions, transportContext);
90
+ if (!verified.isValid)
91
+ throw new VerificationFailedError({
92
+ reason: verified.invalidMessage ?? verified.invalidReason ?? 'x402 verification failed',
93
+ });
94
+ const settled = await parameters.server.settlePayment(paymentPayload, parameters.requirement, parameters.paymentRequired.extensions, transportContext);
95
+ if (!settled.success)
96
+ throw new VerificationFailedError({
97
+ reason: settled.errorMessage ?? settled.errorReason ?? 'x402 settlement failed',
98
+ });
99
+ return {
100
+ reference: settled.transaction,
101
+ ...(verified.skipHandler
102
+ ? { skipHandlerResponse: responseFromSkipHandler(verified.skipHandler) }
103
+ : {}),
104
+ };
105
+ }
106
+ /** Converts an MPP EIP-3009 authorization to the x402 facilitator payload shape. */
107
+ function toPaymentPayload(parameters) {
108
+ return {
109
+ accepted: parameters.requirement,
110
+ payload: {
111
+ authorization: {
112
+ from: parameters.payload.from,
113
+ nonce: parameters.payload.nonce,
114
+ to: parameters.payload.to,
115
+ validAfter: parameters.payload.validAfter,
116
+ validBefore: parameters.payload.validBefore,
117
+ value: parameters.payload.value,
118
+ },
119
+ signature: parameters.payload.signature,
120
+ },
121
+ resource: parameters.paymentRequired.resource,
122
+ x402Version: 2,
123
+ };
124
+ }
125
+ function responseFromSkipHandler(directive) {
126
+ const contentType = directive.contentType ?? 'application/json';
127
+ const value = directive.body ?? {};
128
+ const body = contentType.toLowerCase().includes('json')
129
+ ? JSON.stringify(value)
130
+ : value;
131
+ return new Response(body, {
132
+ headers: {
133
+ 'Cache-Control': 'private, no-store',
134
+ 'Content-Type': contentType,
135
+ },
136
+ status: 200,
137
+ });
138
+ }
139
+ function parseChainId(network) {
140
+ if (!network.startsWith(Types.evmNetworkPrefix))
141
+ return undefined;
142
+ const reference = network.slice(Types.evmNetworkPrefix.length);
143
+ if (!/^\d+$/.test(reference))
144
+ return undefined;
145
+ const chainId = Number(reference);
146
+ if (!Number.isSafeInteger(chainId) || chainId <= 0)
147
+ return undefined;
148
+ return chainId;
149
+ }
150
+ //# sourceMappingURL=Evm.js.map
@@ -0,0 +1,36 @@
1
+ import type { HTTPRequestContext, PaywallConfig, RoutesConfig } from '@x402/core/server';
2
+ import { x402HTTPResourceServer } from '@x402/core/server';
3
+ import * as Evm from './Evm.js';
4
+ /** Configuration for MPP negotiation over an existing x402 route table. */
5
+ export type Config = Evm.Config & {
6
+ /** Optional x402 paywall configuration used while resolving payment requirements. */
7
+ paywallConfig?: PaywallConfig | undefined;
8
+ /** Existing x402 route configuration. */
9
+ routes: RoutesConfig;
10
+ };
11
+ /** Decision produced before an official x402 framework adapter runs. */
12
+ export type Result = {
13
+ status: 'handled';
14
+ response: Response;
15
+ } | {
16
+ status: 'paid';
17
+ withReceipt(response: Response): Response;
18
+ } | {
19
+ status: 'unprotected';
20
+ } | {
21
+ status: 'x402';
22
+ };
23
+ /** Shared negotiator used by the x402 compatibility integrations. */
24
+ export type Negotiator = {
25
+ /** x402 HTTP server passed through to official framework adapters. */
26
+ httpServer: x402HTTPResourceServer;
27
+ /** Selects MPP, x402, or an immediate response for a request. */
28
+ negotiate(request: Request, context: HTTPRequestContext): Promise<Result>;
29
+ };
30
+ /** Creates the native MPP side of an x402 compatibility integration. */
31
+ export declare function create(config: Config): Negotiator;
32
+ /** Reads a current or legacy x402 payment credential. */
33
+ export declare function getX402Credential(request: Request): string | undefined;
34
+ /** Converts a Fetch request to the context consumed by x402 core. */
35
+ export declare function createContext(request: Request, body?: unknown): HTTPRequestContext;
36
+ //# sourceMappingURL=Negotiator.d.ts.map