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.
- package/CHANGELOG.md +13 -0
- package/dist/cli/internal.js +2 -2
- package/dist/cli/internal.js.map +1 -1
- package/dist/cli/plugins/evm.d.ts +2 -0
- package/dist/cli/plugins/evm.d.ts.map +1 -0
- package/dist/cli/plugins/evm.js +51 -0
- package/dist/cli/plugins/evm.js.map +1 -0
- package/dist/cli/plugins/index.d.ts +1 -0
- package/dist/cli/plugins/index.d.ts.map +1 -1
- package/dist/cli/plugins/index.js +1 -0
- package/dist/cli/plugins/index.js.map +1 -1
- package/dist/cli/validate/challenge.d.ts +2 -0
- package/dist/cli/validate/challenge.d.ts.map +1 -1
- package/dist/cli/validate/challenge.js +184 -78
- package/dist/cli/validate/challenge.js.map +1 -1
- package/dist/cli/validate/discovery.d.ts +1 -0
- package/dist/cli/validate/discovery.d.ts.map +1 -1
- package/dist/cli/validate/discovery.js +99 -35
- package/dist/cli/validate/discovery.js.map +1 -1
- package/dist/cli/validate/helpers.d.ts +14 -1
- package/dist/cli/validate/helpers.d.ts.map +1 -1
- package/dist/cli/validate/helpers.js +0 -13
- package/dist/cli/validate/helpers.js.map +1 -1
- package/dist/cli/validate/index.d.ts +1 -0
- package/dist/cli/validate/index.d.ts.map +1 -1
- package/dist/cli/validate/index.js +98 -151
- package/dist/cli/validate/index.js.map +1 -1
- package/dist/cli/validate/payment.d.ts +1 -0
- package/dist/cli/validate/payment.d.ts.map +1 -1
- package/dist/cli/validate/payment.js +215 -144
- package/dist/cli/validate/payment.js.map +1 -1
- package/dist/evm/Chains.d.ts +4 -0
- package/dist/evm/Chains.d.ts.map +1 -1
- package/dist/evm/Chains.js +4 -0
- package/dist/evm/Chains.js.map +1 -1
- package/dist/internal/AcceptPayment.d.ts.map +1 -1
- package/dist/internal/AcceptPayment.js +7 -1
- package/dist/internal/AcceptPayment.js.map +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/stripe/server/internal/html.gen.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/dist/tempo/server/internal/request-body.d.ts.map +1 -1
- package/dist/tempo/server/internal/request-body.js +2 -0
- package/dist/tempo/server/internal/request-body.js.map +1 -1
- package/dist/tempo/session/server/ChannelStore.js +4 -4
- package/dist/tempo/session/server/ChannelStore.js.map +1 -1
- package/dist/tempo/session/server/CredentialVerification.d.ts +8 -0
- package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
- package/dist/tempo/session/server/CredentialVerification.js +21 -1
- package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
- package/dist/tempo/session/server/Session.d.ts.map +1 -1
- package/dist/tempo/session/server/Session.js +1 -0
- package/dist/tempo/session/server/Session.js.map +1 -1
- package/dist/validation/core.d.ts +79 -0
- package/dist/validation/core.d.ts.map +1 -0
- package/dist/validation/core.js +256 -0
- package/dist/validation/core.js.map +1 -0
- package/dist/validation/index.d.ts +3 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +2 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/x402/Assets.d.ts +9 -0
- package/dist/x402/Assets.d.ts.map +1 -1
- package/dist/x402/Assets.js +38 -0
- package/dist/x402/Assets.js.map +1 -1
- package/package.json +9 -4
- package/src/cli/internal.ts +2 -2
- package/src/cli/plugins/evm.ts +55 -0
- package/src/cli/plugins/index.ts +1 -0
- package/src/cli/validate/challenge.ts +334 -121
- package/src/cli/validate/discovery.test.ts +307 -0
- package/src/cli/validate/discovery.ts +104 -38
- package/src/cli/validate/helpers.ts +15 -13
- package/src/cli/validate/index.ts +117 -240
- package/src/cli/validate/payment.ts +261 -178
- package/src/cli/validate.test.ts +143 -3
- package/src/client/internal/Fetch.test.ts +142 -2
- package/src/evm/Chains.ts +6 -0
- package/src/evm/PublicInterface.test-d.ts +7 -0
- package/src/internal/AcceptPayment.test.ts +45 -0
- package/src/internal/AcceptPayment.ts +11 -1
- package/src/stripe/server/internal/html/package.json +1 -1
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/server/internal/html/package.json +1 -1
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/server/internal/request-body.test.ts +9 -0
- package/src/tempo/server/internal/request-body.ts +2 -0
- package/src/tempo/session/server/ChannelStore.test.ts +17 -0
- package/src/tempo/session/server/ChannelStore.ts +4 -4
- package/src/tempo/session/server/CredentialVerification.test.ts +118 -1
- package/src/tempo/session/server/CredentialVerification.ts +30 -0
- package/src/tempo/session/server/Session.integration.test.ts +5 -0
- package/src/tempo/session/server/Session.test.ts +119 -53
- package/src/tempo/session/server/Session.ts +1 -0
- package/src/validation/core.ts +348 -0
- package/src/validation/index.ts +11 -0
- package/src/x402/Assets.test.ts +42 -0
- package/src/x402/Assets.ts +40 -0
- package/src/x402/Exact.localnet.test.ts +475 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as Challenge from '../../Challenge.js'
|
|
2
2
|
import * as Constants from '../../Constants.js'
|
|
3
3
|
import { chainId as tempoChainIds } from '../../tempo/internal/defaults.js'
|
|
4
|
+
import * as x402Header from '../../x402/Header.js'
|
|
4
5
|
import { pc } from '../utils.js'
|
|
5
|
-
import { extractRequestBodyFromDiscovery } from './discovery.js'
|
|
6
|
+
import { buildUrl, extractRequestBodyFromDiscovery } from './discovery.js'
|
|
6
7
|
import type { CheckResult, EndpointSpec } from './helpers.js'
|
|
7
8
|
import {
|
|
8
|
-
buildUrl,
|
|
9
9
|
check,
|
|
10
10
|
fail,
|
|
11
11
|
fetchWithTimeout,
|
|
@@ -33,7 +33,11 @@ export async function validateChallenge(
|
|
|
33
33
|
query?: string[] | undefined
|
|
34
34
|
discoveryDoc?: Record<string, unknown> | undefined
|
|
35
35
|
},
|
|
36
|
-
): Promise<{
|
|
36
|
+
): Promise<{
|
|
37
|
+
results: CheckResult[]
|
|
38
|
+
resolvedBody?: string | undefined
|
|
39
|
+
challenges?: Challenge.Challenge[] | undefined
|
|
40
|
+
}> {
|
|
37
41
|
const results: CheckResult[] = []
|
|
38
42
|
const url = buildUrl(baseUrl, endpoint, options?.query)
|
|
39
43
|
const fetchHeaders: Record<string, string> = {}
|
|
@@ -106,9 +110,17 @@ export async function validateChallenge(
|
|
|
106
110
|
// Check WWW-Authenticate header
|
|
107
111
|
const wwwAuth = response.headers.get(Constants.Headers.wwwAuthenticate)
|
|
108
112
|
if (!wwwAuth) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
const x402Results = checkX402Headers(response)
|
|
114
|
+
if (x402Results.length > 0) {
|
|
115
|
+
results.push(
|
|
116
|
+
skip('Not an MPP endpoint', 'No WWW-Authenticate header — x402 protocol detected'),
|
|
117
|
+
)
|
|
118
|
+
results.push(...x402Results)
|
|
119
|
+
} else {
|
|
120
|
+
results.push(
|
|
121
|
+
skip('Not an MPP endpoint', 'No WWW-Authenticate header (may be x402 or other protocol)'),
|
|
122
|
+
)
|
|
123
|
+
}
|
|
112
124
|
return { results }
|
|
113
125
|
}
|
|
114
126
|
if (!wwwAuth.startsWith(`${Constants.Schemes.payment} `)) {
|
|
@@ -117,160 +129,331 @@ export async function validateChallenge(
|
|
|
117
129
|
}
|
|
118
130
|
results.push(check('WWW-Authenticate header present', 'Payment scheme'))
|
|
119
131
|
|
|
120
|
-
// Parse
|
|
121
|
-
let
|
|
132
|
+
// Parse all challenges
|
|
133
|
+
let challenges: Challenge.Challenge[]
|
|
122
134
|
try {
|
|
123
|
-
|
|
135
|
+
challenges = Challenge.fromResponseList(response)
|
|
124
136
|
} catch (error) {
|
|
125
|
-
|
|
137
|
+
const msg = (error as Error).message
|
|
138
|
+
results.push(
|
|
139
|
+
fail(
|
|
140
|
+
'Challenge parseable',
|
|
141
|
+
msg,
|
|
142
|
+
'The Payment challenge must include a request="<base64url>" parameter containing JSON-encoded payment details.',
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
const rawParams = wwwAuth.slice(wwwAuth.indexOf(' ') + 1)
|
|
146
|
+
results.push(
|
|
147
|
+
warn('Received', rawParams.length > 200 ? rawParams.slice(0, 200) + '...' : rawParams),
|
|
148
|
+
)
|
|
149
|
+
return { results }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (challenges.length === 0) {
|
|
153
|
+
results.push(fail('Challenge parseable', 'No Payment challenges found in header'))
|
|
126
154
|
return { results }
|
|
127
155
|
}
|
|
128
|
-
results.push(check('Challenge parseable', `${challenge.method}/${challenge.intent}`))
|
|
129
156
|
|
|
130
|
-
|
|
131
|
-
|
|
157
|
+
const methodList = challenges.map((c) => `${c.method}/${c.intent}`).join(', ')
|
|
158
|
+
results.push(
|
|
159
|
+
check(
|
|
160
|
+
'Challenge parseable',
|
|
161
|
+
challenges.length === 1 ? methodList : `${challenges.length} methods: ${methodList}`,
|
|
162
|
+
),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
// Validate common fields across all challenges
|
|
166
|
+
const serverHost = new URL(baseUrl).hostname
|
|
167
|
+
validateIds(challenges, results)
|
|
168
|
+
validateRealms(challenges, results)
|
|
169
|
+
validateExpiration(challenges, results)
|
|
170
|
+
validateRealmMatchesHost(challenges, serverHost, results)
|
|
171
|
+
|
|
172
|
+
// Method-specific validation
|
|
173
|
+
const hasMultipleMethods = challenges.length > 1
|
|
174
|
+
for (const ch of challenges) {
|
|
175
|
+
const request = ch.request as Record<string, unknown>
|
|
176
|
+
validateMethodFields(ch, request, results, hasMultipleMethods)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (verbose) {
|
|
180
|
+
for (const ch of challenges) {
|
|
181
|
+
console.log(
|
|
182
|
+
pc.dim(` Challenge (${ch.method}/${ch.intent}): ${JSON.stringify(ch, null, 2)}`),
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { results, resolvedBody: fetchBody, challenges }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function validateIds(challenges: Challenge.Challenge[], results: CheckResult[]): void {
|
|
191
|
+
const missing = challenges.filter((ch) => !ch.id)
|
|
192
|
+
if (missing.length === 0) {
|
|
193
|
+
results.push(check('Challenge has id'))
|
|
194
|
+
} else {
|
|
132
195
|
results.push(
|
|
133
196
|
fail(
|
|
134
197
|
'Challenge has id',
|
|
135
|
-
|
|
136
|
-
'Every challenge must include a unique id field.
|
|
198
|
+
`${missing.length} missing`,
|
|
199
|
+
'Every challenge must include a unique id field.',
|
|
137
200
|
),
|
|
138
201
|
)
|
|
139
|
-
|
|
202
|
+
}
|
|
203
|
+
}
|
|
140
204
|
|
|
141
|
-
|
|
205
|
+
function validateRealms(challenges: Challenge.Challenge[], results: CheckResult[]): void {
|
|
206
|
+
const missing = challenges.filter((ch) => !ch.realm)
|
|
207
|
+
if (missing.length === 0) {
|
|
208
|
+
results.push(check('Challenge has realm'))
|
|
209
|
+
} else {
|
|
142
210
|
results.push(
|
|
143
211
|
fail(
|
|
144
212
|
'Challenge has realm',
|
|
145
|
-
|
|
146
|
-
"Set realm to your server's hostname.
|
|
213
|
+
`${missing.length} missing`,
|
|
214
|
+
"Set realm to your server's hostname.",
|
|
215
|
+
),
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function validateExpiration(challenges: Challenge.Challenge[], results: CheckResult[]): void {
|
|
221
|
+
const now = new Date()
|
|
222
|
+
for (const ch of challenges) {
|
|
223
|
+
if (!ch.expires) {
|
|
224
|
+
results.push(
|
|
225
|
+
warn(
|
|
226
|
+
'Challenge has expiration',
|
|
227
|
+
'missing expires field',
|
|
228
|
+
'Add an expires field (ISO 8601) to prevent replay attacks.',
|
|
229
|
+
),
|
|
230
|
+
)
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
if (new Date(ch.expires) <= now) {
|
|
234
|
+
results.push(
|
|
235
|
+
fail(
|
|
236
|
+
'Challenge expires in the future',
|
|
237
|
+
`Expired at ${ch.expires}`,
|
|
238
|
+
'The expires timestamp must be in the future.',
|
|
239
|
+
),
|
|
240
|
+
)
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const soonest = Math.min(
|
|
245
|
+
...challenges.map((ch) => new Date(ch.expires!).getTime() - now.getTime()),
|
|
246
|
+
)
|
|
247
|
+
results.push(check('Challenge expires in the future', `${Math.round(soonest / 60000)}m from now`))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function validateRealmMatchesHost(
|
|
251
|
+
challenges: Challenge.Challenge[],
|
|
252
|
+
serverHost: string,
|
|
253
|
+
results: CheckResult[],
|
|
254
|
+
): void {
|
|
255
|
+
const realms = [...new Set(challenges.map((ch) => ch.realm ?? ''))]
|
|
256
|
+
const badRealms = realms.filter((r) => r && r !== serverHost && !serverHost.endsWith(`.${r}`))
|
|
257
|
+
if (badRealms.length > 0) {
|
|
258
|
+
results.push(
|
|
259
|
+
warn(
|
|
260
|
+
'Realm matches server hostname',
|
|
261
|
+
`realm="${badRealms[0]}" vs host="${serverHost}"`,
|
|
262
|
+
'Set the realm to your production hostname (or base domain) in the challenge.',
|
|
147
263
|
),
|
|
148
264
|
)
|
|
149
|
-
else
|
|
265
|
+
} else {
|
|
266
|
+
results.push(check('Realm matches server hostname'))
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function validateMethodFields(
|
|
271
|
+
challenge: Challenge.Challenge,
|
|
272
|
+
request: Record<string, unknown>,
|
|
273
|
+
results: CheckResult[],
|
|
274
|
+
hasMultipleMethods: boolean,
|
|
275
|
+
): void {
|
|
276
|
+
const tag = hasMultipleMethods ? `[${challenge.method}] ` : ''
|
|
150
277
|
|
|
151
|
-
if (
|
|
278
|
+
if (challenge.method === Constants.Methods.tempo)
|
|
279
|
+
validateTempoFields(request, tag, results, challenge)
|
|
280
|
+
else if (challenge.method === Constants.Methods.stripe)
|
|
281
|
+
validateStripeFields(request, tag, results)
|
|
282
|
+
else if (challenge.method === Constants.Methods.evm) validateEvmFields(request, tag, results)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function validateTempoFields(
|
|
286
|
+
request: Record<string, unknown>,
|
|
287
|
+
tag: string,
|
|
288
|
+
results: CheckResult[],
|
|
289
|
+
challenge: Challenge.Challenge,
|
|
290
|
+
): void {
|
|
291
|
+
const methodDetails = request.methodDetails as Record<string, unknown> | undefined
|
|
292
|
+
const hasSplits = Array.isArray(methodDetails?.splits) && methodDetails.splits.length > 0
|
|
293
|
+
|
|
294
|
+
if (isValidAddress(request.recipient)) {
|
|
295
|
+
results.push(check(`${tag}Valid recipient address`))
|
|
296
|
+
} else if (request.recipient === undefined && hasSplits) {
|
|
297
|
+
results.push(check(`${tag}Uses splits (no single recipient)`))
|
|
298
|
+
} else if (request.recipient === undefined) {
|
|
152
299
|
results.push(
|
|
153
300
|
fail(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
'Set
|
|
301
|
+
`${tag}Valid recipient address`,
|
|
302
|
+
'Missing recipient (and no splits defined)',
|
|
303
|
+
'Set request.recipient to a valid 0x address, or use methodDetails.splits for multiple recipients.',
|
|
157
304
|
),
|
|
158
305
|
)
|
|
159
|
-
|
|
306
|
+
} else {
|
|
160
307
|
results.push(
|
|
161
308
|
fail(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
'Set
|
|
309
|
+
`${tag}Valid recipient address`,
|
|
310
|
+
`Got: ${String(request.recipient)}`,
|
|
311
|
+
'Set request.recipient to a valid 0x-prefixed 40-hex-char address.',
|
|
165
312
|
),
|
|
166
313
|
)
|
|
314
|
+
}
|
|
167
315
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const now = new Date()
|
|
172
|
-
if (expiresDate <= now) {
|
|
173
|
-
results.push(
|
|
174
|
-
fail(
|
|
175
|
-
'Challenge expires in the future',
|
|
176
|
-
`Expired at ${challenge.expires}`,
|
|
177
|
-
'The expires timestamp must be in the future when the challenge is issued. Use a 5-10 minute window from the current time.',
|
|
178
|
-
),
|
|
179
|
-
)
|
|
180
|
-
} else {
|
|
181
|
-
const diffMs = expiresDate.getTime() - now.getTime()
|
|
182
|
-
const diffMin = Math.round(diffMs / 60000)
|
|
183
|
-
results.push(check('Challenge expires in the future', `${diffMin}m from now`))
|
|
184
|
-
}
|
|
316
|
+
if (isValidAddress(request.currency)) {
|
|
317
|
+
const network = detectTestnet(challenge) ? 'testnet' : 'mainnet'
|
|
318
|
+
results.push(check(`${tag}Valid currency address`, network))
|
|
185
319
|
} else {
|
|
186
320
|
results.push(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
'
|
|
321
|
+
fail(
|
|
322
|
+
`${tag}Valid currency address`,
|
|
323
|
+
`Got: ${String(request.currency)}`,
|
|
324
|
+
'Set request.currency to a valid token address.',
|
|
191
325
|
),
|
|
192
326
|
)
|
|
193
327
|
}
|
|
194
328
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
} else {
|
|
203
|
-
results.push(
|
|
204
|
-
warn(
|
|
205
|
-
'Realm matches server hostname',
|
|
206
|
-
`realm="${realm}" vs host="${serverHost}"`,
|
|
207
|
-
'Set the realm to your production hostname (or base domain) in the challenge. Clients use realm to verify they are paying the right server.',
|
|
208
|
-
),
|
|
209
|
-
)
|
|
210
|
-
}
|
|
211
|
-
} catch {}
|
|
329
|
+
validateAmount(
|
|
330
|
+
request,
|
|
331
|
+
tag,
|
|
332
|
+
results,
|
|
333
|
+
'token\'s smallest unit (e.g. "10000" = $0.01 for 6-decimal tokens)',
|
|
334
|
+
)
|
|
335
|
+
}
|
|
212
336
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
results.push(
|
|
220
|
-
fail(
|
|
221
|
-
'Valid recipient address',
|
|
222
|
-
`Got: ${String(request.recipient)}`,
|
|
223
|
-
'Set request.recipient to a valid 0x-prefixed 40-hex-char address. This is where payment will be sent.',
|
|
224
|
-
),
|
|
225
|
-
)
|
|
226
|
-
}
|
|
337
|
+
function validateStripeFields(
|
|
338
|
+
request: Record<string, unknown>,
|
|
339
|
+
tag: string,
|
|
340
|
+
results: CheckResult[],
|
|
341
|
+
): void {
|
|
342
|
+
validateAmount(request, tag, results, 'currency\'s smallest unit (e.g. "100" = $1.00 for USD)')
|
|
227
343
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
344
|
+
const currency = request.currency as string | undefined
|
|
345
|
+
const validCurrency = typeof currency === 'string' && /^[a-z]{3}$/i.test(currency)
|
|
346
|
+
if (validCurrency) {
|
|
347
|
+
results.push(check(`${tag}Valid currency code`, currency.toUpperCase()))
|
|
348
|
+
} else {
|
|
349
|
+
results.push(
|
|
350
|
+
fail(
|
|
351
|
+
`${tag}Valid currency code`,
|
|
352
|
+
currency ? `Got: ${currency}` : 'missing',
|
|
353
|
+
'Must be a three-letter ISO currency code (e.g. "usd").',
|
|
354
|
+
),
|
|
355
|
+
)
|
|
356
|
+
}
|
|
241
357
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
fail(
|
|
255
|
-
'Amount is valid integer string',
|
|
256
|
-
`Got: ${String(request.amount)}`,
|
|
257
|
-
'request.amount must be a string of digits (no decimals, no prefix). Example: "10000" for $0.01 with 6-decimal tokens.',
|
|
258
|
-
),
|
|
259
|
-
)
|
|
260
|
-
}
|
|
261
|
-
} else if (challenge.method === Constants.Methods.stripe) {
|
|
262
|
-
if (request.amount !== undefined) {
|
|
263
|
-
results.push(check('Stripe challenge has amount'))
|
|
264
|
-
} else {
|
|
265
|
-
results.push(warn('Stripe challenge has amount', 'No amount field'))
|
|
266
|
-
}
|
|
358
|
+
const methodDetails = request.methodDetails as Record<string, unknown> | undefined
|
|
359
|
+
const networkId = methodDetails?.networkId as string | undefined
|
|
360
|
+
if (networkId) {
|
|
361
|
+
results.push(check(`${tag}Has networkId`, networkId.slice(0, 20)))
|
|
362
|
+
} else {
|
|
363
|
+
results.push(
|
|
364
|
+
fail(
|
|
365
|
+
`${tag}Has networkId`,
|
|
366
|
+
'Missing methodDetails.networkId',
|
|
367
|
+
'Set methodDetails.networkId to your Stripe Business Network profile ID.',
|
|
368
|
+
),
|
|
369
|
+
)
|
|
267
370
|
}
|
|
268
371
|
|
|
269
|
-
|
|
270
|
-
|
|
372
|
+
const pmTypes = methodDetails?.paymentMethodTypes as string[] | undefined
|
|
373
|
+
if (Array.isArray(pmTypes) && pmTypes.length > 0) {
|
|
374
|
+
results.push(check(`${tag}Has paymentMethodTypes`, pmTypes.join(', ')))
|
|
375
|
+
} else {
|
|
376
|
+
results.push(
|
|
377
|
+
fail(
|
|
378
|
+
`${tag}Has paymentMethodTypes`,
|
|
379
|
+
'Missing or empty',
|
|
380
|
+
'Set methodDetails.paymentMethodTypes to an array (e.g. ["card"]).',
|
|
381
|
+
),
|
|
382
|
+
)
|
|
271
383
|
}
|
|
384
|
+
}
|
|
272
385
|
|
|
273
|
-
|
|
386
|
+
function validateEvmFields(
|
|
387
|
+
request: Record<string, unknown>,
|
|
388
|
+
tag: string,
|
|
389
|
+
results: CheckResult[],
|
|
390
|
+
): void {
|
|
391
|
+
if (isValidAddress(request.recipient)) {
|
|
392
|
+
results.push(check(`${tag}Valid recipient address`))
|
|
393
|
+
} else {
|
|
394
|
+
results.push(
|
|
395
|
+
fail(
|
|
396
|
+
`${tag}Valid recipient address`,
|
|
397
|
+
`Got: ${String(request.recipient)}`,
|
|
398
|
+
'Set request.recipient to a valid 0x-prefixed Ethereum address.',
|
|
399
|
+
),
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (isValidAddress(request.currency)) {
|
|
404
|
+
results.push(check(`${tag}Valid currency address`))
|
|
405
|
+
} else {
|
|
406
|
+
results.push(
|
|
407
|
+
fail(
|
|
408
|
+
`${tag}Valid currency address`,
|
|
409
|
+
`Got: ${String(request.currency)}`,
|
|
410
|
+
'Set request.currency to a valid ERC-20 token contract address.',
|
|
411
|
+
),
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
validateAmount(request, tag, results, "token's smallest unit")
|
|
416
|
+
|
|
417
|
+
const methodDetails = request.methodDetails as Record<string, unknown> | undefined
|
|
418
|
+
const chainId = methodDetails?.chainId as number | undefined
|
|
419
|
+
if (typeof chainId === 'number' && chainId > 0) {
|
|
420
|
+
results.push(check(`${tag}Has chainId`, String(chainId)))
|
|
421
|
+
} else {
|
|
422
|
+
results.push(
|
|
423
|
+
fail(
|
|
424
|
+
`${tag}Has chainId`,
|
|
425
|
+
'Missing methodDetails.chainId',
|
|
426
|
+
'Set methodDetails.chainId to the EVM chain ID.',
|
|
427
|
+
),
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function validateAmount(
|
|
433
|
+
request: Record<string, unknown>,
|
|
434
|
+
tag: string,
|
|
435
|
+
results: CheckResult[],
|
|
436
|
+
unitHint: string,
|
|
437
|
+
): void {
|
|
438
|
+
if (isValidIntegerAmount(request.amount)) {
|
|
439
|
+
results.push(check(`${tag}Amount is valid integer string`))
|
|
440
|
+
} else if (request.amount === undefined || request.amount === null) {
|
|
441
|
+
results.push(
|
|
442
|
+
fail(
|
|
443
|
+
`${tag}Amount is valid integer string`,
|
|
444
|
+
'Missing amount',
|
|
445
|
+
`Set request.amount to a string of digits in the ${unitHint}.`,
|
|
446
|
+
),
|
|
447
|
+
)
|
|
448
|
+
} else {
|
|
449
|
+
results.push(
|
|
450
|
+
fail(
|
|
451
|
+
`${tag}Amount is valid integer string`,
|
|
452
|
+
`Got: ${String(request.amount)}`,
|
|
453
|
+
'request.amount must be a string of digits (no decimals, no prefix).',
|
|
454
|
+
),
|
|
455
|
+
)
|
|
456
|
+
}
|
|
274
457
|
}
|
|
275
458
|
|
|
276
459
|
export async function validateErrorHandling(
|
|
@@ -333,3 +516,33 @@ export async function validateErrorHandling(
|
|
|
333
516
|
|
|
334
517
|
return results
|
|
335
518
|
}
|
|
519
|
+
|
|
520
|
+
function checkX402Headers(response: Response): CheckResult[] {
|
|
521
|
+
const results: CheckResult[] = []
|
|
522
|
+
const paymentRequiredRaw =
|
|
523
|
+
response.headers.get('payment-required') ?? response.headers.get('x-payment-required')
|
|
524
|
+
if (!paymentRequiredRaw) return results
|
|
525
|
+
|
|
526
|
+
const headerName = response.headers.get('payment-required')
|
|
527
|
+
? 'PAYMENT-REQUIRED'
|
|
528
|
+
: 'X-Payment-Required'
|
|
529
|
+
|
|
530
|
+
try {
|
|
531
|
+
const decoded = x402Header.decodePaymentRequired(paymentRequiredRaw)
|
|
532
|
+
results.push(
|
|
533
|
+
check(
|
|
534
|
+
'x402 payment challenge',
|
|
535
|
+
`${headerName} with valid x402 v${decoded.x402Version} payload`,
|
|
536
|
+
),
|
|
537
|
+
)
|
|
538
|
+
} catch {
|
|
539
|
+
results.push(
|
|
540
|
+
warn(
|
|
541
|
+
'x402 payment challenge',
|
|
542
|
+
`${headerName} header present but failed schema validation`,
|
|
543
|
+
'Ensure the PAYMENT-REQUIRED header contains a valid base64-encoded JSON payload matching the x402 schema.',
|
|
544
|
+
),
|
|
545
|
+
)
|
|
546
|
+
}
|
|
547
|
+
return results
|
|
548
|
+
}
|