mppx 0.9.2 → 0.10.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.
- package/CHANGELOG.md +32 -0
- package/README.md +19 -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.js +137 -137
- package/dist/cli/config.d.ts +4 -0
- package/dist/cli/config.js +1 -0
- package/dist/cli/internal.d.ts +7 -1
- package/dist/cli/internal.js +51 -5
- package/dist/cli/plugins/index.js +12 -12
- package/dist/cli/plugins/plugin.d.ts +2 -2
- package/dist/cli/sessions/request.d.ts +1 -0
- package/dist/cli/sessions/request.js +1 -0
- package/dist/cli/validate/discovery.d.ts +3 -1
- package/dist/cli/validate/discovery.js +28 -1
- package/dist/cli/validate/helpers.d.ts +3 -2
- package/dist/cli/validate/helpers.js +3 -0
- package/dist/cli/validate/index.js +3 -1
- package/dist/cli/validate/payment.js +118 -59
- package/dist/client/Mppx.d.ts +32 -0
- package/dist/client/Mppx.js +180 -6
- package/dist/client/internal/Fetch.d.ts +6 -0
- package/dist/client/internal/Fetch.js +4 -2
- 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/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 +4 -3
- package/dist/stripe/Methods.js +14 -11
- package/dist/stripe/client/Charge.d.ts +3 -3
- package/dist/stripe/client/Methods.d.ts +3 -3
- 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 +29 -0
- package/dist/stripe/internal/payment-intent.js +8 -0
- package/dist/stripe/server/Charge.d.ts +5 -3
- package/dist/stripe/server/Charge.js +17 -21
- package/dist/stripe/server/Methods.d.ts +6 -4
- package/dist/stripe/server/Methods.js +42 -14
- package/dist/stripe/server/internal/analytics.d.ts +6 -0
- package/dist/stripe/server/internal/analytics.js +16 -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 +1 -0
- package/dist/stripe/server/internal/record-payment.js +7 -5
- package/dist/tempo/client/Charge.d.ts +8 -2
- package/dist/tempo/client/Charge.js +19 -11
- package/dist/tempo/client/Subscription.js +6 -2
- package/dist/tempo/internal/fee-payer.d.ts +2 -0
- package/dist/tempo/internal/fee-payer.js +4 -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/CredentialState.d.ts +2 -0
- package/dist/tempo/session/client/CredentialState.js +9 -3
- package/dist/tempo/session/client/Session.d.ts +6 -0
- package/dist/tempo/session/client/Session.js +4 -1
- package/dist/tempo/session/client/SessionManager.d.ts +11 -0
- package/dist/tempo/session/client/SessionManager.js +23 -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/validation/core.d.ts +1 -0
- package/dist/validation/core.js +7 -2
- package/dist/viem/Client.d.ts +4 -0
- package/dist/viem/Client.js +13 -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.
|
|
@@ -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) => {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type { EndpointSpec } from './helpers.js';
|
|
1
|
+
import type { CheckResult, EndpointSpec } from './helpers.js';
|
|
2
2
|
export declare function fetchDiscoveryDoc(baseUrl: string): Promise<{
|
|
3
3
|
doc: unknown;
|
|
4
4
|
raw: string;
|
|
5
5
|
} | {
|
|
6
6
|
error: string;
|
|
7
7
|
}>;
|
|
8
|
+
/** Checks that the server publishes nonempty text documentation at /llms.txt. */
|
|
9
|
+
export declare function validateLlmsDoc(baseUrl: string): Promise<CheckResult>;
|
|
8
10
|
export declare function extractEndpointsFromDiscovery(doc: Record<string, unknown>): EndpointSpec[];
|
|
9
11
|
export declare function extractRequestBodyFromDiscovery(doc: Record<string, unknown>, endpoint: EndpointSpec): string | undefined;
|
|
10
12
|
export declare function buildUrl(baseUrl: string, endpoint: EndpointSpec, query?: string[]): string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fetchWithTimeout, HTTP_METHODS } from './helpers.js';
|
|
1
|
+
import { check, fetchWithTimeout, HTTP_METHODS } from './helpers.js';
|
|
2
2
|
export async function fetchDiscoveryDoc(baseUrl) {
|
|
3
3
|
// Trailing slash makes URL treat baseUrl as a directory, so relative resolution appends rather than replaces the last segment.
|
|
4
4
|
const url = new URL('openapi.json', baseUrl.replace(/\/?$/, '/')).href;
|
|
@@ -18,6 +18,33 @@ export async function fetchDiscoveryDoc(baseUrl) {
|
|
|
18
18
|
return { error: error.message };
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
/** Checks that the server publishes nonempty text documentation at /llms.txt. */
|
|
22
|
+
export async function validateLlmsDoc(baseUrl) {
|
|
23
|
+
const url = new URL('/llms.txt', baseUrl).href;
|
|
24
|
+
const label = 'llms.txt found and nonempty';
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetchWithTimeout(url, {});
|
|
27
|
+
if (!response.ok)
|
|
28
|
+
throw new Error(`HTTP ${response.status}`);
|
|
29
|
+
const body = (await response.text()).trim();
|
|
30
|
+
if (!body)
|
|
31
|
+
throw new Error('Empty document');
|
|
32
|
+
const contentType = response.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase();
|
|
33
|
+
if ((contentType && !contentType.startsWith('text/')) ||
|
|
34
|
+
contentType === 'text/html' ||
|
|
35
|
+
/^<(?:!doctype html|html|head|body)\b/i.test(body))
|
|
36
|
+
throw new Error('Expected text documentation, received non-text or HTML content');
|
|
37
|
+
return check(label, url);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
return {
|
|
41
|
+
label,
|
|
42
|
+
detail: `${url}: ${error.message}`,
|
|
43
|
+
severity: 'suggested',
|
|
44
|
+
hint: 'Serve nonempty text documentation at /llms.txt.',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
21
48
|
// Extracts testable endpoints from an OpenAPI doc. Prefers endpoints with
|
|
22
49
|
// explicit x-payment-info (the server declares them as paid). Falls back to
|
|
23
50
|
// endpoints that list a 402 response (weaker signal, but still worth testing).
|
|
@@ -2,7 +2,7 @@ export type CheckResult = {
|
|
|
2
2
|
label: string;
|
|
3
3
|
detail?: string | undefined;
|
|
4
4
|
hint?: string | undefined;
|
|
5
|
-
severity: 'pass' | 'fail' | 'warn' | 'skip';
|
|
5
|
+
severity: 'pass' | 'fail' | 'warn' | 'suggested' | 'skip';
|
|
6
6
|
};
|
|
7
7
|
export type EndpointSpec = {
|
|
8
8
|
method: string;
|
|
@@ -33,10 +33,11 @@ export type Counts = {
|
|
|
33
33
|
passed: number;
|
|
34
34
|
failed: number;
|
|
35
35
|
warnings: number;
|
|
36
|
+
suggested: number;
|
|
36
37
|
skipped: number;
|
|
37
38
|
};
|
|
38
39
|
export declare function printResults(results: CheckResult[], counts: Counts): void;
|
|
39
|
-
export declare function fetchWithTimeout(url:
|
|
40
|
+
export declare function fetchWithTimeout(url: RequestInfo | URL, init: RequestInit, timeoutMs?: number): Promise<Response>;
|
|
40
41
|
export declare function formatBytes(bytes: number): string;
|
|
41
42
|
export declare const HTTP_METHODS: Set<string>;
|
|
42
43
|
export declare function isValidAddress(addr: unknown): boolean;
|
|
@@ -28,6 +28,7 @@ const SEVERITY_ICONS = {
|
|
|
28
28
|
pass: pc.green('✓'),
|
|
29
29
|
fail: pc.red('✗'),
|
|
30
30
|
warn: pc.yellow('⚠'),
|
|
31
|
+
suggested: pc.cyan('◇'),
|
|
31
32
|
skip: pc.dim('○'),
|
|
32
33
|
};
|
|
33
34
|
export function printCheck(result) {
|
|
@@ -50,6 +51,8 @@ export function printResults(results, counts) {
|
|
|
50
51
|
counts.failed++;
|
|
51
52
|
else if (result.severity === 'warn')
|
|
52
53
|
counts.warnings++;
|
|
54
|
+
else if (result.severity === 'suggested')
|
|
55
|
+
counts.suggested++;
|
|
53
56
|
else if (result.severity === 'skip')
|
|
54
57
|
counts.skipped++;
|
|
55
58
|
}
|
|
@@ -53,7 +53,7 @@ const validate = Cli.create('validate', {
|
|
|
53
53
|
// Streaming human-readable output
|
|
54
54
|
const baseUrl = c.args.url.replace(/\/$/, '').replace(/\/openapi\.json$/i, '');
|
|
55
55
|
console.log(`\n${pc.bold('mppx validate')} ${pc.dim(baseUrl)}\n`);
|
|
56
|
-
const counts = { passed: 0, failed: 0, warnings: 0, skipped: 0 };
|
|
56
|
+
const counts = { passed: 0, failed: 0, warnings: 0, suggested: 0, skipped: 0 };
|
|
57
57
|
let sawMppEndpoint = false;
|
|
58
58
|
let sawNonMppPaymentEndpoint = false;
|
|
59
59
|
let sawMalformedChallenge = false;
|
|
@@ -165,6 +165,8 @@ function printSummary(counts, flags, endpointsLength) {
|
|
|
165
165
|
parts.push(pc.red(`${counts.failed} failed`));
|
|
166
166
|
if (counts.warnings > 0)
|
|
167
167
|
parts.push(pc.yellow(`${counts.warnings} warning(s)`));
|
|
168
|
+
if (counts.suggested > 0)
|
|
169
|
+
parts.push(pc.cyan(`${counts.suggested} suggested`));
|
|
168
170
|
if (counts.skipped > 0)
|
|
169
171
|
parts.push(pc.yellow(`${counts.skipped} skipped`));
|
|
170
172
|
console.log(`${pc.bold('Summary:')} ${parts.join(', ')}`);
|
|
@@ -11,8 +11,9 @@ import * as Constants from '../../Constants.js';
|
|
|
11
11
|
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
|
+
import { isTempoSessionChallenge } from '../../tempo/session/client/Transports.js';
|
|
14
15
|
import { resolveAccount, resolveAccountName } from '../account.js';
|
|
15
|
-
import { loadConfig, resolvePlugin } from '../internal.js';
|
|
16
|
+
import { assertSamePaymentRequest, flattenConfigMethods, loadConfig, preparePayment, resolvePlugin, } from '../internal.js';
|
|
16
17
|
import { fetchTokenInfo, confirm, pc } from '../utils.js';
|
|
17
18
|
import { buildUrl } from './discovery.js';
|
|
18
19
|
import { check, fail, fetchWithTimeout, formatBytes, isValidIntegerAmount, parseHeaders, skip, warn, } from './helpers.js';
|
|
@@ -151,10 +152,13 @@ export async function validatePaymentFlow(baseUrl, endpoint, verbose, options) {
|
|
|
151
152
|
results.push(fail('Payment: parse challenge', 'No Payment challenges in response'));
|
|
152
153
|
return results;
|
|
153
154
|
}
|
|
155
|
+
const loaded = await loadConfig().catch(() => undefined);
|
|
154
156
|
// Testnet Tempo: always use ephemeral wallet (zero-setup, free money)
|
|
155
157
|
const tempoTestnetChallenge = challenges.find((ch) => {
|
|
156
158
|
if (ch.method !== Constants.Methods.tempo)
|
|
157
159
|
return false;
|
|
160
|
+
if (flattenConfigMethods(loaded?.config)?.some((method) => method.name === ch.method && method.intent === ch.intent))
|
|
161
|
+
return false;
|
|
158
162
|
const req = ch.request;
|
|
159
163
|
const md = req.methodDetails;
|
|
160
164
|
return typeof md?.chainId === 'number' && md.chainId !== tempoChainIds.mainnet;
|
|
@@ -170,9 +174,10 @@ export async function validatePaymentFlow(baseUrl, endpoint, verbose, options) {
|
|
|
170
174
|
});
|
|
171
175
|
try {
|
|
172
176
|
const mppx = Mppx.create({ methods: provisioned.methods, polyfill: false });
|
|
173
|
-
const
|
|
177
|
+
const credentialContext = await preparePayment(tempoTestnetChallenge, loaded?.config.extensions);
|
|
178
|
+
const cred = await mppx.createCredential(fakeResp, credentialContext);
|
|
174
179
|
results.push(check('Payment: submitted', 'ephemeral testnet wallet'));
|
|
175
|
-
await sendAndValidateResponse(results, url, endpoint, cred, fetchHeaders, fetchBody, verbose, tempoModerato);
|
|
180
|
+
await sendAndValidateResponse(results, url, endpoint, cred, fetchHeaders, fetchBody, verbose, tempoModerato, Challenge.credentialHeader(tempoTestnetChallenge));
|
|
176
181
|
}
|
|
177
182
|
catch (error) {
|
|
178
183
|
results.push(fail('Payment: create credential', error.message));
|
|
@@ -185,7 +190,6 @@ export async function validatePaymentFlow(baseUrl, endpoint, verbose, options) {
|
|
|
185
190
|
}
|
|
186
191
|
// Try each remaining challenge method (tempo testnet already handled above),
|
|
187
192
|
// so validate exercises every payment method the server offers, same as mainnet.
|
|
188
|
-
const loaded = await loadConfig().catch(() => undefined);
|
|
189
193
|
const isInteractive = options.interactive;
|
|
190
194
|
const stripeKey = process.env.MPPX_STRIPE_SECRET_KEY ?? resolveStripeKey(verbose);
|
|
191
195
|
const isStripeTestKey = stripeKey?.startsWith('sk_test_') || stripeKey?.startsWith('rk_test_');
|
|
@@ -255,31 +259,42 @@ async function attemptCryptoPayment(challenge, tag, ctx) {
|
|
|
255
259
|
: undefined;
|
|
256
260
|
const decimals = methodDetails?.decimals ?? request.decimals ?? 6;
|
|
257
261
|
const currency = request.currency;
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
walletAddress = await resolveWalletAddress();
|
|
262
|
-
}
|
|
263
|
-
catch { }
|
|
264
|
-
if (!walletAddress) {
|
|
265
|
-
results.push(skip(tag, 'no wallet configured. Run "mppx account create" to create one.'));
|
|
262
|
+
const { plugin, method: directMethod } = resolvePlugin(challenge, loaded?.config);
|
|
263
|
+
if (!plugin && !directMethod) {
|
|
264
|
+
results.push(skip(tag, methodSetupHint(challenge)));
|
|
266
265
|
return;
|
|
267
266
|
}
|
|
267
|
+
// Direct methods own their payer; the CLI wallet may be unrelated or absent.
|
|
268
|
+
let walletAddress;
|
|
269
|
+
if (!directMethod) {
|
|
270
|
+
try {
|
|
271
|
+
walletAddress = await resolveWalletAddress();
|
|
272
|
+
}
|
|
273
|
+
catch { }
|
|
274
|
+
if (!walletAddress) {
|
|
275
|
+
results.push(skip(tag, 'no wallet configured. Run "mppx account create" to create one.'));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
268
279
|
// Pre-flight balance check and chain resolution
|
|
269
280
|
let paymentChain;
|
|
270
|
-
if (challenge.method === Constants.Methods.tempo)
|
|
271
|
-
|
|
281
|
+
if (challenge.method === Constants.Methods.tempo) {
|
|
282
|
+
const chainId = methodDetails?.chainId ??
|
|
283
|
+
(directMethod ? undefined : tempoMainnetChain.id);
|
|
284
|
+
if (chainId !== undefined)
|
|
285
|
+
paymentChain = chainId === tempoModerato.id ? tempoModerato : resolveEvmChain(chainId);
|
|
286
|
+
}
|
|
272
287
|
else if (challenge.method === Constants.Methods.evm) {
|
|
273
288
|
const chainId = methodDetails?.chainId;
|
|
274
289
|
if (chainId)
|
|
275
290
|
paymentChain = resolveEvmChain(chainId);
|
|
276
291
|
}
|
|
277
292
|
let tokenSymbol;
|
|
278
|
-
if (requiredAmount && currency && paymentChain) {
|
|
293
|
+
if (walletAddress && requiredAmount && currency && paymentChain) {
|
|
279
294
|
try {
|
|
280
295
|
let balance;
|
|
281
296
|
if (challenge.method === Constants.Methods.tempo) {
|
|
282
|
-
const client = createClient({ chain:
|
|
297
|
+
const client = createClient({ chain: paymentChain, transport: http() });
|
|
283
298
|
const info = await fetchTokenInfo(client, currency, walletAddress);
|
|
284
299
|
balance = info.balance;
|
|
285
300
|
tokenSymbol = info.symbol;
|
|
@@ -308,8 +323,10 @@ async function attemptCryptoPayment(challenge, tag, ctx) {
|
|
|
308
323
|
const chainName = paymentChain?.name;
|
|
309
324
|
const paymentDesc = `${amountDisplay}${tokenDisplay ? ` ${tokenDisplay}` : ''}${chainName ? ` on ${chainName}` : ''}`;
|
|
310
325
|
if (!options.silent)
|
|
311
|
-
console.log(pc.dim(
|
|
312
|
-
|
|
326
|
+
console.log(pc.dim(walletAddress
|
|
327
|
+
? ` Attempting payment with wallet ${walletAddress}`
|
|
328
|
+
: ' Attempting payment with configured method'));
|
|
329
|
+
if (!directMethod && paymentChain?.testnet) {
|
|
313
330
|
if (!options.silent)
|
|
314
331
|
console.log(pc.dim(` Auto-approved: ${paymentDesc} (testnet)`));
|
|
315
332
|
}
|
|
@@ -328,16 +345,9 @@ async function attemptCryptoPayment(challenge, tag, ctx) {
|
|
|
328
345
|
if (!options.silent)
|
|
329
346
|
console.log(pc.dim(` Auto-approved: ${paymentDesc}`));
|
|
330
347
|
}
|
|
331
|
-
// Resolve plugin and pay
|
|
332
|
-
const resolved = resolvePlugin(challenge, loaded?.config);
|
|
333
|
-
const plugin = resolved.plugin;
|
|
334
|
-
const directMethod = resolved.method;
|
|
335
|
-
if (!plugin && !directMethod) {
|
|
336
|
-
results.push(skip(tag, methodSetupHint(challenge)));
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
348
|
let methods;
|
|
340
349
|
let createCredentialFn;
|
|
350
|
+
let credentialContext;
|
|
341
351
|
if (plugin) {
|
|
342
352
|
try {
|
|
343
353
|
const pluginResult = await plugin.setup({
|
|
@@ -347,6 +357,7 @@ async function attemptCryptoPayment(challenge, tag, ctx) {
|
|
|
347
357
|
});
|
|
348
358
|
methods = pluginResult.methods;
|
|
349
359
|
createCredentialFn = pluginResult.createCredential;
|
|
360
|
+
credentialContext = pluginResult.credentialContext;
|
|
350
361
|
}
|
|
351
362
|
catch (error) {
|
|
352
363
|
results.push(skip(tag, error.message));
|
|
@@ -356,14 +367,58 @@ async function attemptCryptoPayment(challenge, tag, ctx) {
|
|
|
356
367
|
else {
|
|
357
368
|
methods = [directMethod];
|
|
358
369
|
}
|
|
359
|
-
|
|
370
|
+
if (directMethod && isTempoSessionChallenge(challenge)) {
|
|
371
|
+
let initial = true;
|
|
372
|
+
const mppx = Mppx.create({
|
|
373
|
+
methods: [directMethod],
|
|
374
|
+
polyfill: false,
|
|
375
|
+
async onChallenge(retry, { createCredential }) {
|
|
376
|
+
const context = await preparePayment(retry, loaded?.config.extensions);
|
|
377
|
+
assertSamePaymentRequest(challenge, retry);
|
|
378
|
+
const credential = await createCredential(context);
|
|
379
|
+
results.push(check(`${tag}: submitted`));
|
|
380
|
+
return credential;
|
|
381
|
+
},
|
|
382
|
+
fetch: async (input, init) => {
|
|
383
|
+
if (initial) {
|
|
384
|
+
initial = false;
|
|
385
|
+
return new Response(null, {
|
|
386
|
+
status: 402,
|
|
387
|
+
headers: {
|
|
388
|
+
[Constants.Headers.wwwAuthenticate]: Challenge.serialize(challenge),
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
return fetchWithTimeout(input, init ?? {}, 30_000);
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
try {
|
|
396
|
+
const response = await mppx.fetch(url, {
|
|
397
|
+
method: endpoint.method,
|
|
398
|
+
headers: fetchHeaders,
|
|
399
|
+
body: fetchBody ?? null,
|
|
400
|
+
});
|
|
401
|
+
await validatePaymentResponse(results, response, verbose, paymentChain);
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
results.push(fail(tag, error.message));
|
|
405
|
+
}
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const credential = await createAndSend(challenge, methods, createCredentialFn, tag, results, loaded?.config.extensions, credentialContext);
|
|
360
409
|
if (!credential)
|
|
361
410
|
return;
|
|
362
411
|
plugin?.prepareCredentialRequest?.({ challenge, credential, headers: fetchHeaders });
|
|
363
|
-
await sendAndValidateResponse(results, url, endpoint, credential, fetchHeaders, fetchBody, verbose, paymentChain);
|
|
412
|
+
await sendAndValidateResponse(results, url, endpoint, credential, fetchHeaders, fetchBody, verbose, paymentChain, Challenge.credentialHeader(challenge));
|
|
364
413
|
}
|
|
365
414
|
async function attemptStripePayment(challenge, tag, ctx) {
|
|
366
|
-
const { results, url, endpoint, fetchHeaders, fetchBody, verbose, loaded, options,
|
|
415
|
+
const { results, url, endpoint, fetchHeaders, fetchBody, verbose, loaded, options, stripeKey } = ctx;
|
|
416
|
+
const { plugin, method: directMethod } = resolvePlugin(challenge, loaded?.config);
|
|
417
|
+
if (!plugin && !directMethod) {
|
|
418
|
+
results.push(skip(tag, 'no Stripe payment method available'));
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const isStripeTestKey = Boolean(plugin && !loaded?.config.plugins?.includes(plugin) && ctx.isStripeTestKey);
|
|
367
422
|
const request = challenge.request;
|
|
368
423
|
const requiredAmount = isValidIntegerAmount(request.amount)
|
|
369
424
|
? BigInt(request.amount)
|
|
@@ -392,40 +447,40 @@ async function attemptStripePayment(challenge, tag, ctx) {
|
|
|
392
447
|
if (!options.silent)
|
|
393
448
|
console.log(pc.dim(` Auto-approved: ${paymentDesc}`));
|
|
394
449
|
}
|
|
395
|
-
// Resolve plugin
|
|
396
|
-
const resolved = resolvePlugin(challenge, loaded?.config);
|
|
397
|
-
const plugin = resolved.plugin;
|
|
398
|
-
if (!plugin) {
|
|
399
|
-
results.push(skip(tag, 'no Stripe plugin available'));
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
450
|
let methods;
|
|
403
451
|
let createCredentialFn;
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
methodOpts
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
452
|
+
let credentialContext;
|
|
453
|
+
if (plugin) {
|
|
454
|
+
try {
|
|
455
|
+
const methodOpts = { paymentMethod: 'pm_card_visa' };
|
|
456
|
+
if (stripeKey)
|
|
457
|
+
methodOpts.secretKey = stripeKey;
|
|
458
|
+
const pluginResult = await plugin.setup({
|
|
459
|
+
challenge,
|
|
460
|
+
options: {},
|
|
461
|
+
methodOpts,
|
|
462
|
+
});
|
|
463
|
+
methods = pluginResult.methods;
|
|
464
|
+
createCredentialFn = pluginResult.createCredential;
|
|
465
|
+
credentialContext = pluginResult.credentialContext;
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
results.push(skip(tag, error.message));
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
415
471
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
return;
|
|
472
|
+
else {
|
|
473
|
+
methods = [directMethod];
|
|
419
474
|
}
|
|
420
|
-
const credential = await createAndSend(challenge, methods, createCredentialFn, tag, results);
|
|
475
|
+
const credential = await createAndSend(challenge, methods, createCredentialFn, tag, results, loaded?.config.extensions, credentialContext);
|
|
421
476
|
if (!credential)
|
|
422
477
|
return;
|
|
423
|
-
plugin
|
|
478
|
+
plugin?.prepareCredentialRequest?.({ challenge, credential, headers: fetchHeaders });
|
|
424
479
|
// Stripe testmode: detect livemode rejection gracefully
|
|
425
480
|
if (isStripeTestKey) {
|
|
426
481
|
const resp = await fetchWithTimeout(url, {
|
|
427
482
|
method: endpoint.method,
|
|
428
|
-
headers: { ...fetchHeaders, [
|
|
483
|
+
headers: { ...fetchHeaders, [Challenge.credentialHeader(challenge)]: credential },
|
|
429
484
|
body: fetchBody ?? null,
|
|
430
485
|
}, 30_000);
|
|
431
486
|
if (resp.status >= 200 && resp.status < 300) {
|
|
@@ -436,21 +491,22 @@ async function attemptStripePayment(challenge, tag, ctx) {
|
|
|
436
491
|
}
|
|
437
492
|
return;
|
|
438
493
|
}
|
|
439
|
-
await sendAndValidateResponse(results, url, endpoint, credential, fetchHeaders, fetchBody, verbose, undefined);
|
|
494
|
+
await sendAndValidateResponse(results, url, endpoint, credential, fetchHeaders, fetchBody, verbose, undefined, Challenge.credentialHeader(challenge));
|
|
440
495
|
}
|
|
441
|
-
async function createAndSend(challenge, methods, createCredentialFn, tag, results) {
|
|
496
|
+
async function createAndSend(challenge, methods, createCredentialFn, tag, results, extensions, initialCredentialContext) {
|
|
442
497
|
const fakeResponse = new Response(null, {
|
|
443
498
|
status: 402,
|
|
444
499
|
headers: { [Constants.Headers.wwwAuthenticate]: Challenge.serialize(challenge) },
|
|
445
500
|
});
|
|
446
501
|
try {
|
|
502
|
+
const credentialContext = await preparePayment(challenge, extensions, initialCredentialContext);
|
|
447
503
|
let credential;
|
|
448
504
|
if (createCredentialFn) {
|
|
449
|
-
credential = await createCredentialFn(fakeResponse);
|
|
505
|
+
credential = await createCredentialFn(fakeResponse, credentialContext);
|
|
450
506
|
}
|
|
451
507
|
else {
|
|
452
508
|
const mppx = Mppx.create({ methods, polyfill: false });
|
|
453
|
-
credential = await mppx.createCredential(fakeResponse);
|
|
509
|
+
credential = await mppx.createCredential(fakeResponse, credentialContext);
|
|
454
510
|
}
|
|
455
511
|
results.push(check(`${tag}: submitted`));
|
|
456
512
|
return credential;
|
|
@@ -466,12 +522,12 @@ async function createAndSend(challenge, methods, createCredentialFn, tag, result
|
|
|
466
522
|
return undefined;
|
|
467
523
|
}
|
|
468
524
|
}
|
|
469
|
-
async function sendAndValidateResponse(results, url, endpoint, credential, baseHeaders, fetchBody, verbose, explorerChain) {
|
|
525
|
+
async function sendAndValidateResponse(results, url, endpoint, credential, baseHeaders, fetchBody, verbose, explorerChain, credentialHeader = Constants.Headers.authorization) {
|
|
470
526
|
let paymentResponse;
|
|
471
527
|
try {
|
|
472
528
|
paymentResponse = await fetchWithTimeout(url, {
|
|
473
529
|
method: endpoint.method,
|
|
474
|
-
headers: { ...baseHeaders, [
|
|
530
|
+
headers: { ...baseHeaders, [credentialHeader]: credential },
|
|
475
531
|
body: fetchBody ?? null,
|
|
476
532
|
}, 30_000);
|
|
477
533
|
}
|
|
@@ -479,6 +535,9 @@ async function sendAndValidateResponse(results, url, endpoint, credential, baseH
|
|
|
479
535
|
results.push(fail('Payment: send credential', error.message));
|
|
480
536
|
return results;
|
|
481
537
|
}
|
|
538
|
+
return validatePaymentResponse(results, paymentResponse, verbose, explorerChain);
|
|
539
|
+
}
|
|
540
|
+
async function validatePaymentResponse(results, paymentResponse, verbose, explorerChain) {
|
|
482
541
|
if (paymentResponse.status === 402) {
|
|
483
542
|
const body = await paymentResponse.text().catch(() => '');
|
|
484
543
|
let detail = 'Payment rejected';
|
package/dist/client/Mppx.d.ts
CHANGED
|
@@ -24,6 +24,25 @@ export type PreparedPayment<methods extends readonly Method.AnyClient[] = readon
|
|
|
24
24
|
/** Attaches a credential using the protocol that produced the selected challenge. */
|
|
25
25
|
setCredential: (request: Transport.RequestOf<transport>, credential: string) => Transport.RequestOf<transport>;
|
|
26
26
|
}>;
|
|
27
|
+
/** A payment challenge prepared together with the exact HTTP request that produced it. */
|
|
28
|
+
export type PreparedRequest<methods extends readonly Method.AnyClient[]> = Readonly<PreparedPayment<methods, Transport.Transport<RequestInit, Response>> & {
|
|
29
|
+
/** Exact request that returned the selected payment challenge. */
|
|
30
|
+
request: Request;
|
|
31
|
+
/** Payment-required response returned for {@link request}. */
|
|
32
|
+
response: Response;
|
|
33
|
+
/** Redirects followed before receiving the payment challenge. */
|
|
34
|
+
redirects: readonly PreparedRequest.Redirect[];
|
|
35
|
+
/** Creates and sends a credential to the prepared request without following redirects. */
|
|
36
|
+
pay: (context?: AnyContextFor<methods> | undefined) => Promise<Response>;
|
|
37
|
+
}>;
|
|
38
|
+
export declare namespace PreparedRequest {
|
|
39
|
+
/** A redirect followed while discovering a payment challenge. */
|
|
40
|
+
type Redirect = Readonly<{
|
|
41
|
+
from: string;
|
|
42
|
+
status: number;
|
|
43
|
+
to: string;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
27
46
|
/**
|
|
28
47
|
* Client-side payment handler.
|
|
29
48
|
*/
|
|
@@ -50,6 +69,12 @@ export type Mppx<methods extends Methods = Methods, transport extends Transport.
|
|
|
50
69
|
* ```
|
|
51
70
|
*/
|
|
52
71
|
preparePayment: (response: Transport.ResponseOf<transport>, options?: preparePayment.Options<FlattenMethods<methods>, transport> | undefined) => Promise<PreparedPayment<FlattenMethods<methods>, transport>>;
|
|
72
|
+
/**
|
|
73
|
+
* Follows safe pre-payment redirects and prepares the challenge with the exact request that
|
|
74
|
+
* produced it. Credential-bearing requests never follow redirects. Requires a runtime that
|
|
75
|
+
* exposes manual redirect responses; browsers return opaque redirects and are not supported.
|
|
76
|
+
*/
|
|
77
|
+
prepareRequest: transport extends Transport.Transport<RequestInit, Response> ? (input: RequestInfo | URL, init?: RequestInit | undefined, options?: prepareRequest.Options<FlattenMethods<methods>> | undefined) => Promise<PreparedRequest<FlattenMethods<methods>>> : never;
|
|
53
78
|
/** Creates a credential from a payment-required response by routing to the correct method. */
|
|
54
79
|
createCredential: (response: Transport.ResponseOf<transport>, context?: AnyContextFor<FlattenMethods<methods>> | undefined, options?: createCredential.Options<FlattenMethods<methods>, transport> | undefined) => Promise<string>;
|
|
55
80
|
/** Register a client event handler by canonical event name. */
|
|
@@ -89,6 +114,13 @@ export declare namespace preparePayment {
|
|
|
89
114
|
/** Options for selecting a payment without creating its credential. */
|
|
90
115
|
type Options<methods extends readonly Method.AnyClient[] = readonly Method.AnyClient[], transport extends Transport.AnyTransport = Transport.Transport> = createCredential.Options<methods, transport>;
|
|
91
116
|
}
|
|
117
|
+
export declare namespace prepareRequest {
|
|
118
|
+
/** Options for preparing a request-bound payment. */
|
|
119
|
+
type Options<methods extends readonly Method.AnyClient[] = readonly Method.AnyClient[]> = Omit<preparePayment.Options<methods, Transport.Transport<RequestInit, Response>>, 'request'> & {
|
|
120
|
+
/** Maximum redirects followed before rejecting the request. @default 20 */
|
|
121
|
+
maxRedirects?: number | undefined;
|
|
122
|
+
};
|
|
123
|
+
}
|
|
92
124
|
export declare namespace createCredential {
|
|
93
125
|
type Options<methods extends readonly Method.AnyClient[] = readonly Method.AnyClient[], transport extends Transport.AnyTransport = Transport.Transport> = {
|
|
94
126
|
/** Request-local Accept-Payment override for manual rawFetch + createCredential flows. */
|