mppx 0.8.6 → 0.8.8

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 (108) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/cli/internal.js +2 -2
  3. package/dist/cli/internal.js.map +1 -1
  4. package/dist/cli/plugins/evm.d.ts +2 -0
  5. package/dist/cli/plugins/evm.d.ts.map +1 -0
  6. package/dist/cli/plugins/evm.js +51 -0
  7. package/dist/cli/plugins/evm.js.map +1 -0
  8. package/dist/cli/plugins/index.d.ts +1 -0
  9. package/dist/cli/plugins/index.d.ts.map +1 -1
  10. package/dist/cli/plugins/index.js +1 -0
  11. package/dist/cli/plugins/index.js.map +1 -1
  12. package/dist/cli/plugins/stripe.d.ts.map +1 -1
  13. package/dist/cli/plugins/stripe.js +3 -2
  14. package/dist/cli/plugins/stripe.js.map +1 -1
  15. package/dist/cli/validate/challenge.d.ts +2 -0
  16. package/dist/cli/validate/challenge.d.ts.map +1 -1
  17. package/dist/cli/validate/challenge.js +191 -84
  18. package/dist/cli/validate/challenge.js.map +1 -1
  19. package/dist/cli/validate/discovery.d.ts +1 -0
  20. package/dist/cli/validate/discovery.d.ts.map +1 -1
  21. package/dist/cli/validate/discovery.js +99 -35
  22. package/dist/cli/validate/discovery.js.map +1 -1
  23. package/dist/cli/validate/helpers.d.ts +14 -1
  24. package/dist/cli/validate/helpers.d.ts.map +1 -1
  25. package/dist/cli/validate/helpers.js +0 -13
  26. package/dist/cli/validate/helpers.js.map +1 -1
  27. package/dist/cli/validate/index.d.ts +1 -0
  28. package/dist/cli/validate/index.d.ts.map +1 -1
  29. package/dist/cli/validate/index.js +102 -153
  30. package/dist/cli/validate/index.js.map +1 -1
  31. package/dist/cli/validate/payment.d.ts +4 -1
  32. package/dist/cli/validate/payment.d.ts.map +1 -1
  33. package/dist/cli/validate/payment.js +312 -106
  34. package/dist/cli/validate/payment.js.map +1 -1
  35. package/dist/evm/Chains.d.ts +4 -0
  36. package/dist/evm/Chains.d.ts.map +1 -1
  37. package/dist/evm/Chains.js +4 -0
  38. package/dist/evm/Chains.js.map +1 -1
  39. package/dist/internal/AcceptPayment.d.ts.map +1 -1
  40. package/dist/internal/AcceptPayment.js +7 -1
  41. package/dist/internal/AcceptPayment.js.map +1 -1
  42. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  43. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  44. package/dist/stripe/server/internal/html.gen.js +1 -1
  45. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  46. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  47. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  48. package/dist/tempo/server/internal/html.gen.js +1 -1
  49. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  50. package/dist/tempo/server/internal/request-body.d.ts.map +1 -1
  51. package/dist/tempo/server/internal/request-body.js +2 -0
  52. package/dist/tempo/server/internal/request-body.js.map +1 -1
  53. package/dist/tempo/session/server/ChannelStore.js +4 -4
  54. package/dist/tempo/session/server/ChannelStore.js.map +1 -1
  55. package/dist/tempo/session/server/CredentialVerification.d.ts +8 -0
  56. package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
  57. package/dist/tempo/session/server/CredentialVerification.js +21 -1
  58. package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
  59. package/dist/tempo/session/server/Session.d.ts.map +1 -1
  60. package/dist/tempo/session/server/Session.js +1 -0
  61. package/dist/tempo/session/server/Session.js.map +1 -1
  62. package/dist/validation/core.d.ts +75 -0
  63. package/dist/validation/core.d.ts.map +1 -0
  64. package/dist/validation/core.js +269 -0
  65. package/dist/validation/core.js.map +1 -0
  66. package/dist/validation/index.d.ts +3 -0
  67. package/dist/validation/index.d.ts.map +1 -0
  68. package/dist/validation/index.js +2 -0
  69. package/dist/validation/index.js.map +1 -0
  70. package/dist/x402/Assets.d.ts +9 -0
  71. package/dist/x402/Assets.d.ts.map +1 -1
  72. package/dist/x402/Assets.js +38 -0
  73. package/dist/x402/Assets.js.map +1 -1
  74. package/package.json +9 -4
  75. package/src/cli/internal.ts +2 -2
  76. package/src/cli/plugins/evm.ts +55 -0
  77. package/src/cli/plugins/index.ts +1 -0
  78. package/src/cli/plugins/stripe.ts +3 -2
  79. package/src/cli/validate/challenge.ts +345 -127
  80. package/src/cli/validate/discovery.test.ts +307 -0
  81. package/src/cli/validate/discovery.ts +104 -38
  82. package/src/cli/validate/helpers.ts +15 -13
  83. package/src/cli/validate/index.ts +121 -242
  84. package/src/cli/validate/payment.ts +413 -144
  85. package/src/cli/validate.test.ts +237 -3
  86. package/src/client/internal/Fetch.test.ts +142 -2
  87. package/src/evm/Chains.ts +6 -0
  88. package/src/evm/PublicInterface.test-d.ts +7 -0
  89. package/src/internal/AcceptPayment.test.ts +45 -0
  90. package/src/internal/AcceptPayment.ts +11 -1
  91. package/src/stripe/server/internal/html/package.json +1 -1
  92. package/src/stripe/server/internal/html.gen.ts +1 -1
  93. package/src/tempo/server/internal/html/package.json +1 -1
  94. package/src/tempo/server/internal/html.gen.ts +1 -1
  95. package/src/tempo/server/internal/request-body.test.ts +9 -0
  96. package/src/tempo/server/internal/request-body.ts +2 -0
  97. package/src/tempo/session/server/ChannelStore.test.ts +17 -0
  98. package/src/tempo/session/server/ChannelStore.ts +4 -4
  99. package/src/tempo/session/server/CredentialVerification.test.ts +118 -1
  100. package/src/tempo/session/server/CredentialVerification.ts +30 -0
  101. package/src/tempo/session/server/Session.integration.test.ts +5 -0
  102. package/src/tempo/session/server/Session.test.ts +119 -53
  103. package/src/tempo/session/server/Session.ts +1 -0
  104. package/src/validation/core.ts +374 -0
  105. package/src/validation/index.ts +11 -0
  106. package/src/x402/Assets.test.ts +42 -0
  107. package/src/x402/Assets.ts +40 -0
  108. package/src/x402/Exact.localnet.test.ts +475 -0
@@ -1,25 +1,9 @@
1
1
  import { Cli, z } from 'incur'
2
2
 
3
- import { validate as validateDiscovery } from '../../discovery/Validate.js'
3
+ import { validate as validateCore, validateStream } from '../../validation/core.js'
4
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'
5
+ import type { Counts } from './helpers.js'
6
+ import { printResults, printSection } from './helpers.js'
23
7
 
24
8
  const validate = Cli.create('validate', {
25
9
  description: 'Validate an MPP server implementation end-to-end',
@@ -37,260 +21,157 @@ const validate = Cli.create('validate', {
37
21
  query: z.array(z.string()).optional().describe('Query parameter (key=value, repeatable)'),
38
22
  verbose: z.number().default(0).meta({ count: true }).describe('Verbosity level'),
39
23
  yes: z.boolean().default(false).describe('Auto-approve mainnet payments'),
24
+ outputJson: z.boolean().default(false).describe('Output results as JSON'),
40
25
  }),
41
26
  alias: {
42
27
  endpoint: 'e',
43
28
  verbose: 'v',
44
29
  yes: 'y',
30
+ outputJson: 'j',
45
31
  },
46
32
  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 }
33
+ // JSON mode: batch everything
34
+ if (c.options.outputJson) {
35
+ const result = await validateCore({
36
+ url: c.args.url,
37
+ endpoint: c.options.endpoint,
38
+ body: c.options.body,
39
+ query: c.options.query,
40
+ verbose: c.options.verbose > 0,
41
+ yes: c.options.yes,
42
+ interactive: false,
43
+ })
44
+ console.log(JSON.stringify(result, null, 2))
45
+ const noEndpoints = result.endpoints.length === 0 && !c.options.endpoint
46
+ if (result.summary.failed > 0 || !result.discovery.found || noEndpoints) process.exit(1)
47
+ return
48
+ }
50
49
 
50
+ // Streaming human-readable output
51
+ const baseUrl = c.args.url.replace(/\/$/, '').replace(/\/openapi\.json$/i, '')
51
52
  console.log(`\n${pc.bold('mppx validate')} ${pc.dim(baseUrl)}\n`)
52
53
 
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,
54
+ const counts: Counts = { passed: 0, failed: 0, warnings: 0, skipped: 0 }
55
+ let sawMppEndpoint = false
56
+ let sawNonMppPaymentEndpoint = false
57
+ let sawMalformedChallenge = false
58
+ let sawTestnet = false
59
+ let sawMainnet = false
60
+ let paymentSucceeded = false
61
+ let discoveryFound = false
62
+ let endpointCount = 0
63
+
64
+ for await (const event of validateStream({
65
+ url: c.args.url,
61
66
  endpoint: c.options.endpoint,
62
67
  body: c.options.body,
63
68
  query: c.options.query,
69
+ verbose: c.options.verbose > 0,
64
70
  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}`))
71
+ interactive: !!process.stdin.isTTY,
72
+ onPaymentResults: (results) => printResults(results, counts),
73
+ })) {
74
+ switch (event.phase) {
75
+ case 'discovery':
76
+ printSection('Discovery (/openapi.json)')
77
+ printResults(event.results, counts)
78
+ discoveryFound = event.discovery.found
79
+ if (!discoveryFound && !c.options.endpoint) {
80
+ console.log('')
81
+ console.log(pc.yellow(' No discovery document found.'))
82
+ console.log(
83
+ pc.dim(
84
+ ' MPP servers must serve an OpenAPI document at /openapi.json with x-payment-info extensions.',
85
+ ),
86
+ )
87
+ console.log(
88
+ pc.dim(
89
+ ' To test a specific endpoint: mppx validate <url> --endpoint POST:/your/path',
90
+ ),
91
+ )
92
+ console.log('')
93
+ process.exit(1)
94
+ }
95
+ if (event.discovery.endpoints.length === 0 && !c.options.endpoint && discoveryFound) {
96
+ console.log(pc.dim(' Use --endpoint to specify endpoints manually.'))
97
+ console.log('')
98
+ process.exit(1)
99
+ }
100
+ break
101
+ case 'endpoint':
102
+ endpointCount++
103
+ printSection(`${event.endpoint.method} ${event.endpoint.path}`)
104
+ break
105
+ case 'challenge':
106
+ console.log(pc.dim(' Challenge'))
107
+ printResults(event.results, counts)
108
+ if (event.isMpp) {
109
+ sawMppEndpoint = true
110
+ if (event.isTestnet) sawTestnet = true
111
+ if (event.isMainnet) sawMainnet = true
112
+ }
113
+ if (event.isNonMppPayment) sawNonMppPaymentEndpoint = true
114
+ if (event.isMalformedChallenge) sawMalformedChallenge = true
115
+ break
116
+ case 'errorHandling':
117
+ console.log(pc.dim(' Error Handling'))
118
+ printResults(event.results, counts)
119
+ break
120
+ case 'payment':
121
+ if (event.succeeded) paymentSucceeded = true
122
+ break
131
123
  }
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
124
  }
156
- endpoints.push(parsed)
157
- } else if (discoveryDoc) {
158
- endpoints = extractEndpointsFromDiscovery(discoveryDoc)
159
125
 
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,
126
+ // Summary
127
+ printSummary(
128
+ counts,
229
129
  {
230
- body,
231
- query: options.query,
232
- discoveryDoc: discoveryDoc ?? undefined,
130
+ sawMppEndpoint,
131
+ sawNonMppPaymentEndpoint,
132
+ sawMalformedChallenge,
133
+ sawTestnet,
134
+ sawMainnet,
135
+ paymentSucceeded,
233
136
  },
137
+ endpointCount,
234
138
  )
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
- }
139
+ },
140
+ })
275
141
 
276
- return { sawMppEndpoint, sawNonMppPaymentEndpoint, sawTestnet, sawMainnet, paymentSucceeded }
277
- }
142
+ export default validate
278
143
 
279
144
  function printSummary(
280
145
  counts: Counts,
281
146
  flags: {
282
147
  sawMppEndpoint: boolean
283
148
  sawNonMppPaymentEndpoint: boolean
149
+ sawMalformedChallenge: boolean
284
150
  sawTestnet: boolean
285
151
  sawMainnet: boolean
286
152
  paymentSucceeded: boolean
287
153
  },
288
154
  endpointsLength: number,
289
155
  ): void {
290
- // No MPP endpoints found
291
156
  if (!flags.sawMppEndpoint && endpointsLength > 0) {
292
157
  console.log('')
293
- if (flags.sawNonMppPaymentEndpoint) {
158
+ if (flags.sawMalformedChallenge) {
159
+ console.log(
160
+ pc.yellow(
161
+ ` Payment scheme detected but challenge format is invalid on ${endpointsLength} endpoint(s).`,
162
+ ),
163
+ )
164
+ console.log(
165
+ pc.dim(
166
+ ' The server uses WWW-Authenticate: Payment but the challenge parameters do not conform to MPP.',
167
+ ),
168
+ )
169
+ console.log(
170
+ pc.dim(
171
+ ' Fix: encode payment details as base64url JSON in a request="..." parameter. See errors above.',
172
+ ),
173
+ )
174
+ } else if (flags.sawNonMppPaymentEndpoint) {
294
175
  console.log(
295
176
  pc.yellow(
296
177
  ` No MPP endpoints found. Tested ${endpointsLength} endpoint(s) but none use WWW-Authenticate: Payment.`,
@@ -318,7 +199,6 @@ function printSummary(
318
199
  process.exit(1)
319
200
  }
320
201
 
321
- // Summary
322
202
  console.log('')
323
203
  const parts: string[] = []
324
204
  if (counts.passed > 0) parts.push(pc.green(`${counts.passed} passed`))
@@ -327,17 +207,16 @@ function printSummary(
327
207
  if (counts.skipped > 0) parts.push(pc.yellow(`${counts.skipped} skipped`))
328
208
  console.log(`${pc.bold('Summary:')} ${parts.join(', ')}`)
329
209
 
330
- // Cross-promotion
331
210
  if (flags.paymentSucceeded && flags.sawTestnet && !flags.sawMainnet) {
332
211
  console.log('')
333
212
  console.log(
334
- pc.dim(' Tip: validate your mainnet server too to confirm real payments work end-to-end.'),
213
+ pc.dim(' Tip: also validate your mainnet server to confirm real payments work end-to-end.'),
335
214
  )
336
215
  } else if (flags.sawMainnet && !flags.sawTestnet) {
337
216
  console.log('')
338
217
  console.log(
339
218
  pc.dim(
340
- ' Tip: validate a testnet server too for free. This CLI automatically provisions and funds a testnet wallet for testing.',
219
+ ' Tip: also validate your testnet server for free. This CLI automatically provisions and funds a testnet wallet for testing.',
341
220
  ),
342
221
  )
343
222
  }