x402-surface-check 0.2.1 → 0.2.3
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/README.md +2 -2
- package/bin/x402-surface-check.mjs +46 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,10 +14,10 @@ npx --yes x402-surface-check --endpoint --method POST https://x402.rpc.ankr.com/
|
|
|
14
14
|
|
|
15
15
|
## What It Checks
|
|
16
16
|
|
|
17
|
-
- Manifest endpoint discovery from `endpoints[]`, `x402Endpoints`, category arrays, resource strings, and OpenAPI paths
|
|
17
|
+
- Manifest endpoint discovery from `items[]`, `endpoints[]`, `x402Endpoints`, category arrays, resource strings, and OpenAPI paths
|
|
18
18
|
- No-payment HTTP 402 challenge shape
|
|
19
19
|
- x402 v1 and v2 price fields
|
|
20
|
-
- MPP `WWW-Authenticate: Payment` challenges
|
|
20
|
+
- MPP `WWW-Authenticate: Payment` and x402 V2 `WWW-Authenticate: X402 requirements=...` challenges
|
|
21
21
|
- `amount` / `maxAmountRequired`, `asset`, `network`, and `payTo`
|
|
22
22
|
- Placeholder recipients such as zero addresses and Solana system-program values
|
|
23
23
|
- Testnet or staging rails such as Base Sepolia and Solana devnet
|
|
@@ -151,6 +151,19 @@ function endpointEntries(document, sourceUrl, limit) {
|
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
if (Array.isArray(document.items)) {
|
|
155
|
+
for (const item of document.items) {
|
|
156
|
+
if (item?.type && item.type !== 'http') continue
|
|
157
|
+
const rawPath = item?.resource ?? item?.url ?? item?.endpoint ?? item?.path
|
|
158
|
+
if (!rawPath) continue
|
|
159
|
+
entries.push({
|
|
160
|
+
name: item.metadata?.name ?? item.id ?? item.name ?? String(rawPath).split('/').filter(Boolean).at(-1) ?? String(rawPath),
|
|
161
|
+
url: endpointUrl(rawPath, baseUrl, sourceUrl),
|
|
162
|
+
method: String(item.method ?? 'GET').toUpperCase(),
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
154
167
|
if (document.openapi && document.paths && typeof document.paths === 'object') {
|
|
155
168
|
const baseUrl = document.servers?.find(server => typeof server?.url === 'string')?.url
|
|
156
169
|
?? sourceUrl
|
|
@@ -215,17 +228,25 @@ function parseEncodedChallenge(value) {
|
|
|
215
228
|
}
|
|
216
229
|
}
|
|
217
230
|
|
|
218
|
-
function
|
|
219
|
-
|
|
231
|
+
function authenticateParams(value, scheme) {
|
|
232
|
+
const header = String(value ?? '').replace(/^www-authenticate:\s*/i, '').trim()
|
|
233
|
+
if (!header || !new RegExp(`^${scheme}\\s+`, 'i').test(header)) return null
|
|
220
234
|
const params = {}
|
|
221
235
|
const pattern = /([a-zA-Z][\w-]*)="([^"]*)"/g
|
|
222
|
-
let match = pattern.exec(
|
|
236
|
+
let match = pattern.exec(header)
|
|
223
237
|
|
|
224
238
|
while (match) {
|
|
225
239
|
params[match[1]] = match[2]
|
|
226
|
-
match = pattern.exec(
|
|
240
|
+
match = pattern.exec(header)
|
|
227
241
|
}
|
|
228
242
|
|
|
243
|
+
return params
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function parsePaymentAuthenticate(value) {
|
|
247
|
+
const params = authenticateParams(value, 'Payment')
|
|
248
|
+
if (!params) return null
|
|
249
|
+
|
|
229
250
|
const request = parseEncodedChallenge(params.request)
|
|
230
251
|
if (!request) return null
|
|
231
252
|
|
|
@@ -251,6 +272,19 @@ function parsePaymentAuthenticate(value) {
|
|
|
251
272
|
}
|
|
252
273
|
}
|
|
253
274
|
|
|
275
|
+
function parseX402Authenticate(value) {
|
|
276
|
+
const params = authenticateParams(value, 'X402')
|
|
277
|
+
if (!params) return null
|
|
278
|
+
|
|
279
|
+
const requirements = parseEncodedChallenge(params.requirements ?? params.request)
|
|
280
|
+
if (!requirements || !Array.isArray(requirements.accepts)) return null
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
protocol: requirements.protocol ?? 'x402',
|
|
284
|
+
...requirements,
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
254
288
|
async function fetchDocument(url) {
|
|
255
289
|
const response = await fetch(url, {
|
|
256
290
|
headers: {
|
|
@@ -283,16 +317,18 @@ async function probeEndpoint(entry) {
|
|
|
283
317
|
const headerChallenge = parseEncodedChallenge(
|
|
284
318
|
response.headers.get('payment-required') ?? response.headers.get('x-payment-required'),
|
|
285
319
|
)
|
|
286
|
-
const
|
|
320
|
+
const authenticateChallenge = parsePaymentAuthenticate(response.headers.get('www-authenticate'))
|
|
321
|
+
?? parseX402Authenticate(response.headers.get('www-authenticate'))
|
|
287
322
|
|
|
288
323
|
if (!body.json?.accepts?.length) {
|
|
289
324
|
if (headerChallenge) {
|
|
290
325
|
body.json = headerChallenge
|
|
291
326
|
}
|
|
292
|
-
else if (
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
327
|
+
else if (authenticateChallenge) {
|
|
328
|
+
authenticateChallenge.resource = authenticateChallenge.resource ?? { url: entry.url }
|
|
329
|
+
authenticateChallenge.resource.url = authenticateChallenge.resource.url || entry.url
|
|
330
|
+
authenticateChallenge.accepts[0].resource = authenticateChallenge.accepts[0].resource || entry.url
|
|
331
|
+
body.json = authenticateChallenge
|
|
296
332
|
}
|
|
297
333
|
}
|
|
298
334
|
|
|
@@ -385,7 +421,7 @@ function findingList(documentResult, challengeResults, preflightResults, entries
|
|
|
385
421
|
}
|
|
386
422
|
|
|
387
423
|
if (entries.length === 0) {
|
|
388
|
-
findings.push('P1 - Document does not expose any manifest, OpenAPI, category, or resource endpoints for no-payment probes.')
|
|
424
|
+
findings.push('P1 - Document does not expose any manifest, OpenAPI, item, category, or resource endpoints for no-payment probes.')
|
|
389
425
|
}
|
|
390
426
|
|
|
391
427
|
for (const result of challengeResults) {
|