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.
- package/CHANGELOG.md +6 -0
- package/dist/cli/plugins/stripe.d.ts.map +1 -1
- package/dist/cli/plugins/stripe.js +3 -2
- package/dist/cli/plugins/stripe.js.map +1 -1
- package/dist/cli/validate/challenge.d.ts.map +1 -1
- package/dist/cli/validate/challenge.js +10 -9
- package/dist/cli/validate/challenge.js.map +1 -1
- package/dist/cli/validate/index.d.ts.map +1 -1
- package/dist/cli/validate/index.js +5 -3
- package/dist/cli/validate/index.js.map +1 -1
- package/dist/cli/validate/payment.d.ts +3 -1
- package/dist/cli/validate/payment.d.ts.map +1 -1
- package/dist/cli/validate/payment.js +281 -146
- package/dist/cli/validate/payment.js.map +1 -1
- package/dist/validation/core.d.ts +4 -8
- package/dist/validation/core.d.ts.map +1 -1
- package/dist/validation/core.js +37 -24
- package/dist/validation/core.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/plugins/stripe.ts +3 -2
- package/src/cli/validate/challenge.ts +13 -8
- package/src/cli/validate/index.ts +5 -3
- package/src/cli/validate/payment.ts +354 -168
- package/src/cli/validate.test.ts +94 -0
- package/src/validation/core.ts +67 -41
package/src/cli/validate.test.ts
CHANGED
|
@@ -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
|
+
})
|
package/src/validation/core.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
120
|
-
(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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> {
|