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.
- package/CHANGELOG.md +28 -0
- package/README.md +26 -0
- package/dist/Method.d.ts +2 -0
- package/dist/cli/Extension.d.ts +20 -0
- package/dist/cli/Extension.js +5 -0
- package/dist/cli/cli.d.ts +1 -0
- package/dist/cli/cli.js +135 -135
- package/dist/cli/config.d.ts +4 -0
- package/dist/cli/config.js +1 -0
- package/dist/cli/internal.d.ts +3 -0
- package/dist/cli/internal.js +15 -2
- package/dist/cli/plugins/evm.js +2 -0
- package/dist/cli/plugins/index.d.ts +1 -0
- package/dist/cli/plugins/index.js +13 -13
- package/dist/cli/plugins/plugin.d.ts +2 -2
- package/dist/cli/plugins/x402.d.ts +3 -0
- package/dist/cli/plugins/x402.js +61 -0
- package/dist/cli/sessions/request.d.ts +1 -0
- package/dist/cli/sessions/request.js +1 -0
- package/dist/cli/validate/payment.js +14 -8
- package/dist/evm/client/Charge.d.ts +2 -2
- package/dist/evm/client/Charge.js +1 -1
- package/dist/evm/server/Charge.d.ts +7 -2
- package/dist/evm/server/Charge.js +28 -2
- package/dist/evm/server/Methods.d.ts +1 -1
- package/dist/internal/version.d.ts +5 -0
- package/dist/internal/version.js +6 -0
- package/dist/middlewares/internal/mppx.js +17 -3
- package/dist/server/Mppx.d.ts +7 -6
- package/dist/server/Mppx.js +84 -2
- package/dist/server/internal/html/config.js +1 -3
- package/dist/stripe/Methods.d.ts +24 -5
- package/dist/stripe/Methods.js +14 -11
- package/dist/stripe/client/Charge.d.ts +23 -5
- package/dist/stripe/client/Methods.d.ts +23 -5
- package/dist/stripe/internal/constants.d.ts +4 -0
- package/dist/stripe/internal/constants.js +2 -0
- package/dist/stripe/internal/parse-units.d.ts +8 -0
- package/dist/stripe/internal/parse-units.js +20 -0
- package/dist/stripe/internal/payment-intent.d.ts +39 -1
- package/dist/stripe/internal/payment-intent.js +16 -1
- package/dist/stripe/internal/types.d.ts +4 -0
- package/dist/stripe/server/Charge.d.ts +25 -5
- package/dist/stripe/server/Charge.js +35 -23
- package/dist/stripe/server/Methods.d.ts +8 -0
- package/dist/stripe/server/Methods.js +54 -9
- package/dist/stripe/server/internal/analytics.d.ts +6 -0
- package/dist/stripe/server/internal/analytics.js +16 -0
- package/dist/stripe/server/internal/hosted-fee-payer.d.ts +10 -0
- package/dist/stripe/server/internal/hosted-fee-payer.js +54 -0
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/stripe/server/internal/record-payment.d.ts +4 -1
- package/dist/stripe/server/internal/record-payment.js +35 -5
- package/dist/tempo/client/Subscription.js +6 -2
- package/dist/tempo/internal/account.d.ts +1 -0
- package/dist/tempo/internal/fee-payer.d.ts +2 -0
- package/dist/tempo/internal/fee-payer.js +4 -0
- package/dist/tempo/internal/remote-fee-payer.d.ts +2 -0
- package/dist/tempo/server/Subscription.js +1 -0
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/session/client/SessionManager.d.ts +5 -0
- package/dist/tempo/session/client/SessionManager.js +16 -2
- package/dist/tempo/session/precompile/Chain.js +3 -0
- package/dist/tempo/subscription/KeyAuthorization.d.ts +7 -1
- package/dist/tempo/subscription/KeyAuthorization.js +21 -3
- package/dist/viem/Client.js +1 -0
- package/dist/x402/client/Exact.d.ts +2 -2
- package/dist/x402/client/Exact.js +1 -1
- package/dist/x402/server/EvmCharge.d.ts +2 -0
- package/dist/x402/server/EvmCharge.js +34 -26
- 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,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
|
|
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
|
|
85
|
-
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
|
|
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
|
|
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
|
|
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,
|
|
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. */
|
|
@@ -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
|
|
25
|
-
if (!reservedMppxKeys.has(mi.intent) && mppx[mi.intent])
|
|
26
|
-
|
|
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] = {};
|
package/dist/server/Mppx.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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:
|
|
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:
|
|
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
|
};
|
package/dist/server/Mppx.js
CHANGED
|
@@ -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
|
-
|
|
343
|
-
|
|
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)}
|
package/dist/stripe/Methods.d.ts
CHANGED
|
@@ -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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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;
|
package/dist/stripe/Methods.js
CHANGED
|
@@ -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.
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|