mppx 0.9.1 → 0.9.3

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 (73) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +26 -0
  3. package/dist/Method.d.ts +2 -0
  4. package/dist/cli/Extension.d.ts +20 -0
  5. package/dist/cli/Extension.js +5 -0
  6. package/dist/cli/cli.d.ts +1 -0
  7. package/dist/cli/cli.js +135 -135
  8. package/dist/cli/config.d.ts +4 -0
  9. package/dist/cli/config.js +1 -0
  10. package/dist/cli/internal.d.ts +3 -0
  11. package/dist/cli/internal.js +15 -2
  12. package/dist/cli/plugins/evm.js +2 -0
  13. package/dist/cli/plugins/index.d.ts +1 -0
  14. package/dist/cli/plugins/index.js +13 -13
  15. package/dist/cli/plugins/plugin.d.ts +2 -2
  16. package/dist/cli/plugins/x402.d.ts +3 -0
  17. package/dist/cli/plugins/x402.js +61 -0
  18. package/dist/cli/sessions/request.d.ts +1 -0
  19. package/dist/cli/sessions/request.js +1 -0
  20. package/dist/cli/validate/payment.js +14 -8
  21. package/dist/evm/client/Charge.d.ts +2 -2
  22. package/dist/evm/client/Charge.js +1 -1
  23. package/dist/evm/server/Charge.d.ts +7 -2
  24. package/dist/evm/server/Charge.js +28 -2
  25. package/dist/evm/server/Methods.d.ts +1 -1
  26. package/dist/internal/version.d.ts +5 -0
  27. package/dist/internal/version.js +6 -0
  28. package/dist/middlewares/internal/mppx.js +17 -3
  29. package/dist/server/Mppx.d.ts +7 -6
  30. package/dist/server/Mppx.js +84 -2
  31. package/dist/server/internal/html/config.js +1 -3
  32. package/dist/stripe/Methods.d.ts +24 -5
  33. package/dist/stripe/Methods.js +14 -11
  34. package/dist/stripe/client/Charge.d.ts +23 -5
  35. package/dist/stripe/client/Methods.d.ts +23 -5
  36. package/dist/stripe/internal/constants.d.ts +4 -0
  37. package/dist/stripe/internal/constants.js +2 -0
  38. package/dist/stripe/internal/parse-units.d.ts +8 -0
  39. package/dist/stripe/internal/parse-units.js +20 -0
  40. package/dist/stripe/internal/payment-intent.d.ts +39 -1
  41. package/dist/stripe/internal/payment-intent.js +16 -1
  42. package/dist/stripe/internal/types.d.ts +4 -0
  43. package/dist/stripe/server/Charge.d.ts +25 -5
  44. package/dist/stripe/server/Charge.js +35 -23
  45. package/dist/stripe/server/Methods.d.ts +8 -0
  46. package/dist/stripe/server/Methods.js +54 -9
  47. package/dist/stripe/server/internal/analytics.d.ts +6 -0
  48. package/dist/stripe/server/internal/analytics.js +16 -0
  49. package/dist/stripe/server/internal/hosted-fee-payer.d.ts +10 -0
  50. package/dist/stripe/server/internal/hosted-fee-payer.js +54 -0
  51. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  52. package/dist/stripe/server/internal/html.gen.js +1 -1
  53. package/dist/stripe/server/internal/record-payment.d.ts +4 -1
  54. package/dist/stripe/server/internal/record-payment.js +35 -5
  55. package/dist/tempo/client/Subscription.js +6 -2
  56. package/dist/tempo/internal/account.d.ts +1 -0
  57. package/dist/tempo/internal/fee-payer.d.ts +2 -0
  58. package/dist/tempo/internal/fee-payer.js +4 -0
  59. package/dist/tempo/internal/remote-fee-payer.d.ts +2 -0
  60. package/dist/tempo/server/Subscription.js +1 -0
  61. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  62. package/dist/tempo/server/internal/html.gen.js +1 -1
  63. package/dist/tempo/session/client/SessionManager.d.ts +5 -0
  64. package/dist/tempo/session/client/SessionManager.js +16 -2
  65. package/dist/tempo/session/precompile/Chain.js +3 -0
  66. package/dist/tempo/subscription/KeyAuthorization.d.ts +7 -1
  67. package/dist/tempo/subscription/KeyAuthorization.js +21 -3
  68. package/dist/viem/Client.js +1 -0
  69. package/dist/x402/client/Exact.d.ts +2 -2
  70. package/dist/x402/client/Exact.js +1 -1
  71. package/dist/x402/server/EvmCharge.d.ts +2 -0
  72. package/dist/x402/server/EvmCharge.js +34 -26
  73. package/package.json +1 -1
@@ -33,8 +33,8 @@ export interface Plugin {
33
33
  methods: Method.AnyClient[];
34
34
  /** Optional context to pass to createCredential */
35
35
  credentialContext?: unknown;
36
- /** Override credential creation entirely (e.g., delegating to an external CLI) */
37
- createCredential?: ((response: Response) => Promise<string>) | undefined;
36
+ /** Override credential creation entirely (e.g., delegating to an external CLI). */
37
+ createCredential?: ((response: Response, credentialContext?: unknown) => Promise<string>) | undefined;
38
38
  }>;
39
39
  /**
40
40
  * Modify the credential request before sending.
@@ -0,0 +1,3 @@
1
+ /** Pays recognized x402 exact EVM challenges with the configured CLI account. */
2
+ export declare function x402(): import("./plugin.js").Plugin;
3
+ //# sourceMappingURL=x402.d.ts.map
@@ -0,0 +1,61 @@
1
+ import * as viemChains from 'viem/chains';
2
+ import { evm as evmMethods, assets as evmAssets } from '../../evm/client/index.js';
3
+ import * as Assets from '../../x402/Assets.js';
4
+ import * as x402_ChallengeBrand from '../../x402/internal/ChallengeBrand.js';
5
+ import { resolveAccount } from '../account.js';
6
+ import { createPlugin } from './plugin.js';
7
+ const knownAssets = [
8
+ ...Object.entries(evmAssets.base),
9
+ ...Object.entries(evmAssets.baseSepolia),
10
+ ...Object.entries(evmAssets.celo),
11
+ ...Object.entries(evmAssets.celoSepolia),
12
+ ];
13
+ /** Pays recognized x402 exact EVM challenges with the configured CLI account. */
14
+ export function x402() {
15
+ return createPlugin({
16
+ method: 'evm',
17
+ supports: isSupportedChallenge,
18
+ async setup({ challenge, options }) {
19
+ const request = challenge.request;
20
+ const chain = request.network ? resolveEvmChain(Assets.toChainId(request.network)) : undefined;
21
+ const known = request.network && request.asset
22
+ ? knownAssets.find(([, asset]) => Assets.matches(asset, request.asset, request.network))
23
+ : undefined;
24
+ const name = request.extra?.name;
25
+ const version = request.extra?.version;
26
+ const currency = request.asset && request.network && typeof name === 'string' && typeof version === 'string'
27
+ ? Assets.define({
28
+ address: request.asset,
29
+ decimals: known?.[1].decimals ?? 6,
30
+ network: request.network,
31
+ transfer: { name, type: 'eip3009', version },
32
+ })
33
+ : undefined;
34
+ const account = await resolveAccount(options.account);
35
+ return {
36
+ explorerUrl: chain?.blockExplorers?.default?.url,
37
+ methods: [...evmMethods({ account, ...(currency && { currencies: [currency] }) })],
38
+ tokenDecimals: known?.[1].decimals ?? 6,
39
+ tokenSymbol: known?.[0] ?? '',
40
+ };
41
+ },
42
+ });
43
+ }
44
+ /** Returns whether the CLI can create an x402 credential for this challenge. */
45
+ function isSupportedChallenge(challenge) {
46
+ if (!x402_ChallengeBrand.is(challenge))
47
+ return false;
48
+ const request = challenge.request;
49
+ const transfer = request.extra;
50
+ const transferMethod = transfer?.assetTransferMethod;
51
+ return (request.scheme === 'exact' &&
52
+ typeof request.asset === 'string' &&
53
+ typeof request.network === 'string' &&
54
+ typeof transfer?.name === 'string' &&
55
+ typeof transfer?.version === 'string' &&
56
+ (transferMethod === undefined || transferMethod === 'eip3009'));
57
+ }
58
+ function resolveEvmChain(chainId) {
59
+ return Object.values(viemChains).find((chain) => chain.id === chainId);
60
+ }
61
+ //# sourceMappingURL=x402.js.map
@@ -17,6 +17,7 @@ export type PersistentSessionRequestOptions = {
17
17
  export type PersistentSessionRequestParameters = {
18
18
  challenge: TempoSessionChallenge;
19
19
  challengeResponse: Response;
20
+ credentialContext?: unknown;
20
21
  endpoint: string;
21
22
  fetch: typeof globalThis.fetch;
22
23
  fetchInput: RequestInfo | URL;
@@ -198,6 +198,7 @@ export async function runPersistentSessionRequest(parameters) {
198
198
  bootstrap: false,
199
199
  client,
200
200
  channelStore,
201
+ credentialContext: parameters.credentialContext,
201
202
  decimals: sessionDecimals(parameters.challenge),
202
203
  maxDeposit: resolveSessionMaxDeposit(parameters.challenge, parameters.methodOptions, isTestnet(chain)),
203
204
  fetch: async (input, init) => {
@@ -12,7 +12,7 @@ import * as Receipt from '../../Receipt.js';
12
12
  import { tempo as tempoMethods } from '../../tempo/client/index.js';
13
13
  import { chainId as tempoChainIds } from '../../tempo/internal/defaults.js';
14
14
  import { resolveAccount, resolveAccountName } from '../account.js';
15
- import { loadConfig, resolvePlugin } from '../internal.js';
15
+ import { loadConfig, preparePayment, resolvePlugin } from '../internal.js';
16
16
  import { fetchTokenInfo, confirm, pc } from '../utils.js';
17
17
  import { buildUrl } from './discovery.js';
18
18
  import { check, fail, fetchWithTimeout, formatBytes, isValidIntegerAmount, parseHeaders, skip, warn, } from './helpers.js';
@@ -151,6 +151,7 @@ export async function validatePaymentFlow(baseUrl, endpoint, verbose, options) {
151
151
  results.push(fail('Payment: parse challenge', 'No Payment challenges in response'));
152
152
  return results;
153
153
  }
154
+ const loaded = await loadConfig().catch(() => undefined);
154
155
  // Testnet Tempo: always use ephemeral wallet (zero-setup, free money)
155
156
  const tempoTestnetChallenge = challenges.find((ch) => {
156
157
  if (ch.method !== Constants.Methods.tempo)
@@ -170,7 +171,8 @@ export async function validatePaymentFlow(baseUrl, endpoint, verbose, options) {
170
171
  });
171
172
  try {
172
173
  const mppx = Mppx.create({ methods: provisioned.methods, polyfill: false });
173
- const cred = await mppx.createCredential(fakeResp);
174
+ const credentialContext = await preparePayment(tempoTestnetChallenge, loaded?.config.extensions);
175
+ const cred = await mppx.createCredential(fakeResp, credentialContext);
174
176
  results.push(check('Payment: submitted', 'ephemeral testnet wallet'));
175
177
  await sendAndValidateResponse(results, url, endpoint, cred, fetchHeaders, fetchBody, verbose, tempoModerato);
176
178
  }
@@ -185,7 +187,6 @@ export async function validatePaymentFlow(baseUrl, endpoint, verbose, options) {
185
187
  }
186
188
  // Try each remaining challenge method (tempo testnet already handled above),
187
189
  // so validate exercises every payment method the server offers, same as mainnet.
188
- const loaded = await loadConfig().catch(() => undefined);
189
190
  const isInteractive = options.interactive;
190
191
  const stripeKey = process.env.MPPX_STRIPE_SECRET_KEY ?? resolveStripeKey(verbose);
191
192
  const isStripeTestKey = stripeKey?.startsWith('sk_test_') || stripeKey?.startsWith('rk_test_');
@@ -338,6 +339,7 @@ async function attemptCryptoPayment(challenge, tag, ctx) {
338
339
  }
339
340
  let methods;
340
341
  let createCredentialFn;
342
+ let credentialContext;
341
343
  if (plugin) {
342
344
  try {
343
345
  const pluginResult = await plugin.setup({
@@ -347,6 +349,7 @@ async function attemptCryptoPayment(challenge, tag, ctx) {
347
349
  });
348
350
  methods = pluginResult.methods;
349
351
  createCredentialFn = pluginResult.createCredential;
352
+ credentialContext = pluginResult.credentialContext;
350
353
  }
351
354
  catch (error) {
352
355
  results.push(skip(tag, error.message));
@@ -356,7 +359,7 @@ async function attemptCryptoPayment(challenge, tag, ctx) {
356
359
  else {
357
360
  methods = [directMethod];
358
361
  }
359
- const credential = await createAndSend(challenge, methods, createCredentialFn, tag, results);
362
+ const credential = await createAndSend(challenge, methods, createCredentialFn, tag, results, loaded?.config.extensions, credentialContext);
360
363
  if (!credential)
361
364
  return;
362
365
  plugin?.prepareCredentialRequest?.({ challenge, credential, headers: fetchHeaders });
@@ -401,6 +404,7 @@ async function attemptStripePayment(challenge, tag, ctx) {
401
404
  }
402
405
  let methods;
403
406
  let createCredentialFn;
407
+ let credentialContext;
404
408
  try {
405
409
  const methodOpts = { paymentMethod: 'pm_card_visa' };
406
410
  if (stripeKey)
@@ -412,12 +416,13 @@ async function attemptStripePayment(challenge, tag, ctx) {
412
416
  });
413
417
  methods = pluginResult.methods;
414
418
  createCredentialFn = pluginResult.createCredential;
419
+ credentialContext = pluginResult.credentialContext;
415
420
  }
416
421
  catch (error) {
417
422
  results.push(skip(tag, error.message));
418
423
  return;
419
424
  }
420
- const credential = await createAndSend(challenge, methods, createCredentialFn, tag, results);
425
+ const credential = await createAndSend(challenge, methods, createCredentialFn, tag, results, loaded?.config.extensions, credentialContext);
421
426
  if (!credential)
422
427
  return;
423
428
  plugin.prepareCredentialRequest?.({ challenge, credential, headers: fetchHeaders });
@@ -438,19 +443,20 @@ async function attemptStripePayment(challenge, tag, ctx) {
438
443
  }
439
444
  await sendAndValidateResponse(results, url, endpoint, credential, fetchHeaders, fetchBody, verbose, undefined);
440
445
  }
441
- async function createAndSend(challenge, methods, createCredentialFn, tag, results) {
446
+ async function createAndSend(challenge, methods, createCredentialFn, tag, results, extensions, initialCredentialContext) {
442
447
  const fakeResponse = new Response(null, {
443
448
  status: 402,
444
449
  headers: { [Constants.Headers.wwwAuthenticate]: Challenge.serialize(challenge) },
445
450
  });
446
451
  try {
452
+ const credentialContext = await preparePayment(challenge, extensions, initialCredentialContext);
447
453
  let credential;
448
454
  if (createCredentialFn) {
449
- credential = await createCredentialFn(fakeResponse);
455
+ credential = await createCredentialFn(fakeResponse, credentialContext);
450
456
  }
451
457
  else {
452
458
  const mppx = Mppx.create({ methods, polyfill: false });
453
- credential = await mppx.createCredential(fakeResponse);
459
+ credential = await mppx.createCredential(fakeResponse, credentialContext);
454
460
  }
455
461
  results.push(check(`${tag}: submitted`));
456
462
  return credential;
@@ -81,8 +81,8 @@ export declare namespace charge {
81
81
  signTypedData?: (parameters: any) => Promise<`0x${string}`>;
82
82
  };
83
83
  type Parameters = {
84
- /** Account used to sign EVM charge credentials. */
85
- account: Account;
84
+ /** Account used to sign credentials. May instead be supplied to `createCredential` after payment preparation. */
85
+ account?: Account | undefined;
86
86
  /** EIP-3009 token domain metadata for custom currencies. */
87
87
  authorization?: Types.AuthorizationConfig | undefined;
88
88
  /** Optional token decimals used to parse `maxAmount` when currency metadata is not provided. */
@@ -34,7 +34,7 @@ export function charge(parameters) {
34
34
  context,
35
35
  });
36
36
  const account = (context?.account ?? parameters.account);
37
- if (!account.signTypedData)
37
+ if (!account?.signTypedData)
38
38
  throw new Error('EVM authorization requires a typed-data signer.');
39
39
  const request = challenge.request;
40
40
  assertPolicy(parameters, request);
@@ -1,5 +1,6 @@
1
1
  import type * as Credential from '../../Credential.js';
2
2
  import * as Method from '../../Method.js';
3
+ import * as ServerTransport from '../../server/Transport.js';
3
4
  import * as X402 from '../../x402/server/EvmCharge.js';
4
5
  import * as Assets from '../Assets.js';
5
6
  import * as Methods from '../Methods.js';
@@ -8,11 +9,15 @@ import * as Types from '../Types.js';
8
9
  * Creates an EVM charge server method.
9
10
  *
10
11
  * Speaks the native Payment-auth `evm/charge` wire format.
12
+ * Supports non-mutating `validateCredential` checks before settlement through
13
+ * `broadcastCredential`. Validation checks the signature and payment terms, and
14
+ * checks the facilitator when using facilitator-backed settlement. Custom
15
+ * settlers remain responsible for chain-state checks and replay protection.
11
16
  */
12
- export declare function charge(parameters: charge.NativeConfig): Method.Server<typeof Methods.charge, charge.Defaults>;
17
+ export declare function charge(parameters: charge.NativeConfig): Method.Server<typeof Methods.charge, charge.Defaults, ServerTransport.Http>;
13
18
  export declare namespace charge {
14
19
  type Parameters = NativeConfig;
15
- type Native = Method.Server<typeof Methods.charge, Defaults>;
20
+ type Native = Method.Server<typeof Methods.charge, Defaults, ServerTransport.Http>;
16
21
  type NativeConfig = BaseConfig & CurrencyConfig & RecipientConfig & Method.ComposableHooks<typeof Methods.charge>;
17
22
  type BaseConfig = {
18
23
  /** EIP-3009 token domain metadata. Required for raw addresses and viem tokens; inferred for known assets. */
@@ -22,7 +22,7 @@ export function charge(parameters) {
22
22
  recipient: config.recipient,
23
23
  },
24
24
  transport,
25
- async verify({ credential }) {
25
+ async validate({ credential }) {
26
26
  const payload = credential.payload;
27
27
  const request = credential.challenge.request;
28
28
  const chainId = request.methodDetails.chainId;
@@ -70,11 +70,34 @@ export function charge(parameters) {
70
70
  if (credential.source && credential.source !== source) {
71
71
  throw new VerificationFailedError({ reason: 'EVM authorization source mismatch' });
72
72
  }
73
- const settled = await config.settle({
73
+ const authorization = {
74
74
  credential,
75
75
  payload,
76
76
  request,
77
77
  source,
78
+ };
79
+ await config.validate?.(authorization);
80
+ return {
81
+ challenge: credential.challenge,
82
+ credential,
83
+ details: { payer: getAddress(payload.from) },
84
+ intent: Methods.charge.intent,
85
+ method: Methods.charge.name,
86
+ request: credential.challenge.request,
87
+ source,
88
+ };
89
+ },
90
+ async broadcast({ credential }) {
91
+ const payload = credential.payload;
92
+ const request = credential.challenge.request;
93
+ const settled = await config.settle({
94
+ credential,
95
+ payload,
96
+ request,
97
+ source: Types.toSource({
98
+ address: getAddress(payload.from),
99
+ chainId: request.methodDetails.chainId,
100
+ }),
78
101
  });
79
102
  return Receipt.from({
80
103
  method: Types.paymentMethod,
@@ -133,6 +156,9 @@ function resolveConfig(config) {
133
156
  decimals,
134
157
  recipient: getAddress(recipient),
135
158
  settle,
159
+ validate: !config.settle && x402.facilitator
160
+ ? (parameters) => X402.verifyWithFacilitator(x402, parameters)
161
+ : undefined,
136
162
  x402,
137
163
  };
138
164
  }
@@ -67,7 +67,7 @@ export declare function evm<const parameters extends evm.Parameters>(parameters:
67
67
  }[] | undefined;
68
68
  }>>;
69
69
  };
70
- }, charge_.Defaults, undefined, {}, string | undefined>];
70
+ }, charge_.Defaults, import("../../server/Transport.js").Http, {}, string | undefined>];
71
71
  export declare namespace evm {
72
72
  type Parameters = charge_.Parameters;
73
73
  /** Creates an EVM `charge` server method. */
@@ -0,0 +1,5 @@
1
+ /** Current mppx package version. */
2
+ export declare const version = "0.9.3";
3
+ /** Canonical SDK identifier for this mppx release. */
4
+ export declare const sdkIdentifier = "mppx/0.9.3";
5
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1,6 @@
1
+ // Generated by scripts/sync-version.ts.
2
+ /** Current mppx package version. */
3
+ export const version = '0.9.3';
4
+ /** Canonical SDK identifier for this mppx release. */
5
+ export const sdkIdentifier = `mppx/${version}`;
6
+ //# sourceMappingURL=version.js.map
@@ -21,9 +21,23 @@ export function wrap(mppx, wrapper) {
21
21
  };
22
22
  Object.assign(wrapWithMeta, methodFn);
23
23
  result[key] = wrapWithMeta;
24
- // Also set shorthand intent key if Mppx registered it (no collision)
25
- if (!reservedMppxKeys.has(mi.intent) && mppx[mi.intent])
26
- result[mi.intent] = wrapWithMeta;
24
+ // Also set shorthand intent key, using the core-composed handler on collisions.
25
+ if (!reservedMppxKeys.has(mi.intent) && mppx[mi.intent]) {
26
+ const intentFn = mppx[mi.intent];
27
+ if (intentFn === methodFn)
28
+ result[mi.intent] = wrapWithMeta;
29
+ else {
30
+ const wrapIntentWithMeta = (options) => {
31
+ const configured = intentFn(options);
32
+ const handler = wrapper(intentFn, options);
33
+ if (configured._internal)
34
+ handler._internal = configured._internal;
35
+ return handler;
36
+ };
37
+ Object.assign(wrapIntentWithMeta, intentFn);
38
+ result[mi.intent] = wrapIntentWithMeta;
39
+ }
40
+ }
27
41
  // Build nested handlers: wrapped.tempo.charge(...)
28
42
  if (!result[mi.name] || typeof result[mi.name] !== 'object')
29
43
  result[mi.name] = {};
@@ -134,15 +134,16 @@ export type Mppx<methods extends Methods = Methods, transport extends Transport.
134
134
  onPaymentFailed(handler: ServerEventHandler<FlattenMethods<methods>, transport, 'payment.failed'>): Unsubscribe;
135
135
  /** Register a handler for successful verified payments. */
136
136
  onPaymentSuccess(handler: ServerEventHandler<FlattenMethods<methods>, transport, 'payment.success'>): Unsubscribe;
137
- } & (transport extends Transport.Http ? {
137
+ } & (transport extends Transport.Http | Transport.Mcp | Transport.McpSdk ? {
138
138
  /**
139
139
  * Combines multiple method handlers into a single route handler that presents
140
- * all methods to the client via multiple `WWW-Authenticate` headers.
140
+ * all methods through HTTP headers or an MCP payment-required challenge list.
141
141
  *
142
142
  * Each entry is a `[method, options]` tuple where `method` is one of the
143
143
  * server methods passed to `Mppx.create()`, looked up by `name`+`intent`.
144
144
  *
145
- * Only available on HTTP transports.
145
+ * Available on HTTP, MCP JSON-RPC, and MCP SDK transports. MCP handlers
146
+ * accept the transport's input and return its challenge and receipt types.
146
147
  * No-credential authorize hooks run in entry order; the first 200 response
147
148
  * wins, and earlier hooks may have already run side effects.
148
149
  *
@@ -174,7 +175,7 @@ export type Mppx<methods extends Methods = Methods, transport extends Transport.
174
175
  * })
175
176
  * ```
176
177
  */
177
- compose(...entries: ComposeEntry<FlattenMethods<methods>>[]): ComposedHandler;
178
+ compose(...entries: ComposeEntry<FlattenMethods<methods>>[]): ComposedHandler<transport>;
178
179
  } : {}) & Handlers<FlattenMethods<methods>, transport> & {
179
180
  /**
180
181
  * Generate Challenge objects for registered methods without going through
@@ -364,7 +365,7 @@ declare namespace MethodFn {
364
365
  };
365
366
  }
366
367
  /** A configured handler — the return value of e.g. `mppx.charge({ ... })`. @internal */
367
- type ConfiguredHandler = ((input: Request) => Promise<MethodFn.Response<Transport.Http>>) & {
368
+ type ConfiguredHandler<transport extends Transport.AnyTransport = Transport.Http> = ((input: Transport.InputOf<transport>) => Promise<MethodFn.Response<transport>>) & {
368
369
  _internal: {
369
370
  _method: Method.AnyServer;
370
371
  description?: string | undefined;
@@ -377,7 +378,7 @@ type ConfiguredHandler = ((input: Request) => Promise<MethodFn.Response<Transpor
377
378
  _stableBinding?: Method.StableBindingFn<Method.Method> | undefined;
378
379
  };
379
380
  };
380
- type ComposedHandler = ((input: Request) => Promise<MethodFn.Response<Transport.Http>>) & {
381
+ type ComposedHandler<transport extends Transport.AnyTransport = Transport.Http> = ((input: Transport.InputOf<transport>) => Promise<MethodFn.Response<transport>>) & {
381
382
  _internal?: {
382
383
  offers: readonly ConfiguredHandler['_internal'][];
383
384
  };
@@ -7,6 +7,7 @@ import * as Errors from '../Errors.js';
7
7
  import * as Expires from '../Expires.js';
8
8
  import * as AcceptPayment from '../internal/AcceptPayment.js';
9
9
  import * as Env from '../internal/env.js';
10
+ import * as Mcp from '../Mcp.js';
10
11
  import * as PaymentRequest from '../PaymentRequest.js';
11
12
  import * as x402_Header from '../x402/Header.js';
12
13
  import * as x402_Types from '../x402/Types.js';
@@ -70,6 +71,7 @@ export function create(config) {
70
71
  serverEvents.on('payment.success', (async (ctx) => {
71
72
  if (ctx.method.name === mi.name && ctx.method.intent === mi.intent) {
72
73
  await mi.onPaymentSuccess({
74
+ challenge: ctx.challenge,
73
75
  input: ctx.input,
74
76
  receipt: ctx.receipt,
75
77
  request: ctx.request,
@@ -339,8 +341,9 @@ export function create(config) {
339
341
  }
340
342
  const verifyCredentialFn = broadcastCredentialFn;
341
343
  function composeFn(...entries) {
342
- if (transport.name !== 'http')
343
- throw new Error('compose() only supports HTTP transport');
344
+ const isMcp = transport.name === 'mcp' || transport.name === 'mcp-sdk';
345
+ if (transport.name !== 'http' && !isMcp)
346
+ throw new Error('compose() only supports HTTP and MCP transports');
344
347
  if (entries.length === 0)
345
348
  throw new Error('compose() requires at least one entry');
346
349
  const configured = entries.map(([methodOrKey, options]) => {
@@ -352,8 +355,15 @@ export function create(config) {
352
355
  const handlerFn = handlers[key];
353
356
  if (!handlerFn)
354
357
  throw new Error(`No handler for "${key}". Is this method in your methods array?`);
358
+ const method = handlerFn._method;
359
+ if (isMcp && method?.transport && method.transport.name !== transport.name)
360
+ throw new Error('MCP compose() requires methods using the configured MCP transport');
361
+ if (isMcp && method?.canOffer)
362
+ throw new Error('MCP compose() does not support HTTP canOffer hooks');
355
363
  return handlerFn(options);
356
364
  });
365
+ if (isMcp)
366
+ return composeMcpHandlers(configured, transport);
357
367
  return composeHandlers(configured, undefined, selectOffers);
358
368
  }
359
369
  function onChallengeCreated(handler) {
@@ -1657,6 +1667,78 @@ function getConfiguredOffers(handler) {
1657
1667
  return internal.offers;
1658
1668
  return [internal];
1659
1669
  }
1670
+ /** Dispatches one MCP credential or gathers unsigned offers in their configured order. */
1671
+ function composeMcpHandlers(handlers, transport) {
1672
+ return async (input) => {
1673
+ let credential;
1674
+ try {
1675
+ const parsed = transport.getCredential(input);
1676
+ credential = parsed ? hydrateCredentialMeta(parsed) : null;
1677
+ }
1678
+ catch {
1679
+ // Let the method emit the normal malformed-credential error and retry challenge.
1680
+ return handlers[0](input);
1681
+ }
1682
+ if (credential) {
1683
+ const candidates = handlers.filter((handler) => {
1684
+ const internal = handler._internal;
1685
+ return (internal.name === credential.challenge.method &&
1686
+ internal.intent === credential.challenge.intent);
1687
+ });
1688
+ const match = candidates.find((handler) => {
1689
+ const internal = handler._internal;
1690
+ try {
1691
+ const mismatch = internal._stableBinding
1692
+ ? getRequestBindingMismatch(getStableBinding(internal._canonicalRequest, internal._stableBinding), getStableBinding(credential.challenge.request, internal._stableBinding))
1693
+ : getPinnedRequestBindingMismatch(internal._canonicalRequest, credential.challenge.request);
1694
+ return !mismatch && opaqueValuesMatch(internal.meta, credential.challenge.meta);
1695
+ }
1696
+ catch {
1697
+ return false;
1698
+ }
1699
+ });
1700
+ // Verification and settlement belong to exactly one handler, including failures.
1701
+ return (match ?? candidates[0] ?? handlers[0])(input);
1702
+ }
1703
+ const challenges = [];
1704
+ let first;
1705
+ for (const handler of handlers) {
1706
+ const result = await handler(input);
1707
+ if (result.status === 200)
1708
+ return result;
1709
+ const response = result.challenge;
1710
+ const error = 'code' in response ? response : response.error;
1711
+ // Request-hook and configuration failures must retain their original error semantics.
1712
+ if (!error || error.code !== Mcp.paymentRequiredCode)
1713
+ return result;
1714
+ const data = error.data;
1715
+ if (!data?.challenges?.length)
1716
+ return result;
1717
+ first ??= response;
1718
+ challenges.push(...data.challenges);
1719
+ }
1720
+ if (!first)
1721
+ throw new Error('compose() requires at least one handler');
1722
+ if ('code' in first) {
1723
+ const { McpError } = await import('@modelcontextprotocol/sdk/types.js');
1724
+ return {
1725
+ status: 402,
1726
+ challenge: new McpError(first.code, 'Payment Required', {
1727
+ ...first.data,
1728
+ challenges,
1729
+ }),
1730
+ };
1731
+ }
1732
+ return {
1733
+ status: 402,
1734
+ challenge: {
1735
+ jsonrpc: first.jsonrpc,
1736
+ id: first.id,
1737
+ error: { ...first.error, data: { ...first.error.data, challenges } },
1738
+ },
1739
+ };
1740
+ };
1741
+ }
1660
1742
  function isComposedHandlerMetadata(internal) {
1661
1743
  return 'offers' in internal && !('_canonicalRequest' in internal);
1662
1744
  }
@@ -106,9 +106,7 @@ export function render(options) {
106
106
  ${data.challenge.description
107
107
  ? `data-description="${sanitize(data.challenge.description)}"`
108
108
  : ''}
109
- ${data.challenge.expires
110
- ? `data-expires="${sanitize(data.challenge.expires)}"`
111
- : ''}
109
+ ${data.challenge.expires ? `data-expires="${sanitize(data.challenge.expires)}"` : ''}
112
110
  ${data.challenge.expires ? `data-expires-label="${sanitize(text.expires)}"` : ''}
113
111
  >
114
112
  ${sanitize(data.label)}
@@ -1,4 +1,5 @@
1
1
  import * as z from '../zod.js';
2
+ import * as PaymentIntent from './internal/payment-intent.js';
2
3
  /**
3
4
  * Stripe charge intent for one-time payments via Shared Payment Tokens (SPTs).
4
5
  *
@@ -22,9 +23,18 @@ export declare const charge: {
22
23
  externalId: z.ZodMiniOptional<z.ZodMiniString<string>>;
23
24
  metadata: z.ZodMiniOptional<z.ZodMiniRecord<z.ZodMiniString<string>, z.ZodMiniString<string>>>;
24
25
  networkId: z.ZodMiniString<string>;
25
- paymentIntentOptions: z.ZodMiniOptional<z.ZodMiniObject<{
26
- metadata: z.ZodMiniRecord<z.ZodMiniString<string>, z.ZodMiniString<string>>;
27
- }, z.core.$strip>>;
26
+ paymentIntentOptions: z.ZodMiniOptional<z.ZodMiniUnion<readonly [z.ZodMiniObject<{
27
+ customer: z.ZodMiniOptional<z.ZodMiniString<string>>;
28
+ hooks: z.ZodMiniOptional<z.ZodMiniObject<{
29
+ inputs: z.ZodMiniObject<{
30
+ tax: z.ZodMiniObject<{
31
+ calculation: z.ZodMiniString<string>;
32
+ }, z.core.$strip>;
33
+ }, z.core.$strip>;
34
+ }, z.core.$strip>>;
35
+ metadata: z.ZodMiniOptional<z.ZodMiniRecord<z.ZodMiniString<string>, z.ZodMiniString<string>>>;
36
+ receipt_email: z.ZodMiniOptional<z.ZodMiniString<string>>;
37
+ }, z.core.$strip>, z.ZodMiniCustom<PaymentIntent.ResolveOptions, PaymentIntent.ResolveOptions>]>>;
28
38
  paymentMethodTypes: z.ZodMiniArray<z.ZodMiniString<string>>;
29
39
  recipient: z.ZodMiniOptional<z.ZodMiniString<string>>;
30
40
  }, z.core.$strip>, z.ZodMiniTransform<{
@@ -46,8 +56,17 @@ export declare const charge: {
46
56
  externalId?: string | undefined;
47
57
  metadata?: Record<string, string> | undefined;
48
58
  networkId: string;
49
- paymentIntentOptions?: {
50
- metadata: Record<string, string>;
59
+ paymentIntentOptions?: PaymentIntent.ResolveOptions | {
60
+ customer?: string | undefined;
61
+ hooks?: {
62
+ inputs: {
63
+ tax: {
64
+ calculation: string;
65
+ };
66
+ };
67
+ } | undefined;
68
+ metadata?: Record<string, string> | undefined;
69
+ receipt_email?: string | undefined;
51
70
  } | undefined;
52
71
  paymentMethodTypes: string[];
53
72
  recipient?: string | undefined;
@@ -1,6 +1,6 @@
1
- import { parseUnits } from 'viem';
2
1
  import * as Method from '../Method.js';
3
2
  import * as z from '../zod.js';
3
+ import { parseUnits } from './internal/parse-units.js';
4
4
  import * as PaymentIntent from './internal/payment-intent.js';
5
5
  /**
6
6
  * Stripe charge intent for one-time payments via Shared Payment Tokens (SPTs).
@@ -25,18 +25,21 @@ export const charge = Method.from({
25
25
  externalId: z.optional(z.string()),
26
26
  metadata: z.optional(z.record(z.string(), z.string())),
27
27
  networkId: z.string(),
28
- paymentIntentOptions: z.optional(PaymentIntent.Schema),
28
+ paymentIntentOptions: z.optional(PaymentIntent.InputSchema),
29
29
  paymentMethodTypes: z.array(z.string()).check(z.minLength(1)),
30
30
  recipient: z.optional(z.string()),
31
- }), z.transform(({ amount, decimals, metadata, networkId, paymentIntentOptions: _, paymentMethodTypes, ...rest }) => ({
32
- ...rest,
33
- amount: parseUnits(amount, decimals).toString(),
34
- methodDetails: {
35
- networkId,
36
- paymentMethodTypes,
37
- ...(metadata !== undefined && { metadata }),
38
- },
39
- }))),
31
+ }), z.transform(({ amount, decimals, metadata, networkId, paymentIntentOptions, paymentMethodTypes, ...rest }) => {
32
+ void paymentIntentOptions;
33
+ return {
34
+ ...rest,
35
+ amount: parseUnits(amount, decimals).toString(),
36
+ methodDetails: {
37
+ networkId,
38
+ paymentMethodTypes,
39
+ ...(metadata !== undefined && { metadata }),
40
+ },
41
+ };
42
+ })),
40
43
  },
41
44
  });
42
45
  //# sourceMappingURL=Methods.js.map