mppx 0.8.3 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/cli/cli.d.ts.map +1 -1
  3. package/dist/cli/cli.js +2 -0
  4. package/dist/cli/cli.js.map +1 -1
  5. package/dist/cli/plugins/stripe.d.ts.map +1 -1
  6. package/dist/cli/plugins/stripe.js +32 -12
  7. package/dist/cli/plugins/stripe.js.map +1 -1
  8. package/dist/cli/utils.d.ts.map +1 -1
  9. package/dist/cli/utils.js +1 -1
  10. package/dist/cli/utils.js.map +1 -1
  11. package/dist/cli/validate/challenge.d.ts +14 -0
  12. package/dist/cli/validate/challenge.d.ts.map +1 -0
  13. package/dist/cli/validate/challenge.js +211 -0
  14. package/dist/cli/validate/challenge.js.map +1 -0
  15. package/dist/cli/validate/discovery.d.ts +10 -0
  16. package/dist/cli/validate/discovery.d.ts.map +1 -0
  17. package/dist/cli/validate/discovery.js +128 -0
  18. package/dist/cli/validate/discovery.js.map +1 -0
  19. package/dist/cli/validate/helpers.d.ts +33 -0
  20. package/dist/cli/validate/helpers.d.ts.map +1 -0
  21. package/dist/cli/validate/helpers.js +136 -0
  22. package/dist/cli/validate/helpers.js.map +1 -0
  23. package/dist/cli/validate/index.d.ts +17 -0
  24. package/dist/cli/validate/index.d.ts.map +1 -0
  25. package/dist/cli/validate/index.js +224 -0
  26. package/dist/cli/validate/index.js.map +1 -0
  27. package/dist/cli/validate/payment.d.ts +7 -0
  28. package/dist/cli/validate/payment.d.ts.map +1 -0
  29. package/dist/cli/validate/payment.js +381 -0
  30. package/dist/cli/validate/payment.js.map +1 -0
  31. package/dist/tempo/internal/auto-swap.d.ts.map +1 -1
  32. package/dist/tempo/internal/auto-swap.js +6 -1
  33. package/dist/tempo/internal/auto-swap.js.map +1 -1
  34. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  35. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  36. package/dist/tempo/server/internal/html.gen.js +1 -1
  37. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/cli/cli.test.ts +134 -29
  40. package/src/cli/cli.ts +2 -0
  41. package/src/cli/mcp.test.ts +1 -0
  42. package/src/cli/plugins/stripe.ts +31 -12
  43. package/src/cli/utils.test.ts +36 -2
  44. package/src/cli/utils.ts +2 -1
  45. package/src/cli/validate/challenge.ts +335 -0
  46. package/src/cli/validate/discovery.ts +120 -0
  47. package/src/cli/validate/helpers.ts +154 -0
  48. package/src/cli/validate/index.ts +348 -0
  49. package/src/cli/validate/payment.ts +488 -0
  50. package/src/cli/validate.test.ts +539 -0
  51. package/src/tempo/internal/auto-swap.test.ts +26 -9
  52. package/src/tempo/internal/auto-swap.ts +15 -2
  53. package/src/tempo/server/internal/html.gen.ts +1 -1
@@ -0,0 +1,224 @@
1
+ import { Cli, z } from 'incur';
2
+ import { validate as validateDiscovery } from '../../discovery/Validate.js';
3
+ import { pc } from '../utils.js';
4
+ import { validateChallenge, validateErrorHandling } from './challenge.js';
5
+ import { extractEndpointsFromDiscovery, extractRequestBodyFromDiscovery, fetchDiscoveryDoc, } from './discovery.js';
6
+ import { check, fail, parseEndpointArg, printCheck, printResults, printSection, resolveBodyForEndpoint, warn, } from './helpers.js';
7
+ import { validatePaymentFlow } from './payment.js';
8
+ const validate = Cli.create('validate', {
9
+ description: 'Validate an MPP server implementation end-to-end',
10
+ args: z.object({
11
+ url: z.string().describe('Base URL of the MPP server to validate'),
12
+ }),
13
+ options: z.object({
14
+ endpoint: z.string().optional().describe('Endpoint to test (METHOD:path). Skips discovery.'),
15
+ body: z
16
+ .string()
17
+ .optional()
18
+ .describe('Request body. With --endpoint, used directly. In discovery mode, JSON with / keys is a per-path mapping.'),
19
+ query: z.array(z.string()).optional().describe('Query parameter (key=value, repeatable)'),
20
+ verbose: z.number().default(0).meta({ count: true }).describe('Verbosity level'),
21
+ yes: z.boolean().default(false).describe('Auto-approve mainnet payments'),
22
+ }),
23
+ alias: {
24
+ endpoint: 'e',
25
+ verbose: 'v',
26
+ yes: 'y',
27
+ },
28
+ async run(c) {
29
+ const baseUrl = c.args.url.replace(/\/$/, '').replace(/\/openapi\.json$/i, '');
30
+ const verbose = c.options.verbose > 0;
31
+ const counts = { passed: 0, failed: 0, warnings: 0, skipped: 0 };
32
+ console.log(`\n${pc.bold('mppx validate')} ${pc.dim(baseUrl)}\n`);
33
+ const { endpoints, discoveryDoc, shouldExit } = await discoverEndpoints(baseUrl, c.options, counts);
34
+ if (shouldExit)
35
+ process.exit(1);
36
+ const flags = await validateEndpoints(baseUrl, endpoints, discoveryDoc, counts, {
37
+ verbose,
38
+ endpoint: c.options.endpoint,
39
+ body: c.options.body,
40
+ query: c.options.query,
41
+ yes: c.options.yes,
42
+ });
43
+ printSummary(counts, flags, endpoints.length);
44
+ },
45
+ });
46
+ export default validate;
47
+ // Each stage of validation is defined in its own function below.
48
+ async function discoverEndpoints(baseUrl, options, counts) {
49
+ let endpoints = [];
50
+ let discoveryDoc = null;
51
+ printSection('Discovery (/openapi.json)');
52
+ const discoveryResult = await fetchDiscoveryDoc(baseUrl);
53
+ if ('error' in discoveryResult) {
54
+ printCheck(fail('Document found', discoveryResult.error, 'MPP servers must serve an OpenAPI document at /openapi.json with x-payment-info extensions.'));
55
+ counts.failed++;
56
+ if (!options.endpoint) {
57
+ console.log('');
58
+ console.log(pc.yellow(' No discovery document found.'));
59
+ console.log(pc.dim(' MPP servers must serve an OpenAPI document at /openapi.json with x-payment-info extensions.'));
60
+ console.log(pc.dim(' To test a specific endpoint: mppx validate <url> --endpoint POST:/your/path'));
61
+ console.log('');
62
+ return { endpoints, discoveryDoc, shouldExit: true };
63
+ }
64
+ }
65
+ else {
66
+ printCheck(check('Document found and parseable'));
67
+ counts.passed++;
68
+ const issues = validateDiscovery(discoveryResult.doc);
69
+ const errors = issues.filter((i) => i.severity === 'error');
70
+ const warnings = issues.filter((i) => i.severity === 'warning');
71
+ if (errors.length > 0) {
72
+ printCheck(fail('Valid OpenAPI structure', `${errors.length} error(s)`));
73
+ counts.failed++;
74
+ for (const issue of errors) {
75
+ console.log(pc.dim(` ${issue.path}: ${issue.message}`));
76
+ }
77
+ }
78
+ else {
79
+ printCheck(check('Valid OpenAPI structure'));
80
+ counts.passed++;
81
+ }
82
+ for (const w of warnings) {
83
+ printCheck(warn(w.message, w.path));
84
+ counts.warnings++;
85
+ }
86
+ discoveryDoc = discoveryResult.doc;
87
+ }
88
+ // Resolve endpoints: --endpoint overrides discovery
89
+ if (options.endpoint) {
90
+ const parsed = parseEndpointArg(options.endpoint);
91
+ if (!parsed) {
92
+ console.log(pc.red(`Invalid endpoint format: "${options.endpoint}". Use METHOD:path (e.g. GET:/api/data)`));
93
+ return { endpoints, discoveryDoc, shouldExit: true };
94
+ }
95
+ endpoints.push(parsed);
96
+ }
97
+ else if (discoveryDoc) {
98
+ endpoints = extractEndpointsFromDiscovery(discoveryDoc);
99
+ const NO_AMOUNT = BigInt('999999999999999999');
100
+ endpoints.sort((a, b) => {
101
+ const aAmt = a.amount ? BigInt(a.amount) : NO_AMOUNT;
102
+ const bAmt = b.amount ? BigInt(b.amount) : NO_AMOUNT;
103
+ return aAmt < bAmt ? -1 : aAmt > bAmt ? 1 : 0;
104
+ });
105
+ if (endpoints.length === 0) {
106
+ printCheck(warn('Paid endpoints found', 'No endpoints with x-payment-info'));
107
+ counts.warnings++;
108
+ console.log(pc.dim(' Use --endpoint to specify endpoints manually.'));
109
+ return { endpoints, discoveryDoc, shouldExit: true };
110
+ }
111
+ printCheck(check('Paid endpoints found', `${endpoints.length} endpoint(s)`));
112
+ counts.passed++;
113
+ }
114
+ return { endpoints, discoveryDoc };
115
+ }
116
+ async function validateEndpoints(baseUrl, endpoints, discoveryDoc, counts, options) {
117
+ let sawTestnet = false;
118
+ let sawMainnet = false;
119
+ let paymentSucceeded = false;
120
+ let sawMppEndpoint = false;
121
+ let sawNonMppPaymentEndpoint = false;
122
+ for (const endpoint of endpoints) {
123
+ printSection(`${endpoint.method} ${endpoint.path}`);
124
+ // With --endpoint, --body is used directly. In discovery mode, resolve per-path or auto-generate.
125
+ let body;
126
+ if (options.endpoint) {
127
+ body = options.body;
128
+ }
129
+ else {
130
+ body = resolveBodyForEndpoint(options.body, endpoint.path);
131
+ if (!body && discoveryDoc) {
132
+ body = extractRequestBodyFromDiscovery(discoveryDoc, endpoint);
133
+ if (body && options.verbose) {
134
+ console.log(pc.dim(` Auto-generated body: ${body}`));
135
+ }
136
+ }
137
+ }
138
+ // Challenge
139
+ console.log(pc.dim(' Challenge'));
140
+ const { results: challengeResults, resolvedBody } = await validateChallenge(baseUrl, endpoint, options.verbose, {
141
+ body,
142
+ query: options.query,
143
+ discoveryDoc: discoveryDoc ?? undefined,
144
+ });
145
+ const effectiveBody = resolvedBody ?? body;
146
+ printResults(challengeResults, counts);
147
+ const isMppEndpoint = challengeResults.some((r) => r.severity === 'pass' && r.label === 'Challenge parseable');
148
+ if (!isMppEndpoint) {
149
+ if (challengeResults.some((r) => r.label === 'Not an MPP endpoint'))
150
+ sawNonMppPaymentEndpoint = true;
151
+ continue;
152
+ }
153
+ sawMppEndpoint = true;
154
+ const isTestnetEndpoint = challengeResults.some((r) => r.severity === 'pass' && r.label === 'Valid currency address' && r.detail === 'testnet');
155
+ if (isTestnetEndpoint)
156
+ sawTestnet = true;
157
+ else
158
+ sawMainnet = true;
159
+ // Error Handling
160
+ console.log(pc.dim(' Error Handling'));
161
+ const errorResults = await validateErrorHandling(baseUrl, endpoint, {
162
+ body: effectiveBody,
163
+ query: options.query,
164
+ });
165
+ printResults(errorResults, counts);
166
+ // Payment
167
+ console.log(pc.dim(' Payment'));
168
+ const paymentResults = await validatePaymentFlow(baseUrl, endpoint, options.verbose, {
169
+ body: effectiveBody,
170
+ query: options.query,
171
+ yes: options.yes,
172
+ });
173
+ printResults(paymentResults, counts);
174
+ if (paymentResults.some((r) => r.severity === 'pass' && r.label === 'Payment: successful')) {
175
+ paymentSucceeded = true;
176
+ }
177
+ }
178
+ return { sawMppEndpoint, sawNonMppPaymentEndpoint, sawTestnet, sawMainnet, paymentSucceeded };
179
+ }
180
+ function printSummary(counts, flags, endpointsLength) {
181
+ // No MPP endpoints found
182
+ if (!flags.sawMppEndpoint && endpointsLength > 0) {
183
+ console.log('');
184
+ if (flags.sawNonMppPaymentEndpoint) {
185
+ console.log(pc.yellow(` No MPP endpoints found. Tested ${endpointsLength} endpoint(s) but none use WWW-Authenticate: Payment.`));
186
+ console.log(pc.dim(' This server may use x402 or another payment protocol.'));
187
+ }
188
+ else if (counts.skipped > 0 && counts.failed === 0) {
189
+ console.log(pc.yellow(` Could not reach payment gate on any endpoint (all returned 401/403/200).`));
190
+ console.log(pc.dim(' The server may require authentication before payment. Try providing auth or use --endpoint with a public path.'));
191
+ }
192
+ else {
193
+ console.log(pc.yellow(` No MPP endpoints found. Tested ${endpointsLength} endpoint(s) but none use WWW-Authenticate: Payment.`));
194
+ console.log(pc.dim(' This server may use x402 or another payment protocol.'));
195
+ }
196
+ console.log('');
197
+ process.exit(1);
198
+ }
199
+ // Summary
200
+ console.log('');
201
+ const parts = [];
202
+ if (counts.passed > 0)
203
+ parts.push(pc.green(`${counts.passed} passed`));
204
+ if (counts.failed > 0)
205
+ parts.push(pc.red(`${counts.failed} failed`));
206
+ if (counts.warnings > 0)
207
+ parts.push(pc.yellow(`${counts.warnings} warning(s)`));
208
+ if (counts.skipped > 0)
209
+ parts.push(pc.yellow(`${counts.skipped} skipped`));
210
+ console.log(`${pc.bold('Summary:')} ${parts.join(', ')}`);
211
+ // Cross-promotion
212
+ if (flags.paymentSucceeded && flags.sawTestnet && !flags.sawMainnet) {
213
+ console.log('');
214
+ console.log(pc.dim(' Tip: validate your mainnet server too to confirm real payments work end-to-end.'));
215
+ }
216
+ else if (flags.sawMainnet && !flags.sawTestnet) {
217
+ console.log('');
218
+ console.log(pc.dim(' Tip: validate a testnet server too for free. This CLI automatically provisions and funds a testnet wallet for testing.'));
219
+ }
220
+ console.log('');
221
+ if (counts.failed > 0)
222
+ process.exit(1);
223
+ }
224
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/cli/validate/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { CheckResult, EndpointSpec } from './helpers.js';
2
+ export declare function validatePaymentFlow(baseUrl: string, endpoint: EndpointSpec, verbose: boolean, options?: {
3
+ body?: string | undefined;
4
+ query?: string[] | undefined;
5
+ yes?: boolean | undefined;
6
+ }): Promise<CheckResult[]>;
7
+ //# sourceMappingURL=payment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payment.d.ts","sourceRoot":"","sources":["../../../src/cli/validate/payment.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAyC7D,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,YAAY,EACtB,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAAE,GAC/F,OAAO,CAAC,WAAW,EAAE,CAAC,CAwPxB"}
@@ -0,0 +1,381 @@
1
+ import { createClient, http } from 'viem';
2
+ import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts';
3
+ import { waitForTransactionReceipt } from 'viem/actions';
4
+ import { Actions } from 'viem/tempo';
5
+ import { tempoModerato, tempo as tempoMainnetChain } from 'viem/tempo/chains';
6
+ import * as Challenge from '../../Challenge.js';
7
+ import * as Mppx from '../../client/Mppx.js';
8
+ import * as Constants from '../../Constants.js';
9
+ import * as Receipt from '../../Receipt.js';
10
+ import { tempo as tempoMethods } from '../../tempo/client/index.js';
11
+ import { chainId as tempoChainIds } from '../../tempo/internal/defaults.js';
12
+ import { resolveAccount, resolveAccountName } from '../account.js';
13
+ import { loadConfig, selectChallenge } from '../internal.js';
14
+ import { fetchTokenInfo, confirm, pc } from '../utils.js';
15
+ import { buildUrl, check, fail, fetchWithTimeout, formatBytes, skip, warn } from './helpers.js';
16
+ async function provisionAndPayTestnet(challenge, verbose) {
17
+ try {
18
+ console.log(pc.dim(' Provisioning testnet wallet and funding via faucet...'));
19
+ const key = generatePrivateKey();
20
+ const account = privateKeyToAccount(key);
21
+ const client = createClient({ chain: tempoModerato, transport: http() });
22
+ const hashes = await Actions.faucet.fund(client, { account });
23
+ await Promise.all(hashes.map((hash) => waitForTransactionReceipt(client, { hash })));
24
+ console.log(pc.dim(` Using wallet: ${account.address}`));
25
+ const methods = [...tempoMethods({ account })];
26
+ return { methods };
27
+ }
28
+ catch (error) {
29
+ if (verbose)
30
+ console.log(pc.dim(` Provisioning failed: ${error.message}`));
31
+ return undefined;
32
+ }
33
+ }
34
+ async function resolveWalletAddress() {
35
+ const accountName = resolveAccountName();
36
+ const { isTempoAccount } = await import('../utils.js');
37
+ const { resolveTempoAccount } = await import('../plugins/tempo.js');
38
+ if (isTempoAccount(accountName)) {
39
+ const entry = resolveTempoAccount(accountName);
40
+ if (entry)
41
+ return entry.wallet_address;
42
+ }
43
+ try {
44
+ const account = await resolveAccount();
45
+ return account.address;
46
+ }
47
+ catch {
48
+ return undefined;
49
+ }
50
+ }
51
+ export async function validatePaymentFlow(baseUrl, endpoint, verbose, options) {
52
+ const results = [];
53
+ const url = buildUrl(baseUrl, endpoint, options?.query);
54
+ const fetchHeaders = {};
55
+ let fetchBody;
56
+ if (options?.body) {
57
+ fetchBody = options.body;
58
+ fetchHeaders['content-type'] = 'application/json';
59
+ }
60
+ // Get a fresh challenge
61
+ let challengeResponse;
62
+ try {
63
+ challengeResponse = await fetchWithTimeout(url, {
64
+ method: endpoint.method,
65
+ headers: fetchHeaders,
66
+ body: fetchBody ?? null,
67
+ });
68
+ }
69
+ catch (error) {
70
+ results.push(fail('Payment: fetch challenge', error.message));
71
+ return results;
72
+ }
73
+ if (challengeResponse.status !== 402) {
74
+ results.push(skip('Payment: skipped', `Endpoint returned ${challengeResponse.status}`));
75
+ return results;
76
+ }
77
+ // Parse challenge
78
+ let challenge;
79
+ try {
80
+ challenge = Challenge.fromResponse(challengeResponse);
81
+ }
82
+ catch (error) {
83
+ results.push(fail('Payment: parse challenge', error.message));
84
+ return results;
85
+ }
86
+ // Detect network
87
+ const request = challenge.request;
88
+ const methodDetails = request.methodDetails;
89
+ const isTestnet = typeof methodDetails?.chainId === 'number' && methodDetails.chainId !== tempoChainIds.mainnet;
90
+ // Testnet Tempo: always use ephemeral wallet (zero-setup, free money)
91
+ if (isTestnet && challenge.method === Constants.Methods.tempo) {
92
+ const provisioned = await provisionAndPayTestnet(challenge, verbose);
93
+ if (provisioned) {
94
+ const fakeResp = new Response(null, {
95
+ status: 402,
96
+ headers: { [Constants.Headers.wwwAuthenticate]: Challenge.serialize(challenge) },
97
+ });
98
+ try {
99
+ const mppx = Mppx.create({ methods: provisioned.methods, polyfill: false });
100
+ const cred = await mppx.createCredential(fakeResp);
101
+ results.push(check('Payment: credential created', 'ephemeral testnet wallet'));
102
+ return await sendAndValidateResponse(results, url, endpoint, cred, fetchHeaders, fetchBody, verbose, true);
103
+ }
104
+ catch (error) {
105
+ results.push(fail('Payment: create credential', error.message));
106
+ return results;
107
+ }
108
+ }
109
+ else {
110
+ results.push(fail('Payment: auto-provision wallet', 'Failed to create and fund testnet wallet'));
111
+ return results;
112
+ }
113
+ }
114
+ // Mainnet: use configured wallet
115
+ const loaded = await loadConfig().catch(() => undefined);
116
+ const selected = selectChallenge([challenge], loaded?.config);
117
+ if (!selected) {
118
+ results.push(skip('Payment: no configured method', `Need ${challenge.method}/${challenge.intent}`, 'Run "mppx account create" to create a local wallet for payment testing. The wallet is stored on your machine and only used by mppx.'));
119
+ return results;
120
+ }
121
+ const { plugin, method: directMethod } = selected;
122
+ if (!plugin && !directMethod) {
123
+ results.push(skip('Payment: no plugin available', `${challenge.method}/${challenge.intent}`));
124
+ return results;
125
+ }
126
+ const requiredAmount = request.amount ? BigInt(request.amount) : undefined;
127
+ // Display only -- defaults to 6 (standard for USDC/PathUSD tokens)
128
+ const decimals = request.decimals ?? 6;
129
+ const currency = request.currency;
130
+ // Pre-flight balance check
131
+ let walletAddress;
132
+ if (challenge.method === Constants.Methods.tempo && requiredAmount && currency) {
133
+ try {
134
+ walletAddress = await resolveWalletAddress();
135
+ if (walletAddress) {
136
+ const client = createClient({ chain: tempoMainnetChain, transport: http() });
137
+ const tokenInfo = await fetchTokenInfo(client, currency, walletAddress);
138
+ const requiredDisplay = `$${(Number(requiredAmount) / 10 ** decimals).toFixed(2)}`;
139
+ const balanceDisplay = `$${(Number(tokenInfo.balance) / 10 ** tokenInfo.decimals).toFixed(2)}`;
140
+ if (tokenInfo.balance < requiredAmount) {
141
+ console.log(pc.dim(` Wallet: ${walletAddress}`));
142
+ console.log(pc.dim(` Balance: ${balanceDisplay} ${tokenInfo.symbol}`));
143
+ const hint = `Wallet ${walletAddress} has ${balanceDisplay} but endpoint requires ${requiredDisplay}. Fund this wallet to run payment tests, or use a testnet server.`;
144
+ results.push(skip('Payment: insufficient balance', `Have ${balanceDisplay}, need ${requiredDisplay}`, hint));
145
+ return results;
146
+ }
147
+ }
148
+ }
149
+ catch (e) {
150
+ if (verbose)
151
+ console.log(pc.dim(` Balance check skipped: ${e.message}`));
152
+ }
153
+ }
154
+ // Prompt before paying on mainnet (unless --yes or non-interactive)
155
+ const amountDisplay = requiredAmount
156
+ ? `$${(Number(requiredAmount) / 10 ** decimals).toFixed(2)}`
157
+ : 'unknown amount';
158
+ if (walletAddress)
159
+ console.log(pc.dim(` Using wallet: ${walletAddress}`));
160
+ const isInteractive = process.stdin.isTTY ?? false;
161
+ if (!options?.yes && !isInteractive) {
162
+ results.push(skip('Payment: skipped', 'Non-interactive mode. Use --yes to approve mainnet payments.'));
163
+ return results;
164
+ }
165
+ if (!options?.yes) {
166
+ console.log('');
167
+ const ok = await confirm(` ${pc.yellow('Mainnet payment:')} Will transfer ${amountDisplay} to ${String(request.recipient).slice(0, 10)}... Continue?`, false);
168
+ if (!ok) {
169
+ results.push(skip('Payment: skipped by user', 'mainnet payment declined'));
170
+ return results;
171
+ }
172
+ }
173
+ else {
174
+ console.log(pc.dim(` Auto-approved: ${amountDisplay} to ${String(request.recipient).slice(0, 10)}...`));
175
+ }
176
+ // Setup plugin or use direct method
177
+ let methods;
178
+ let createCredentialFn;
179
+ if (plugin) {
180
+ try {
181
+ const pluginResult = await plugin.setup({
182
+ challenge,
183
+ options: { network: 'mainnet' },
184
+ methodOpts: {},
185
+ });
186
+ methods = pluginResult.methods;
187
+ createCredentialFn = pluginResult.createCredential;
188
+ }
189
+ catch (error) {
190
+ const msg = error.message;
191
+ if (msg.includes('No account found') || msg.includes('not found')) {
192
+ results.push(skip('Payment: no wallet configured', msg));
193
+ }
194
+ else {
195
+ results.push(fail('Payment: wallet setup', msg));
196
+ }
197
+ return results;
198
+ }
199
+ }
200
+ else {
201
+ methods = [directMethod];
202
+ }
203
+ // Create credential
204
+ let credential;
205
+ const fakeResponse = new Response(null, {
206
+ status: 402,
207
+ headers: { [Constants.Headers.wwwAuthenticate]: Challenge.serialize(challenge) },
208
+ });
209
+ try {
210
+ if (createCredentialFn) {
211
+ credential = await createCredentialFn(fakeResponse);
212
+ }
213
+ else {
214
+ const mppx = Mppx.create({ methods, polyfill: false });
215
+ credential = await mppx.createCredential(fakeResponse);
216
+ }
217
+ }
218
+ catch (error) {
219
+ const msg = error.message;
220
+ const isInsufficientBalance = msg.toLowerCase().includes('insufficientbalance') ||
221
+ msg.toLowerCase().includes('insufficient');
222
+ if (isInsufficientBalance) {
223
+ const match = msg.match(/available:\s*(\d+),\s*required:\s*(\d+)/);
224
+ const available = match ? BigInt(match[1]) : undefined;
225
+ const required = match ? BigInt(match[2]) : requiredAmount;
226
+ const fromMatch = msg.match(/from:\s*(0x[0-9a-fA-F]{40})/);
227
+ const fromAddr = fromMatch?.[1];
228
+ const requiredDisplay = required
229
+ ? `$${(Number(required) / 10 ** decimals).toFixed(2)}`
230
+ : 'unknown';
231
+ const availableDisplay = available !== undefined ? `$${(Number(available) / 10 ** decimals).toFixed(2)}` : undefined;
232
+ const detail = availableDisplay
233
+ ? `Have ${availableDisplay}, need ${requiredDisplay}`
234
+ : `Endpoint requires ${requiredDisplay}`;
235
+ const hint = fromAddr
236
+ ? `Wallet ${fromAddr} needs at least ${requiredDisplay}. This is a local wallet created by "mppx account create". Fund it to run payment tests, or point at a testnet server for free validation.`
237
+ : `Fund your mppx wallet with at least ${requiredDisplay} to run payment tests, or use a testnet server.`;
238
+ results.push(skip('Payment: insufficient balance', detail, hint));
239
+ return results;
240
+ }
241
+ results.push(fail('Payment: create credential', msg));
242
+ return results;
243
+ }
244
+ results.push(check('Payment: credential created'));
245
+ // Prepare and send
246
+ plugin?.prepareCredentialRequest?.({ challenge, credential, headers: fetchHeaders });
247
+ return await sendAndValidateResponse(results, url, endpoint, credential, fetchHeaders, fetchBody, verbose, isTestnet);
248
+ }
249
+ async function sendAndValidateResponse(results, url, endpoint, credential, baseHeaders, fetchBody, verbose, isTestnet) {
250
+ let paymentResponse;
251
+ try {
252
+ paymentResponse = await fetchWithTimeout(url, {
253
+ method: endpoint.method,
254
+ headers: { ...baseHeaders, [Constants.Headers.authorization]: credential },
255
+ body: fetchBody ?? null,
256
+ }, 30_000);
257
+ }
258
+ catch (error) {
259
+ results.push(fail('Payment: send credential', error.message));
260
+ return results;
261
+ }
262
+ if (paymentResponse.status === 402) {
263
+ const body = await paymentResponse.text().catch(() => '');
264
+ let detail = 'Payment rejected';
265
+ try {
266
+ const problem = JSON.parse(body);
267
+ detail = problem.detail ?? problem.title ?? detail;
268
+ }
269
+ catch { }
270
+ results.push(fail('Payment: accepted', detail, 'The server rejected a valid credential. Check that your payment verification logic accepts the credential format and that the payment was processed on-chain.'));
271
+ return results;
272
+ }
273
+ if (paymentResponse.status >= 400 && paymentResponse.status < 500) {
274
+ results.push(warn('Payment: post-payment response', `Got ${paymentResponse.status}`, 'Payment succeeded but the endpoint returned a client error. The endpoint likely requires request body parameters. Use --body to provide them.'));
275
+ }
276
+ else if (paymentResponse.status >= 500) {
277
+ results.push(fail('Payment: server response', `Got ${paymentResponse.status}`, 'Payment was accepted but the server errored while generating the response. Check server logs for the underlying error.'));
278
+ return results;
279
+ }
280
+ else {
281
+ results.push(check('Payment: successful', `HTTP ${paymentResponse.status}`));
282
+ }
283
+ // Validate receipt
284
+ const receiptHeader = paymentResponse.headers.get(Constants.Headers.paymentReceipt);
285
+ if (!receiptHeader) {
286
+ results.push(fail('Payment-Receipt header present', undefined, 'After accepting payment, include a Payment-Receipt header with a base64url-encoded JSON object containing: method, reference, status ("success"), and timestamp (ISO 8601).'));
287
+ }
288
+ else {
289
+ results.push(check('Payment-Receipt header present'));
290
+ try {
291
+ const receipt = Receipt.deserialize(receiptHeader);
292
+ results.push(check('Receipt parseable'));
293
+ if (receipt.status === 'success') {
294
+ results.push(check('Receipt status is "success"'));
295
+ }
296
+ else {
297
+ results.push(fail('Receipt status is "success"', `Got: ${receipt.status}`));
298
+ }
299
+ if (receipt.reference) {
300
+ const validTxHash = /^0x[0-9a-fA-F]{64}$/.test(receipt.reference);
301
+ const validStripeRef = receipt.reference.startsWith('pi_');
302
+ if (validTxHash || validStripeRef) {
303
+ results.push(check('Receipt reference valid', receipt.reference.slice(0, 20) + '...'));
304
+ }
305
+ else {
306
+ results.push(warn('Receipt reference format', receipt.reference.slice(0, 40)));
307
+ }
308
+ }
309
+ else {
310
+ results.push(warn('Receipt has reference', 'No reference field'));
311
+ }
312
+ if (receipt.timestamp) {
313
+ const ts = new Date(receipt.timestamp);
314
+ const age = Date.now() - ts.getTime();
315
+ if (age < 60_000) {
316
+ results.push(check('Receipt timestamp recent', `${Math.round(age / 1000)}s ago`));
317
+ }
318
+ else {
319
+ results.push(warn('Receipt timestamp recent', `${Math.round(age / 60000)}m ago`));
320
+ }
321
+ }
322
+ if (verbose) {
323
+ console.log(pc.dim(` Receipt: ${JSON.stringify(receipt, null, 2)}`));
324
+ }
325
+ }
326
+ catch (error) {
327
+ results.push(fail('Receipt parseable', error.message));
328
+ }
329
+ }
330
+ // Validate response body
331
+ const contentType = paymentResponse.headers.get('content-type') ?? '';
332
+ const body = await paymentResponse.text().catch(() => '');
333
+ if (body.length > 0) {
334
+ results.push(check('Response body non-empty', `${contentType.split(';')[0]}, ${formatBytes(body.length)}`));
335
+ }
336
+ else {
337
+ const suspiciousHeaders = [...paymentResponse.headers.entries()].filter(([key]) => !key.startsWith('x-') &&
338
+ ![
339
+ 'content-type',
340
+ 'content-length',
341
+ 'date',
342
+ 'server',
343
+ 'connection',
344
+ 'keep-alive',
345
+ 'cache-control',
346
+ 'vary',
347
+ 'access-control-allow-origin',
348
+ 'payment-receipt',
349
+ 'payment-session',
350
+ 'payment-session-snapshot',
351
+ ].includes(key.toLowerCase()));
352
+ if (suspiciousHeaders.length > 0) {
353
+ results.push(warn('Response body empty -- data may be in headers only', suspiciousHeaders.map(([k]) => k).join(', ')));
354
+ }
355
+ else {
356
+ results.push(warn('Response body empty'));
357
+ }
358
+ }
359
+ if (!contentType) {
360
+ results.push(warn('Content-Type header set'));
361
+ }
362
+ else {
363
+ results.push(check('Content-Type header set', contentType.split(';')[0]));
364
+ }
365
+ // Explorer link for on-chain payments
366
+ if (receiptHeader) {
367
+ try {
368
+ const receipt = Receipt.deserialize(receiptHeader);
369
+ if (receipt.reference && /^0x[0-9a-fA-F]{64}$/.test(receipt.reference)) {
370
+ const chain = isTestnet ? tempoModerato : tempoMainnetChain;
371
+ const explorerUrl = chain.blockExplorers?.default?.url;
372
+ if (explorerUrl) {
373
+ results.push(check('On-chain transaction', `${explorerUrl}/receipt/${receipt.reference}`));
374
+ }
375
+ }
376
+ }
377
+ catch { }
378
+ }
379
+ return results;
380
+ }
381
+ //# sourceMappingURL=payment.js.map