mppx 0.9.3 → 0.10.1
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 +20 -0
- package/dist/cli/cli.js +137 -137
- package/dist/cli/internal.d.ts +4 -1
- package/dist/cli/internal.js +38 -5
- package/dist/cli/plugins/index.js +13 -13
- 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 +106 -53
- package/dist/client/Mppx.d.ts +45 -0
- package/dist/client/Mppx.js +191 -6
- package/dist/client/internal/Fetch.d.ts +6 -0
- package/dist/client/internal/Fetch.js +4 -2
- package/dist/internal/version.d.ts +2 -2
- package/dist/internal/version.js +1 -1
- package/dist/tempo/client/Charge.d.ts +8 -2
- package/dist/tempo/client/Charge.js +19 -11
- 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 +6 -0
- package/dist/tempo/session/client/SessionManager.js +7 -0
- 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/package.json +1 -1
|
@@ -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, preparePayment, 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';
|
|
@@ -156,6 +157,8 @@ export async function validatePaymentFlow(baseUrl, endpoint, verbose, options) {
|
|
|
156
157
|
const tempoTestnetChallenge = challenges.find((ch) => {
|
|
157
158
|
if (ch.method !== Constants.Methods.tempo)
|
|
158
159
|
return false;
|
|
160
|
+
if (flattenConfigMethods(loaded?.config)?.some((method) => method.name === ch.method && method.intent === ch.intent))
|
|
161
|
+
return false;
|
|
159
162
|
const req = ch.request;
|
|
160
163
|
const md = req.methodDetails;
|
|
161
164
|
return typeof md?.chainId === 'number' && md.chainId !== tempoChainIds.mainnet;
|
|
@@ -174,7 +177,7 @@ export async function validatePaymentFlow(baseUrl, endpoint, verbose, options) {
|
|
|
174
177
|
const credentialContext = await preparePayment(tempoTestnetChallenge, loaded?.config.extensions);
|
|
175
178
|
const cred = await mppx.createCredential(fakeResp, credentialContext);
|
|
176
179
|
results.push(check('Payment: submitted', 'ephemeral testnet wallet'));
|
|
177
|
-
await sendAndValidateResponse(results, url, endpoint, cred, fetchHeaders, fetchBody, verbose, tempoModerato);
|
|
180
|
+
await sendAndValidateResponse(results, url, endpoint, cred, fetchHeaders, fetchBody, verbose, tempoModerato, Challenge.credentialHeader(tempoTestnetChallenge));
|
|
178
181
|
}
|
|
179
182
|
catch (error) {
|
|
180
183
|
results.push(fail('Payment: create credential', error.message));
|
|
@@ -256,31 +259,42 @@ async function attemptCryptoPayment(challenge, tag, ctx) {
|
|
|
256
259
|
: undefined;
|
|
257
260
|
const decimals = methodDetails?.decimals ?? request.decimals ?? 6;
|
|
258
261
|
const currency = request.currency;
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
walletAddress = await resolveWalletAddress();
|
|
263
|
-
}
|
|
264
|
-
catch { }
|
|
265
|
-
if (!walletAddress) {
|
|
266
|
-
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)));
|
|
267
265
|
return;
|
|
268
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
|
+
}
|
|
269
279
|
// Pre-flight balance check and chain resolution
|
|
270
280
|
let paymentChain;
|
|
271
|
-
if (challenge.method === Constants.Methods.tempo)
|
|
272
|
-
|
|
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
|
+
}
|
|
273
287
|
else if (challenge.method === Constants.Methods.evm) {
|
|
274
288
|
const chainId = methodDetails?.chainId;
|
|
275
289
|
if (chainId)
|
|
276
290
|
paymentChain = resolveEvmChain(chainId);
|
|
277
291
|
}
|
|
278
292
|
let tokenSymbol;
|
|
279
|
-
if (requiredAmount && currency && paymentChain) {
|
|
293
|
+
if (walletAddress && requiredAmount && currency && paymentChain) {
|
|
280
294
|
try {
|
|
281
295
|
let balance;
|
|
282
296
|
if (challenge.method === Constants.Methods.tempo) {
|
|
283
|
-
const client = createClient({ chain:
|
|
297
|
+
const client = createClient({ chain: paymentChain, transport: http() });
|
|
284
298
|
const info = await fetchTokenInfo(client, currency, walletAddress);
|
|
285
299
|
balance = info.balance;
|
|
286
300
|
tokenSymbol = info.symbol;
|
|
@@ -309,8 +323,10 @@ async function attemptCryptoPayment(challenge, tag, ctx) {
|
|
|
309
323
|
const chainName = paymentChain?.name;
|
|
310
324
|
const paymentDesc = `${amountDisplay}${tokenDisplay ? ` ${tokenDisplay}` : ''}${chainName ? ` on ${chainName}` : ''}`;
|
|
311
325
|
if (!options.silent)
|
|
312
|
-
console.log(pc.dim(
|
|
313
|
-
|
|
326
|
+
console.log(pc.dim(walletAddress
|
|
327
|
+
? ` Attempting payment with wallet ${walletAddress}`
|
|
328
|
+
: ' Attempting payment with configured method'));
|
|
329
|
+
if (!directMethod && paymentChain?.testnet) {
|
|
314
330
|
if (!options.silent)
|
|
315
331
|
console.log(pc.dim(` Auto-approved: ${paymentDesc} (testnet)`));
|
|
316
332
|
}
|
|
@@ -329,14 +345,6 @@ async function attemptCryptoPayment(challenge, tag, ctx) {
|
|
|
329
345
|
if (!options.silent)
|
|
330
346
|
console.log(pc.dim(` Auto-approved: ${paymentDesc}`));
|
|
331
347
|
}
|
|
332
|
-
// Resolve plugin and pay
|
|
333
|
-
const resolved = resolvePlugin(challenge, loaded?.config);
|
|
334
|
-
const plugin = resolved.plugin;
|
|
335
|
-
const directMethod = resolved.method;
|
|
336
|
-
if (!plugin && !directMethod) {
|
|
337
|
-
results.push(skip(tag, methodSetupHint(challenge)));
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
348
|
let methods;
|
|
341
349
|
let createCredentialFn;
|
|
342
350
|
let credentialContext;
|
|
@@ -359,14 +367,58 @@ async function attemptCryptoPayment(challenge, tag, ctx) {
|
|
|
359
367
|
else {
|
|
360
368
|
methods = [directMethod];
|
|
361
369
|
}
|
|
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
|
+
}
|
|
362
408
|
const credential = await createAndSend(challenge, methods, createCredentialFn, tag, results, loaded?.config.extensions, credentialContext);
|
|
363
409
|
if (!credential)
|
|
364
410
|
return;
|
|
365
411
|
plugin?.prepareCredentialRequest?.({ challenge, credential, headers: fetchHeaders });
|
|
366
|
-
await sendAndValidateResponse(results, url, endpoint, credential, fetchHeaders, fetchBody, verbose, paymentChain);
|
|
412
|
+
await sendAndValidateResponse(results, url, endpoint, credential, fetchHeaders, fetchBody, verbose, paymentChain, Challenge.credentialHeader(challenge));
|
|
367
413
|
}
|
|
368
414
|
async function attemptStripePayment(challenge, tag, ctx) {
|
|
369
|
-
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);
|
|
370
422
|
const request = challenge.request;
|
|
371
423
|
const requiredAmount = isValidIntegerAmount(request.amount)
|
|
372
424
|
? BigInt(request.amount)
|
|
@@ -395,42 +447,40 @@ async function attemptStripePayment(challenge, tag, ctx) {
|
|
|
395
447
|
if (!options.silent)
|
|
396
448
|
console.log(pc.dim(` Auto-approved: ${paymentDesc}`));
|
|
397
449
|
}
|
|
398
|
-
// Resolve plugin
|
|
399
|
-
const resolved = resolvePlugin(challenge, loaded?.config);
|
|
400
|
-
const plugin = resolved.plugin;
|
|
401
|
-
if (!plugin) {
|
|
402
|
-
results.push(skip(tag, 'no Stripe plugin available'));
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
450
|
let methods;
|
|
406
451
|
let createCredentialFn;
|
|
407
452
|
let credentialContext;
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
+
}
|
|
420
471
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
return;
|
|
472
|
+
else {
|
|
473
|
+
methods = [directMethod];
|
|
424
474
|
}
|
|
425
475
|
const credential = await createAndSend(challenge, methods, createCredentialFn, tag, results, loaded?.config.extensions, credentialContext);
|
|
426
476
|
if (!credential)
|
|
427
477
|
return;
|
|
428
|
-
plugin
|
|
478
|
+
plugin?.prepareCredentialRequest?.({ challenge, credential, headers: fetchHeaders });
|
|
429
479
|
// Stripe testmode: detect livemode rejection gracefully
|
|
430
480
|
if (isStripeTestKey) {
|
|
431
481
|
const resp = await fetchWithTimeout(url, {
|
|
432
482
|
method: endpoint.method,
|
|
433
|
-
headers: { ...fetchHeaders, [
|
|
483
|
+
headers: { ...fetchHeaders, [Challenge.credentialHeader(challenge)]: credential },
|
|
434
484
|
body: fetchBody ?? null,
|
|
435
485
|
}, 30_000);
|
|
436
486
|
if (resp.status >= 200 && resp.status < 300) {
|
|
@@ -441,7 +491,7 @@ async function attemptStripePayment(challenge, tag, ctx) {
|
|
|
441
491
|
}
|
|
442
492
|
return;
|
|
443
493
|
}
|
|
444
|
-
await sendAndValidateResponse(results, url, endpoint, credential, fetchHeaders, fetchBody, verbose, undefined);
|
|
494
|
+
await sendAndValidateResponse(results, url, endpoint, credential, fetchHeaders, fetchBody, verbose, undefined, Challenge.credentialHeader(challenge));
|
|
445
495
|
}
|
|
446
496
|
async function createAndSend(challenge, methods, createCredentialFn, tag, results, extensions, initialCredentialContext) {
|
|
447
497
|
const fakeResponse = new Response(null, {
|
|
@@ -472,12 +522,12 @@ async function createAndSend(challenge, methods, createCredentialFn, tag, result
|
|
|
472
522
|
return undefined;
|
|
473
523
|
}
|
|
474
524
|
}
|
|
475
|
-
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) {
|
|
476
526
|
let paymentResponse;
|
|
477
527
|
try {
|
|
478
528
|
paymentResponse = await fetchWithTimeout(url, {
|
|
479
529
|
method: endpoint.method,
|
|
480
|
-
headers: { ...baseHeaders, [
|
|
530
|
+
headers: { ...baseHeaders, [credentialHeader]: credential },
|
|
481
531
|
body: fetchBody ?? null,
|
|
482
532
|
}, 30_000);
|
|
483
533
|
}
|
|
@@ -485,6 +535,9 @@ async function sendAndValidateResponse(results, url, endpoint, credential, baseH
|
|
|
485
535
|
results.push(fail('Payment: send credential', error.message));
|
|
486
536
|
return results;
|
|
487
537
|
}
|
|
538
|
+
return validatePaymentResponse(results, paymentResponse, verbose, explorerChain);
|
|
539
|
+
}
|
|
540
|
+
async function validatePaymentResponse(results, paymentResponse, verbose, explorerChain) {
|
|
488
541
|
if (paymentResponse.status === 402) {
|
|
489
542
|
const body = await paymentResponse.text().catch(() => '');
|
|
490
543
|
let detail = 'Payment rejected';
|
package/dist/client/Mppx.d.ts
CHANGED
|
@@ -24,6 +24,30 @@ 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
|
+
/** An HTTP request prepared together with its response and optional payment. */
|
|
28
|
+
export type PreparedRequest<methods extends readonly Method.AnyClient[], requirePayment extends boolean = false> = Readonly<{
|
|
29
|
+
/** Exact request that produced {@link response}. */
|
|
30
|
+
request: Request;
|
|
31
|
+
/** Response returned for {@link request}. */
|
|
32
|
+
response: Response;
|
|
33
|
+
/** Redirects followed before receiving {@link response}. */
|
|
34
|
+
redirects: readonly PreparedRequest.Redirect[];
|
|
35
|
+
/** Selected payment when the response requires payment. */
|
|
36
|
+
payment: requirePayment extends true ? PreparedRequest.Payment<methods> : PreparedRequest.Payment<methods> | undefined;
|
|
37
|
+
}>;
|
|
38
|
+
export declare namespace PreparedRequest {
|
|
39
|
+
/** A request-bound payment that can be inspected or paid. */
|
|
40
|
+
type Payment<methods extends readonly Method.AnyClient[]> = Readonly<PreparedPayment<methods, Transport.Transport<RequestInit, Response>> & {
|
|
41
|
+
/** Creates and sends a credential to the prepared request without following redirects. */
|
|
42
|
+
pay: (context?: AnyContextFor<methods> | undefined) => Promise<Response>;
|
|
43
|
+
}>;
|
|
44
|
+
/** A redirect followed while discovering a payment challenge. */
|
|
45
|
+
type Redirect = Readonly<{
|
|
46
|
+
from: string;
|
|
47
|
+
status: number;
|
|
48
|
+
to: string;
|
|
49
|
+
}>;
|
|
50
|
+
}
|
|
27
51
|
/**
|
|
28
52
|
* Client-side payment handler.
|
|
29
53
|
*/
|
|
@@ -50,6 +74,18 @@ export type Mppx<methods extends Methods = Methods, transport extends Transport.
|
|
|
50
74
|
* ```
|
|
51
75
|
*/
|
|
52
76
|
preparePayment: (response: Transport.ResponseOf<transport>, options?: preparePayment.Options<FlattenMethods<methods>, transport> | undefined) => Promise<PreparedPayment<FlattenMethods<methods>, transport>>;
|
|
77
|
+
/**
|
|
78
|
+
* Follows safe pre-payment redirects and returns the response with its exact request. When the
|
|
79
|
+
* response requires payment, prepares its selected challenge without creating a credential.
|
|
80
|
+
* Credential-bearing requests never follow redirects. Requires a runtime that exposes manual
|
|
81
|
+
* redirect responses; browsers return opaque redirects and are not supported.
|
|
82
|
+
*/
|
|
83
|
+
prepareRequest: transport extends Transport.Transport<RequestInit, Response> ? {
|
|
84
|
+
<const requirePayment extends boolean>(input: RequestInfo | URL, init: RequestInit | undefined, options: prepareRequest.Options<FlattenMethods<methods>, requirePayment> & {
|
|
85
|
+
requirePayment: requirePayment;
|
|
86
|
+
}): Promise<PreparedRequest<FlattenMethods<methods>, requirePayment>>;
|
|
87
|
+
(input: RequestInfo | URL, init?: RequestInit | undefined, options?: prepareRequest.Options<FlattenMethods<methods>> | undefined): Promise<PreparedRequest<FlattenMethods<methods>>>;
|
|
88
|
+
} : never;
|
|
53
89
|
/** Creates a credential from a payment-required response by routing to the correct method. */
|
|
54
90
|
createCredential: (response: Transport.ResponseOf<transport>, context?: AnyContextFor<FlattenMethods<methods>> | undefined, options?: createCredential.Options<FlattenMethods<methods>, transport> | undefined) => Promise<string>;
|
|
55
91
|
/** Register a client event handler by canonical event name. */
|
|
@@ -89,6 +125,15 @@ export declare namespace preparePayment {
|
|
|
89
125
|
/** Options for selecting a payment without creating its credential. */
|
|
90
126
|
type Options<methods extends readonly Method.AnyClient[] = readonly Method.AnyClient[], transport extends Transport.AnyTransport = Transport.Transport> = createCredential.Options<methods, transport>;
|
|
91
127
|
}
|
|
128
|
+
export declare namespace prepareRequest {
|
|
129
|
+
/** Options for preparing a request-bound payment. */
|
|
130
|
+
type Options<methods extends readonly Method.AnyClient[] = readonly Method.AnyClient[], requirePayment extends boolean = false> = Omit<preparePayment.Options<methods, Transport.Transport<RequestInit, Response>>, 'request'> & {
|
|
131
|
+
/** Maximum redirects followed before rejecting the request. @default 20 */
|
|
132
|
+
maxRedirects?: number | undefined;
|
|
133
|
+
/** Throw when the response does not require payment. @default false */
|
|
134
|
+
requirePayment?: requirePayment | undefined;
|
|
135
|
+
};
|
|
136
|
+
}
|
|
92
137
|
export declare namespace createCredential {
|
|
93
138
|
type Options<methods extends readonly Method.AnyClient[] = readonly Method.AnyClient[], transport extends Transport.AnyTransport = Transport.Transport> = {
|
|
94
139
|
/** Request-local Accept-Payment override for manual rawFetch + createCredential flows. */
|