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 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 = path.startsWith('http') ? new URL(resolvedPath) : new URL(resolvedPath, baseUrl)
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.servers?.find(server => typeof server?.url === 'string')?.url
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x402-surface-check",
3
- "version": "0.2.14",
3
+ "version": "0.2.15",
4
4
  "description": "No-payment x402 public-surface checker for manifests, OpenAPI specs, and HTTP 402 challenges.",
5
5
  "type": "module",
6
6
  "bin": {