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,348 @@
1
+ import { Cli, z } from 'incur'
2
+
3
+ import { validate as validateDiscovery } from '../../discovery/Validate.js'
4
+ import { pc } from '../utils.js'
5
+ import { validateChallenge, validateErrorHandling } from './challenge.js'
6
+ import {
7
+ extractEndpointsFromDiscovery,
8
+ extractRequestBodyFromDiscovery,
9
+ fetchDiscoveryDoc,
10
+ } from './discovery.js'
11
+ import {
12
+ check,
13
+ fail,
14
+ parseEndpointArg,
15
+ printCheck,
16
+ printResults,
17
+ printSection,
18
+ resolveBodyForEndpoint,
19
+ warn,
20
+ } from './helpers.js'
21
+ import type { Counts, EndpointSpec } from './helpers.js'
22
+ import { validatePaymentFlow } from './payment.js'
23
+
24
+ const validate = Cli.create('validate', {
25
+ description: 'Validate an MPP server implementation end-to-end',
26
+ args: z.object({
27
+ url: z.string().describe('Base URL of the MPP server to validate'),
28
+ }),
29
+ options: z.object({
30
+ endpoint: z.string().optional().describe('Endpoint to test (METHOD:path). Skips discovery.'),
31
+ body: z
32
+ .string()
33
+ .optional()
34
+ .describe(
35
+ 'Request body. With --endpoint, used directly. In discovery mode, JSON with / keys is a per-path mapping.',
36
+ ),
37
+ query: z.array(z.string()).optional().describe('Query parameter (key=value, repeatable)'),
38
+ verbose: z.number().default(0).meta({ count: true }).describe('Verbosity level'),
39
+ yes: z.boolean().default(false).describe('Auto-approve mainnet payments'),
40
+ }),
41
+ alias: {
42
+ endpoint: 'e',
43
+ verbose: 'v',
44
+ yes: 'y',
45
+ },
46
+ async run(c) {
47
+ const baseUrl = c.args.url.replace(/\/$/, '').replace(/\/openapi\.json$/i, '')
48
+ const verbose = c.options.verbose > 0
49
+ const counts: Counts = { passed: 0, failed: 0, warnings: 0, skipped: 0 }
50
+
51
+ console.log(`\n${pc.bold('mppx validate')} ${pc.dim(baseUrl)}\n`)
52
+
53
+ const { endpoints, discoveryDoc, shouldExit } = await discoverEndpoints(
54
+ baseUrl,
55
+ c.options,
56
+ counts,
57
+ )
58
+ if (shouldExit) process.exit(1)
59
+ const flags = await validateEndpoints(baseUrl, endpoints, discoveryDoc, counts, {
60
+ verbose,
61
+ endpoint: c.options.endpoint,
62
+ body: c.options.body,
63
+ query: c.options.query,
64
+ yes: c.options.yes,
65
+ })
66
+ printSummary(counts, flags, endpoints.length)
67
+ },
68
+ })
69
+
70
+ export default validate
71
+
72
+ // Each stage of validation is defined in its own function below.
73
+
74
+ async function discoverEndpoints(
75
+ baseUrl: string,
76
+ options: {
77
+ endpoint?: string | undefined
78
+ body?: string | undefined
79
+ query?: string[] | undefined
80
+ verbose: number
81
+ yes: boolean
82
+ },
83
+ counts: Counts,
84
+ ): Promise<{
85
+ endpoints: EndpointSpec[]
86
+ discoveryDoc: Record<string, unknown> | null
87
+ shouldExit?: boolean
88
+ }> {
89
+ let endpoints: EndpointSpec[] = []
90
+ let discoveryDoc: Record<string, unknown> | null = null
91
+
92
+ printSection('Discovery (/openapi.json)')
93
+ const discoveryResult = await fetchDiscoveryDoc(baseUrl)
94
+
95
+ if ('error' in discoveryResult) {
96
+ printCheck(
97
+ fail(
98
+ 'Document found',
99
+ discoveryResult.error,
100
+ 'MPP servers must serve an OpenAPI document at /openapi.json with x-payment-info extensions.',
101
+ ),
102
+ )
103
+ counts.failed++
104
+ if (!options.endpoint) {
105
+ console.log('')
106
+ console.log(pc.yellow(' No discovery document found.'))
107
+ console.log(
108
+ pc.dim(
109
+ ' MPP servers must serve an OpenAPI document at /openapi.json with x-payment-info extensions.',
110
+ ),
111
+ )
112
+ console.log(
113
+ pc.dim(' To test a specific endpoint: mppx validate <url> --endpoint POST:/your/path'),
114
+ )
115
+ console.log('')
116
+ return { endpoints, discoveryDoc, shouldExit: true }
117
+ }
118
+ } else {
119
+ printCheck(check('Document found and parseable'))
120
+ counts.passed++
121
+
122
+ const issues = validateDiscovery(discoveryResult.doc)
123
+ const errors = issues.filter((i) => i.severity === 'error')
124
+ const warnings = issues.filter((i) => i.severity === 'warning')
125
+
126
+ if (errors.length > 0) {
127
+ printCheck(fail('Valid OpenAPI structure', `${errors.length} error(s)`))
128
+ counts.failed++
129
+ for (const issue of errors) {
130
+ console.log(pc.dim(` ${issue.path}: ${issue.message}`))
131
+ }
132
+ } else {
133
+ printCheck(check('Valid OpenAPI structure'))
134
+ counts.passed++
135
+ }
136
+
137
+ for (const w of warnings) {
138
+ printCheck(warn(w.message, w.path))
139
+ counts.warnings++
140
+ }
141
+
142
+ discoveryDoc = discoveryResult.doc as Record<string, unknown>
143
+ }
144
+
145
+ // Resolve endpoints: --endpoint overrides discovery
146
+ if (options.endpoint) {
147
+ const parsed = parseEndpointArg(options.endpoint)
148
+ if (!parsed) {
149
+ console.log(
150
+ pc.red(
151
+ `Invalid endpoint format: "${options.endpoint}". Use METHOD:path (e.g. GET:/api/data)`,
152
+ ),
153
+ )
154
+ return { endpoints, discoveryDoc, shouldExit: true }
155
+ }
156
+ endpoints.push(parsed)
157
+ } else if (discoveryDoc) {
158
+ endpoints = extractEndpointsFromDiscovery(discoveryDoc)
159
+
160
+ const NO_AMOUNT = BigInt('999999999999999999')
161
+ endpoints.sort((a, b) => {
162
+ const aAmt = a.amount ? BigInt(a.amount) : NO_AMOUNT
163
+ const bAmt = b.amount ? BigInt(b.amount) : NO_AMOUNT
164
+ return aAmt < bAmt ? -1 : aAmt > bAmt ? 1 : 0
165
+ })
166
+
167
+ if (endpoints.length === 0) {
168
+ printCheck(warn('Paid endpoints found', 'No endpoints with x-payment-info'))
169
+ counts.warnings++
170
+ console.log(pc.dim(' Use --endpoint to specify endpoints manually.'))
171
+ return { endpoints, discoveryDoc, shouldExit: true }
172
+ }
173
+
174
+ printCheck(check('Paid endpoints found', `${endpoints.length} endpoint(s)`))
175
+ counts.passed++
176
+ }
177
+
178
+ return { endpoints, discoveryDoc }
179
+ }
180
+
181
+ async function validateEndpoints(
182
+ baseUrl: string,
183
+ endpoints: EndpointSpec[],
184
+ discoveryDoc: Record<string, unknown> | null,
185
+ counts: Counts,
186
+ options: {
187
+ verbose: boolean
188
+ endpoint?: string | undefined
189
+ body?: string | undefined
190
+ query?: string[] | undefined
191
+ yes: boolean
192
+ },
193
+ ): Promise<{
194
+ sawMppEndpoint: boolean
195
+ sawNonMppPaymentEndpoint: boolean
196
+ sawTestnet: boolean
197
+ sawMainnet: boolean
198
+ paymentSucceeded: boolean
199
+ }> {
200
+ let sawTestnet = false
201
+ let sawMainnet = false
202
+ let paymentSucceeded = false
203
+ let sawMppEndpoint = false
204
+ let sawNonMppPaymentEndpoint = false
205
+
206
+ for (const endpoint of endpoints) {
207
+ printSection(`${endpoint.method} ${endpoint.path}`)
208
+
209
+ // With --endpoint, --body is used directly. In discovery mode, resolve per-path or auto-generate.
210
+ let body: string | undefined
211
+ if (options.endpoint) {
212
+ body = options.body
213
+ } else {
214
+ body = resolveBodyForEndpoint(options.body, endpoint.path)
215
+ if (!body && discoveryDoc) {
216
+ body = extractRequestBodyFromDiscovery(discoveryDoc, endpoint)
217
+ if (body && options.verbose) {
218
+ console.log(pc.dim(` Auto-generated body: ${body}`))
219
+ }
220
+ }
221
+ }
222
+
223
+ // Challenge
224
+ console.log(pc.dim(' Challenge'))
225
+ const { results: challengeResults, resolvedBody } = await validateChallenge(
226
+ baseUrl,
227
+ endpoint,
228
+ options.verbose,
229
+ {
230
+ body,
231
+ query: options.query,
232
+ discoveryDoc: discoveryDoc ?? undefined,
233
+ },
234
+ )
235
+ const effectiveBody = resolvedBody ?? body
236
+ printResults(challengeResults, counts)
237
+
238
+ const isMppEndpoint = challengeResults.some(
239
+ (r) => r.severity === 'pass' && r.label === 'Challenge parseable',
240
+ )
241
+ if (!isMppEndpoint) {
242
+ if (challengeResults.some((r) => r.label === 'Not an MPP endpoint'))
243
+ sawNonMppPaymentEndpoint = true
244
+ continue
245
+ }
246
+ sawMppEndpoint = true
247
+
248
+ const isTestnetEndpoint = challengeResults.some(
249
+ (r) =>
250
+ r.severity === 'pass' && r.label === 'Valid currency address' && r.detail === 'testnet',
251
+ )
252
+ if (isTestnetEndpoint) sawTestnet = true
253
+ else sawMainnet = true
254
+
255
+ // Error Handling
256
+ console.log(pc.dim(' Error Handling'))
257
+ const errorResults = await validateErrorHandling(baseUrl, endpoint, {
258
+ body: effectiveBody,
259
+ query: options.query,
260
+ })
261
+ printResults(errorResults, counts)
262
+
263
+ // Payment
264
+ console.log(pc.dim(' Payment'))
265
+ const paymentResults = await validatePaymentFlow(baseUrl, endpoint, options.verbose, {
266
+ body: effectiveBody,
267
+ query: options.query,
268
+ yes: options.yes,
269
+ })
270
+ printResults(paymentResults, counts)
271
+ if (paymentResults.some((r) => r.severity === 'pass' && r.label === 'Payment: successful')) {
272
+ paymentSucceeded = true
273
+ }
274
+ }
275
+
276
+ return { sawMppEndpoint, sawNonMppPaymentEndpoint, sawTestnet, sawMainnet, paymentSucceeded }
277
+ }
278
+
279
+ function printSummary(
280
+ counts: Counts,
281
+ flags: {
282
+ sawMppEndpoint: boolean
283
+ sawNonMppPaymentEndpoint: boolean
284
+ sawTestnet: boolean
285
+ sawMainnet: boolean
286
+ paymentSucceeded: boolean
287
+ },
288
+ endpointsLength: number,
289
+ ): void {
290
+ // No MPP endpoints found
291
+ if (!flags.sawMppEndpoint && endpointsLength > 0) {
292
+ console.log('')
293
+ if (flags.sawNonMppPaymentEndpoint) {
294
+ console.log(
295
+ pc.yellow(
296
+ ` No MPP endpoints found. Tested ${endpointsLength} endpoint(s) but none use WWW-Authenticate: Payment.`,
297
+ ),
298
+ )
299
+ console.log(pc.dim(' This server may use x402 or another payment protocol.'))
300
+ } else if (counts.skipped > 0 && counts.failed === 0) {
301
+ console.log(
302
+ pc.yellow(` Could not reach payment gate on any endpoint (all returned 401/403/200).`),
303
+ )
304
+ console.log(
305
+ pc.dim(
306
+ ' The server may require authentication before payment. Try providing auth or use --endpoint with a public path.',
307
+ ),
308
+ )
309
+ } else {
310
+ console.log(
311
+ pc.yellow(
312
+ ` No MPP endpoints found. Tested ${endpointsLength} endpoint(s) but none use WWW-Authenticate: Payment.`,
313
+ ),
314
+ )
315
+ console.log(pc.dim(' This server may use x402 or another payment protocol.'))
316
+ }
317
+ console.log('')
318
+ process.exit(1)
319
+ }
320
+
321
+ // Summary
322
+ console.log('')
323
+ const parts: string[] = []
324
+ if (counts.passed > 0) parts.push(pc.green(`${counts.passed} passed`))
325
+ if (counts.failed > 0) parts.push(pc.red(`${counts.failed} failed`))
326
+ if (counts.warnings > 0) parts.push(pc.yellow(`${counts.warnings} warning(s)`))
327
+ if (counts.skipped > 0) parts.push(pc.yellow(`${counts.skipped} skipped`))
328
+ console.log(`${pc.bold('Summary:')} ${parts.join(', ')}`)
329
+
330
+ // Cross-promotion
331
+ if (flags.paymentSucceeded && flags.sawTestnet && !flags.sawMainnet) {
332
+ console.log('')
333
+ console.log(
334
+ pc.dim(' Tip: validate your mainnet server too to confirm real payments work end-to-end.'),
335
+ )
336
+ } else if (flags.sawMainnet && !flags.sawTestnet) {
337
+ console.log('')
338
+ console.log(
339
+ pc.dim(
340
+ ' Tip: validate a testnet server too for free. This CLI automatically provisions and funds a testnet wallet for testing.',
341
+ ),
342
+ )
343
+ }
344
+
345
+ console.log('')
346
+
347
+ if (counts.failed > 0) process.exit(1)
348
+ }