mppx 0.8.7 → 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.
@@ -677,3 +677,97 @@ describe('validate: multi-challenge', () => {
677
677
  expect(output).toContain('Amount is valid integer string (Got: 1.50)')
678
678
  })
679
679
  })
680
+
681
+ describe('validate: payment methods coverage', () => {
682
+ test(
683
+ 'every method in Constants.Methods produces a payment result',
684
+ { timeout: 15_000 },
685
+ async () => {
686
+ const methods = Object.values(Constants.Methods)
687
+ const challenges = methods.map((method) => ({
688
+ id: `${method}-id`,
689
+ realm: 'localhost',
690
+ method,
691
+ intent: 'charge',
692
+ request: { amount: '100', currency: 'usd', methodDetails: {} },
693
+ expires: new Date(Date.now() + 300_000).toISOString(),
694
+ })) as Challenge.Challenge[]
695
+
696
+ const header = challenges.map((c) => Challenge.serialize(c)).join(', ')
697
+ const server = await testServer((req, res) => {
698
+ const url = new URL(req.url!, 'http://localhost')
699
+ if (url.pathname === '/openapi.json') {
700
+ res.setHeader('Content-Type', 'application/json')
701
+ res.end(
702
+ JSON.stringify({
703
+ openapi: '3.1.0',
704
+ info: { title: 'T', version: '1' },
705
+ paths: {
706
+ '/api/test': {
707
+ post: { 'x-payment-info': { amount: '100' }, responses: { '402': {} } },
708
+ },
709
+ },
710
+ }),
711
+ )
712
+ return
713
+ }
714
+ res.writeHead(402, { [Constants.Headers.wwwAuthenticate]: header })
715
+ res.end()
716
+ })
717
+ const { output } = await serve(['validate', server.url, '--outputJson', '--yes'])
718
+ const jsonStart = output.indexOf('{')
719
+ const jsonEnd = output.lastIndexOf('}')
720
+ const result = JSON.parse(output.slice(jsonStart, jsonEnd + 1))
721
+ const paymentLabels = result.endpoints[0].payment.map((r: { label: string }) => r.label)
722
+ for (const method of methods) {
723
+ expect(paymentLabels.some((l: string) => l.includes(`[${method}]`))).toBe(true)
724
+ }
725
+ },
726
+ )
727
+ })
728
+
729
+ describe('validate: JSON mode', () => {
730
+ function mainnetChallenge() {
731
+ return makeChallenge({
732
+ request: {
733
+ amount: '10000',
734
+ currency: '0x20c0000000000000000000000000000000000000',
735
+ recipient: '0x1234567890123456789012345678901234567890',
736
+ methodDetails: { chainId: 4217 },
737
+ },
738
+ } as Partial<Challenge.Challenge>)
739
+ }
740
+
741
+ function parseJson(output: string) {
742
+ const jsonStart = output.indexOf('{')
743
+ const jsonEnd = output.lastIndexOf('}')
744
+ return JSON.parse(output.slice(jsonStart, jsonEnd + 1))
745
+ }
746
+
747
+ test('does not prompt (non-interactive)', { timeout: 15_000 }, async () => {
748
+ const server = await mppServer(mainnetChallenge())
749
+ const { output } = await serve(['validate', server.url, '--outputJson'])
750
+ const result = parseJson(output)
751
+ const allPayment = result.endpoints.flatMap((ep: { payment: unknown[] }) => ep.payment)
752
+ expect(allPayment.length).toBeGreaterThan(0)
753
+ expect(allPayment.every((r: { severity: string }) => r.severity === 'skip')).toBe(true)
754
+ })
755
+
756
+ test('no console leaks before JSON', { timeout: 15_000 }, async () => {
757
+ const server = await mppServer(mainnetChallenge())
758
+ const { output } = await serve(['validate', server.url, '--outputJson', '--yes'])
759
+ const jsonStart = output.indexOf('{')
760
+ expect(jsonStart).toBeGreaterThanOrEqual(0)
761
+ const beforeJson = output.slice(0, jsonStart)
762
+ expect(beforeJson).not.toContain('Using wallet')
763
+ expect(beforeJson).not.toContain('Auto-approved')
764
+ expect(beforeJson).not.toContain('Attempting')
765
+ })
766
+
767
+ test('includes suggestions', { timeout: 15_000 }, async () => {
768
+ const server = await mppServer(mainnetChallenge())
769
+ const { output } = await serve(['validate', server.url, '--outputJson', '--yes'])
770
+ const result = parseJson(output)
771
+ expect(Array.isArray(result.suggestions)).toBe(true)
772
+ })
773
+ })
@@ -1,3 +1,6 @@
1
+ import type { Chain } from 'viem'
2
+ import * as viemChains from 'viem/chains'
3
+
1
4
  import { validateChallenge, validateErrorHandling } from '../cli/validate/challenge.js'
2
5
  import {
3
6
  buildUrl,
@@ -25,6 +28,7 @@ export type ValidateOptions = {
25
28
  verbose?: boolean | undefined
26
29
  yes?: boolean | undefined
27
30
  skipPayment?: boolean | undefined
31
+ interactive?: boolean | undefined
28
32
  discoveryPath?: string | undefined
29
33
  onPaymentResults?: (results: CheckResult[]) => void
30
34
  }
@@ -50,14 +54,7 @@ export type ValidateResult = {
50
54
  discovery: DiscoveryResult
51
55
  endpoints: EndpointValidationResult[]
52
56
  summary: { passed: number; failed: number; warnings: number; skipped: number }
53
- flags: {
54
- sawMppEndpoint: boolean
55
- sawNonMppPaymentEndpoint: boolean
56
- sawMalformedChallenge: boolean
57
- sawTestnet: boolean
58
- sawMainnet: boolean
59
- paymentSucceeded: boolean
60
- }
57
+ suggestions: string[]
61
58
  }
62
59
 
63
60
  export type ValidateEvent =
@@ -69,8 +66,10 @@ export type ValidateEvent =
69
66
  results: CheckResult[]
70
67
  isMpp: boolean
71
68
  isTestnet: boolean
69
+ isMainnet: boolean
72
70
  isNonMppPayment: boolean
73
71
  isMalformedChallenge: boolean
72
+ methods: string[]
74
73
  }
75
74
  | { phase: 'errorHandling'; endpoint: EndpointSpec; results: CheckResult[] }
76
75
  | {
@@ -102,26 +101,25 @@ export async function* validateStream(options: ValidateOptions): AsyncGenerator<
102
101
  }
103
102
  }
104
103
 
105
- const { results: challengeResults, resolvedBody } = await validateChallenge(
106
- baseUrl,
107
- endpoint,
108
- verbose,
109
- {
110
- body,
111
- query: options.query,
112
- discoveryDoc: discovery.doc ?? undefined,
113
- },
114
- )
104
+ const {
105
+ results: challengeResults,
106
+ resolvedBody,
107
+ challenges: parsedChallenges,
108
+ } = await validateChallenge(baseUrl, endpoint, verbose, {
109
+ body,
110
+ query: options.query,
111
+ discoveryDoc: discovery.doc ?? undefined,
112
+ })
115
113
  const effectiveBody = resolvedBody ?? body
116
114
  const isMpp = challengeResults.some(
117
115
  (r) => r.severity === 'pass' && r.label === 'Challenge parseable',
118
116
  )
119
- const isTestnet = challengeResults.some(
120
- (r) =>
121
- r.severity === 'pass' &&
122
- r.label.endsWith('Valid currency address') &&
123
- r.detail === 'testnet',
124
- )
117
+ const chainIds = (parsedChallenges ?? [])
118
+ .filter((ch) => ch.method !== 'stripe')
119
+ .map((ch) => ((ch.request as Record<string, unknown>).methodDetails as any)?.chainId)
120
+ .filter((id): id is number => typeof id === 'number')
121
+ const isTestnet = chainIds.some(chainIsTestnet)
122
+ const isMainnet = chainIds.some((id) => !chainIsTestnet(id))
125
123
  const isNonMppPayment =
126
124
  !isMpp && challengeResults.some((r) => r.label === 'Not an MPP endpoint')
127
125
  const hasPaymentScheme = challengeResults.some(
@@ -132,14 +130,18 @@ export async function* validateStream(options: ValidateOptions): AsyncGenerator<
132
130
  )
133
131
  const isMalformedChallenge = !isMpp && hasPaymentScheme && challengeFailed
134
132
 
133
+ const methods = parsedChallenges?.map((c) => c.method) ?? []
134
+
135
135
  yield {
136
136
  phase: 'challenge',
137
137
  endpoint,
138
138
  results: challengeResults,
139
139
  isMpp,
140
140
  isTestnet,
141
+ isMainnet,
141
142
  isNonMppPayment,
142
143
  isMalformedChallenge,
144
+ methods,
143
145
  }
144
146
 
145
147
  if (isMpp) {
@@ -155,6 +157,8 @@ export async function* validateStream(options: ValidateOptions): AsyncGenerator<
155
157
  body: effectiveBody,
156
158
  query: options.query,
157
159
  yes: options.yes,
160
+ silent: !onResults,
161
+ interactive: options.interactive,
158
162
  ...(onResults && { onResults }),
159
163
  })
160
164
  const succeeded = paymentResults.some(
@@ -175,10 +179,6 @@ export async function validate(options: ValidateOptions): Promise<ValidateResult
175
179
 
176
180
  let sawTestnet = false
177
181
  let sawMainnet = false
178
- let paymentSucceeded = false
179
- let sawMppEndpoint = false
180
- let sawNonMppPaymentEndpoint = false
181
- let sawMalformedChallenge = false
182
182
 
183
183
  for await (const event of validateStream(options)) {
184
184
  switch (event.phase) {
@@ -198,19 +198,15 @@ export async function validate(options: ValidateOptions): Promise<ValidateResult
198
198
  case 'challenge':
199
199
  if (current) current.challenge = event.results
200
200
  if (event.isMpp) {
201
- sawMppEndpoint = true
202
201
  if (event.isTestnet) sawTestnet = true
203
- else sawMainnet = true
202
+ if (event.isMainnet) sawMainnet = true
204
203
  }
205
- if (event.isNonMppPayment) sawNonMppPaymentEndpoint = true
206
- if (event.isMalformedChallenge) sawMalformedChallenge = true
207
204
  break
208
205
  case 'errorHandling':
209
206
  if (current) current.errorHandling = event.results
210
207
  break
211
208
  case 'payment':
212
209
  if (current) current.payment = event.results
213
- if (event.succeeded) paymentSucceeded = true
214
210
  break
215
211
  }
216
212
  }
@@ -235,20 +231,50 @@ export async function validate(options: ValidateOptions): Promise<ValidateResult
235
231
 
236
232
  if (!discovery) throw new Error('Discovery phase did not complete')
237
233
 
234
+ const suggestions = computeSuggestions(endpointResults, { sawTestnet, sawMainnet })
235
+
238
236
  return {
239
237
  url: baseUrl,
240
238
  discovery,
241
239
  endpoints: endpointResults,
242
240
  summary,
243
- flags: {
244
- sawMppEndpoint,
245
- sawNonMppPaymentEndpoint,
246
- sawMalformedChallenge,
247
- sawTestnet,
248
- sawMainnet,
249
- paymentSucceeded,
250
- },
241
+ suggestions,
242
+ }
243
+ }
244
+
245
+ function computeSuggestions(
246
+ endpoints: EndpointValidationResult[],
247
+ flags: { sawTestnet: boolean; sawMainnet: boolean },
248
+ ): string[] {
249
+ const steps: string[] = []
250
+ const allResults = endpoints.flatMap((ep) => [...ep.challenge, ...ep.payment])
251
+
252
+ const sawCryptoMainnet = flags.sawMainnet
253
+ const sawCryptoTestnet = flags.sawTestnet
254
+ const sawStripeLive = allResults.some(
255
+ (r) => r.label.includes('[stripe]') && r.label.includes('livemode'),
256
+ )
257
+
258
+ if (sawCryptoMainnet && !sawCryptoTestnet) {
259
+ steps.push(
260
+ 'Validate your testnet server to test without real funds (Tempo wallets are automatically provisioned).',
261
+ )
262
+ } else if (sawCryptoTestnet && !sawCryptoMainnet) {
263
+ steps.push('Validate your mainnet server to confirm real payments work end-to-end.')
251
264
  }
265
+ if (sawStripeLive) {
266
+ steps.push(
267
+ 'Validate your testmode Stripe server to test card payments end-to-end (auto-pays with test card).',
268
+ )
269
+ }
270
+
271
+ return steps
272
+ }
273
+
274
+ const allChains = Object.values(viemChains) as Chain[]
275
+
276
+ function chainIsTestnet(chainId: number): boolean {
277
+ return allChains.find((c) => c.id === chainId)?.testnet === true
252
278
  }
253
279
 
254
280
  async function runDiscovery(baseUrl: string, options: ValidateOptions): Promise<DiscoveryResult> {