x402-surface-check 0.2.24 → 0.2.25
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 -1
- package/bin/x402-surface-check.mjs +66 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,8 @@ npx --yes x402-surface-check --strict-cache https://api.example.com/openapi.json
|
|
|
17
17
|
|
|
18
18
|
## What It Checks
|
|
19
19
|
|
|
20
|
-
- Manifest endpoint discovery from `items[]`, `endpoints[]`, `resources[]`, `x402Endpoints`, category arrays, raw resource URL strings, method-prefixed resource strings, and OpenAPI paths
|
|
20
|
+
- Manifest endpoint discovery from `items[]`, `endpoints[]`, object-valued `endpoints`, `resources[]`, `x402Endpoints`, category arrays, raw resource URL strings, method-prefixed resource strings, and OpenAPI paths
|
|
21
|
+
- Object-valued manifest endpoint query examples, public catalog/discovery GETs, and payment-bearing two-phase operations without treating expected public catalog reads as failed payment gates
|
|
21
22
|
- Linked discovery documents via `discovery_url`, `discoveryUrl`, `resources_url`, `resourcesUrl`, or manifest-level OpenAPI links
|
|
22
23
|
- OpenAPI `servers[]` base-path preservation, so `/paths` are probed through the documented gateway rather than the domain root
|
|
23
24
|
- OpenAPI query/path examples, JSON request-body examples, nested request schemas, local `$ref` request schemas, and explicit direct-endpoint bodies for safer no-payment probes
|
|
@@ -322,6 +322,47 @@ function openApiProbeUrl(path, operation, baseUrl, document) {
|
|
|
322
322
|
return url.toString()
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
+
function manifestEndpointPaymentSignal(endpoint) {
|
|
326
|
+
if (!endpoint || typeof endpoint !== 'object') return 0
|
|
327
|
+
if (Number(endpoint.phase1_response?.status) === 402) return 2
|
|
328
|
+
if (/payment-required|x-payment|402/i.test(String(endpoint.phase1_response?.header ?? ''))) return 2
|
|
329
|
+
if (/payment|required|402/i.test(String(endpoint.description ?? ''))) return 1
|
|
330
|
+
if (endpoint.accepts || endpoint.schemes || endpoint.payment || endpoint['x-payment-info']) return 1
|
|
331
|
+
return 0
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function manifestEndpointBody(endpoint, document) {
|
|
335
|
+
const body = endpoint?.request_body ?? endpoint?.requestBody
|
|
336
|
+
if (!body || typeof body !== 'object') return undefined
|
|
337
|
+
if (body.example !== undefined) return body.example
|
|
338
|
+
if (body.safe_example !== undefined) return body.safe_example
|
|
339
|
+
if (body.safeExample !== undefined) return body.safeExample
|
|
340
|
+
return exampleValue(body, document)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function manifestEndpointUrl(rawPath, endpoint, baseUrl, sourceUrl) {
|
|
344
|
+
const url = new URL(endpointUrl(rawPath, baseUrl, sourceUrl))
|
|
345
|
+
const parameters = endpoint?.parameters
|
|
346
|
+
if (!parameters || typeof parameters !== 'object') return url.toString()
|
|
347
|
+
|
|
348
|
+
for (const [name, parameter] of Object.entries(parameters)) {
|
|
349
|
+
if (url.pathname.includes(`{${name}}`)) {
|
|
350
|
+
const pathValue = parameter?.example ?? parameter?.default
|
|
351
|
+
if (pathValue !== undefined && pathValue !== '') {
|
|
352
|
+
url.pathname = url.pathname.replaceAll(`{${name}}`, encodeURIComponent(String(pathValue)))
|
|
353
|
+
}
|
|
354
|
+
continue
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const value = parameter?.example ?? parameter?.default
|
|
358
|
+
if (value !== undefined && value !== '') {
|
|
359
|
+
url.searchParams.set(name, String(value))
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return url.toString()
|
|
364
|
+
}
|
|
365
|
+
|
|
325
366
|
function endpointEntries(document, sourceUrl, limit) {
|
|
326
367
|
const entries = []
|
|
327
368
|
const baseUrl = documentBaseUrl(document, sourceUrl)
|
|
@@ -356,6 +397,24 @@ function endpointEntries(document, sourceUrl, limit) {
|
|
|
356
397
|
})
|
|
357
398
|
}
|
|
358
399
|
}
|
|
400
|
+
else if (document.endpoints && typeof document.endpoints === 'object') {
|
|
401
|
+
for (const [key, endpoint] of Object.entries(document.endpoints)) {
|
|
402
|
+
if (!endpoint || typeof endpoint !== 'object') continue
|
|
403
|
+
const rawPath = endpoint.url ?? endpoint.endpoint ?? endpoint.path
|
|
404
|
+
if (!rawPath) continue
|
|
405
|
+
const method = String(endpoint.method ?? 'POST').toUpperCase()
|
|
406
|
+
const paymentSignal = manifestEndpointPaymentSignal(endpoint)
|
|
407
|
+
const hasPathParameters = /\{[^}]+\}/.test(String(rawPath))
|
|
408
|
+
if (paymentSignal === 0 && (method !== 'GET' || hasPathParameters)) continue
|
|
409
|
+
entries.push({
|
|
410
|
+
name: endpoint.id ?? endpoint.name ?? key,
|
|
411
|
+
url: manifestEndpointUrl(rawPath, endpoint, baseUrl, sourceUrl),
|
|
412
|
+
method,
|
|
413
|
+
requestBody: manifestEndpointBody(endpoint, document),
|
|
414
|
+
publicDiscovery: paymentSignal === 0,
|
|
415
|
+
})
|
|
416
|
+
}
|
|
417
|
+
}
|
|
359
418
|
|
|
360
419
|
if (Array.isArray(document.items)) {
|
|
361
420
|
for (const item of document.items) {
|
|
@@ -768,6 +827,13 @@ function findingList(documentResult, challengeResults, preflightResults, entries
|
|
|
768
827
|
if (summary.network) challengeNetworks.add(summary.network)
|
|
769
828
|
const hasChallenge = hasPaymentChallenge(result)
|
|
770
829
|
|
|
830
|
+
if (result.publicDiscovery && !hasChallenge) {
|
|
831
|
+
if (result.status < 200 || result.status >= 300) {
|
|
832
|
+
findings.push(`P2 - ${result.name} is documented as a public discovery route but returned HTTP ${result.status}; check the manifest example parameters or route availability.`)
|
|
833
|
+
}
|
|
834
|
+
continue
|
|
835
|
+
}
|
|
836
|
+
|
|
771
837
|
if (result.status !== 402) {
|
|
772
838
|
if (result.status >= 200 && result.status < 300) {
|
|
773
839
|
if (!looksLikeOperationalHealthEndpoint(result)) {
|