x402-surface-check 0.2.14 → 0.2.15
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 +16 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ npx --yes x402-surface-check --endpoint --method POST https://x402.rpc.ankr.com/
|
|
|
16
16
|
|
|
17
17
|
- Manifest endpoint discovery from `items[]`, `endpoints[]`, `resources[]`, `x402Endpoints`, category arrays, resource strings, and OpenAPI paths
|
|
18
18
|
- Linked discovery documents via `discovery_url`, `discoveryUrl`, `resources_url`, `resourcesUrl`, or manifest-level OpenAPI links
|
|
19
|
+
- OpenAPI `servers[]` base-path preservation, so `/paths` are probed through the documented gateway rather than the domain root
|
|
19
20
|
- OpenAPI query/path examples and JSON request-body examples for safer no-payment probes
|
|
20
21
|
- No-payment HTTP 402 challenge shape
|
|
21
22
|
- x402 v1 and v2 price fields, including `accepts[]` and `schemes[]` challenge arrays
|
|
@@ -26,7 +27,7 @@ npx --yes x402-surface-check --endpoint --method POST https://x402.rpc.ankr.com/
|
|
|
26
27
|
- Placeholder recipients such as zero addresses and Solana system-program values
|
|
27
28
|
- Testnet or staging rails such as Base Sepolia and Solana devnet
|
|
28
29
|
- HTTPS resource URLs and stable resource metadata
|
|
29
|
-
- Browser CORS allowance for the requesting origin and `X-PAYMENT
|
|
30
|
+
- Browser CORS allowance for the requesting origin and `X-PAYMENT`, including the actual 402 challenge response
|
|
30
31
|
- Over-broad public method surfaces
|
|
31
32
|
- Auth, validation, and free/trial responses that appear before a payment challenge, without piling on missing-field findings when no challenge was actually returned
|
|
32
33
|
- Operational health/status endpoints, without treating expected free health checks as paid-route failures
|
|
@@ -132,6 +132,12 @@ function endpointUrl(rawPath, baseUrl, sourceUrl) {
|
|
|
132
132
|
return new URL(value, base).toString()
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
function openApiServerBaseUrl(document, sourceUrl) {
|
|
136
|
+
const rawUrl = document.servers?.find(server => typeof server?.url === 'string')?.url
|
|
137
|
+
if (!rawUrl) return documentBaseUrl(document, sourceUrl)
|
|
138
|
+
return endpointUrl(rawUrl, documentBaseUrl(document, sourceUrl), sourceUrl)
|
|
139
|
+
}
|
|
140
|
+
|
|
135
141
|
function linkedDiscoveryUrl(document, sourceUrl) {
|
|
136
142
|
const rawUrl = document?.discovery_url
|
|
137
143
|
?? document?.discoveryUrl
|
|
@@ -217,7 +223,9 @@ function openApiProbeUrl(path, operation, baseUrl) {
|
|
|
217
223
|
}
|
|
218
224
|
}
|
|
219
225
|
|
|
220
|
-
const url =
|
|
226
|
+
const url = /^https?:\/\//i.test(String(resolvedPath))
|
|
227
|
+
? new URL(resolvedPath)
|
|
228
|
+
: new URL(String(resolvedPath).replace(/^\/+/, ''), `${baseUrl.replace(/\/?$/, '/')}`)
|
|
221
229
|
for (const [name, value] of searchParams.entries()) {
|
|
222
230
|
url.searchParams.set(name, value)
|
|
223
231
|
}
|
|
@@ -273,8 +281,7 @@ function endpointEntries(document, sourceUrl, limit) {
|
|
|
273
281
|
}
|
|
274
282
|
|
|
275
283
|
if (document.openapi && document.paths && typeof document.paths === 'object') {
|
|
276
|
-
const baseUrl = document
|
|
277
|
-
?? sourceUrl
|
|
284
|
+
const baseUrl = openApiServerBaseUrl(document, sourceUrl)
|
|
278
285
|
|
|
279
286
|
for (const [path, operations] of Object.entries(document.paths)) {
|
|
280
287
|
if (!operations || typeof operations !== 'object') continue
|
|
@@ -428,7 +435,7 @@ async function fetchDocument(url) {
|
|
|
428
435
|
}
|
|
429
436
|
}
|
|
430
437
|
|
|
431
|
-
async function probeEndpoint(entry) {
|
|
438
|
+
async function probeEndpoint(entry, origin) {
|
|
432
439
|
const method = entry.method ?? 'POST'
|
|
433
440
|
const response = await fetch(entry.url, {
|
|
434
441
|
method,
|
|
@@ -436,6 +443,7 @@ async function probeEndpoint(entry) {
|
|
|
436
443
|
'user-agent': `x402-surface-check/${packageJson.version}`,
|
|
437
444
|
accept: 'application/json',
|
|
438
445
|
'content-type': 'application/json',
|
|
446
|
+
...(origin ? { origin } : {}),
|
|
439
447
|
},
|
|
440
448
|
body: method === 'GET' || method === 'HEAD'
|
|
441
449
|
? undefined
|
|
@@ -656,6 +664,9 @@ function findingList(documentResult, challengeResults, preflightResults, entries
|
|
|
656
664
|
continue
|
|
657
665
|
}
|
|
658
666
|
|
|
667
|
+
if (!result.headers?.['access-control-allow-origin']) {
|
|
668
|
+
findings.push(`P1 - ${result.name} 402 challenge response does not allow the requesting origin; browser agents cannot read the payment requirements even if preflight succeeds.`)
|
|
669
|
+
}
|
|
659
670
|
if (summary.resourceUrl.startsWith('http://') || summary.extraResource.startsWith('http://')) {
|
|
660
671
|
findings.push(`P1 - ${result.name} challenge uses a non-HTTPS resource URL: ${summary.resourceUrl || summary.extraResource}.`)
|
|
661
672
|
}
|
|
@@ -796,7 +807,7 @@ async function runCheck(options) {
|
|
|
796
807
|
const preflights = []
|
|
797
808
|
|
|
798
809
|
for (const entry of entries) {
|
|
799
|
-
challenges.push(await probeEndpoint(entry))
|
|
810
|
+
challenges.push(await probeEndpoint(entry, origin))
|
|
800
811
|
preflights.push(await probePreflight(entry, origin))
|
|
801
812
|
}
|
|
802
813
|
|