mppx 0.8.6 → 0.8.7

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