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.
- package/CHANGELOG.md +8 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +2 -0
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/plugins/stripe.d.ts.map +1 -1
- package/dist/cli/plugins/stripe.js +32 -12
- package/dist/cli/plugins/stripe.js.map +1 -1
- package/dist/cli/utils.d.ts.map +1 -1
- package/dist/cli/utils.js +1 -1
- package/dist/cli/utils.js.map +1 -1
- package/dist/cli/validate/challenge.d.ts +14 -0
- package/dist/cli/validate/challenge.d.ts.map +1 -0
- package/dist/cli/validate/challenge.js +211 -0
- package/dist/cli/validate/challenge.js.map +1 -0
- package/dist/cli/validate/discovery.d.ts +10 -0
- package/dist/cli/validate/discovery.d.ts.map +1 -0
- package/dist/cli/validate/discovery.js +128 -0
- package/dist/cli/validate/discovery.js.map +1 -0
- package/dist/cli/validate/helpers.d.ts +33 -0
- package/dist/cli/validate/helpers.d.ts.map +1 -0
- package/dist/cli/validate/helpers.js +136 -0
- package/dist/cli/validate/helpers.js.map +1 -0
- package/dist/cli/validate/index.d.ts +17 -0
- package/dist/cli/validate/index.d.ts.map +1 -0
- package/dist/cli/validate/index.js +224 -0
- package/dist/cli/validate/index.js.map +1 -0
- package/dist/cli/validate/payment.d.ts +7 -0
- package/dist/cli/validate/payment.d.ts.map +1 -0
- package/dist/cli/validate/payment.js +381 -0
- package/dist/cli/validate/payment.js.map +1 -0
- package/dist/tempo/internal/auto-swap.d.ts.map +1 -1
- package/dist/tempo/internal/auto-swap.js +6 -1
- package/dist/tempo/internal/auto-swap.js.map +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/html.gen.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/cli.test.ts +134 -29
- package/src/cli/cli.ts +2 -0
- package/src/cli/mcp.test.ts +1 -0
- package/src/cli/plugins/stripe.ts +31 -12
- package/src/cli/utils.test.ts +36 -2
- package/src/cli/utils.ts +2 -1
- package/src/cli/validate/challenge.ts +335 -0
- package/src/cli/validate/discovery.ts +120 -0
- package/src/cli/validate/helpers.ts +154 -0
- package/src/cli/validate/index.ts +348 -0
- package/src/cli/validate/payment.ts +488 -0
- package/src/cli/validate.test.ts +539 -0
- package/src/tempo/internal/auto-swap.test.ts +26 -9
- package/src/tempo/internal/auto-swap.ts +15 -2
- 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
|
+
}
|