x402-surface-check 0.2.13 → 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
@@ -14,8 +14,9 @@ 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 `items[]`, `endpoints[]`, `x402Endpoints`, category arrays, resource strings, and OpenAPI paths
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 `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
@@ -44,6 +45,7 @@ Recent public no-payment checks have found and verified real launch fixes:
44
45
  - anchor-x402: multi-rail x402 challenges verified, with browser preflight blockers isolated before merge. https://github.com/solana-foundation/pay-skills/pull/47#issuecomment-4455678163
45
46
  - Agent Trust Bench: linked discovery URL and browser-compatibility notes verified clean for adversarial agent-payment resources. https://github.com/solana-foundation/pay-skills/pull/23#issuecomment-4455722170
46
47
  - Solrouter: private LLM inference route verified with HTTPS resource-binding and price-alignment notes. https://github.com/solana-foundation/pay-skills/pull/39#issuecomment-4455800060
48
+ - Tetrac: Solana market-data payment gates verified, with browser payment-header preflight blocker isolated. https://github.com/solana-foundation/pay-skills/pull/32#issuecomment-4455923744
47
49
 
48
50
  Field notes and browser version: https://tateprograms.com/x402-surface-check.html
49
51
 
@@ -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
@@ -294,17 +301,32 @@ function endpointEntries(document, sourceUrl, limit) {
294
301
  }
295
302
 
296
303
  for (const resource of document.resources ?? []) {
297
- if (typeof resource !== 'string') continue
298
- const match = resource.match(/^(GET|POST|PUT|PATCH|DELETE)\s+(\S+)/i)
299
- if (!match) continue
300
- const [, method, rawPath] = match
301
- const url = rawPath.startsWith('http')
302
- ? rawPath
303
- : new URL(rawPath, document.baseUrl ?? sourceUrl).toString()
304
+ if (typeof resource === 'string') {
305
+ const match = resource.match(/^(GET|POST|PUT|PATCH|DELETE)\s+(\S+)/i)
306
+ if (!match) continue
307
+ const [, method, rawPath] = match
308
+ const url = rawPath.startsWith('http')
309
+ ? rawPath
310
+ : new URL(rawPath, document.baseUrl ?? sourceUrl).toString()
311
+ entries.push({
312
+ name: rawPath.split('/').filter(Boolean).at(-1) ?? rawPath,
313
+ url,
314
+ method: method.toUpperCase(),
315
+ })
316
+ continue
317
+ }
318
+
319
+ if (!resource || typeof resource !== 'object') continue
320
+ const rawPath = resource.url ?? resource.endpoint ?? resource.resource ?? resource.path
321
+ if (!rawPath) continue
304
322
  entries.push({
305
- name: rawPath.split('/').filter(Boolean).at(-1) ?? rawPath,
306
- url,
307
- method: method.toUpperCase(),
323
+ name: resource.id
324
+ ?? resource.name
325
+ ?? resource.title
326
+ ?? String(resource.path ?? rawPath).split('/').filter(Boolean).at(-1)
327
+ ?? String(rawPath),
328
+ url: endpointUrl(rawPath, baseUrl, sourceUrl),
329
+ method: String(resource.method ?? 'GET').toUpperCase(),
308
330
  })
309
331
  }
310
332
 
@@ -413,7 +435,7 @@ async function fetchDocument(url) {
413
435
  }
414
436
  }
415
437
 
416
- async function probeEndpoint(entry) {
438
+ async function probeEndpoint(entry, origin) {
417
439
  const method = entry.method ?? 'POST'
418
440
  const response = await fetch(entry.url, {
419
441
  method,
@@ -421,6 +443,7 @@ async function probeEndpoint(entry) {
421
443
  'user-agent': `x402-surface-check/${packageJson.version}`,
422
444
  accept: 'application/json',
423
445
  'content-type': 'application/json',
446
+ ...(origin ? { origin } : {}),
424
447
  },
425
448
  body: method === 'GET' || method === 'HEAD'
426
449
  ? undefined
@@ -641,6 +664,9 @@ function findingList(documentResult, challengeResults, preflightResults, entries
641
664
  continue
642
665
  }
643
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
+ }
644
670
  if (summary.resourceUrl.startsWith('http://') || summary.extraResource.startsWith('http://')) {
645
671
  findings.push(`P1 - ${result.name} challenge uses a non-HTTPS resource URL: ${summary.resourceUrl || summary.extraResource}.`)
646
672
  }
@@ -669,6 +695,10 @@ function findingList(documentResult, challengeResults, preflightResults, entries
669
695
  for (const result of preflightResults) {
670
696
  const challengeResult = challengesByEntry.get(entryKey(result))
671
697
  if (!challengeResult || !hasPaymentChallenge(challengeResult)) continue
698
+ const allowedOrigin = result.headers['access-control-allow-origin'] ?? ''
699
+ if (!allowedOrigin) {
700
+ findings.push(`P1 - ${result.name} CORS preflight does not allow the requesting origin; observed allow-origin: none.`)
701
+ }
672
702
  const allowed = result.headers['access-control-allow-headers'] ?? ''
673
703
  if (allowed !== '*' && !/x-payment/i.test(allowed)) {
674
704
  const observed = result.status >= 400
@@ -777,7 +807,7 @@ async function runCheck(options) {
777
807
  const preflights = []
778
808
 
779
809
  for (const entry of entries) {
780
- challenges.push(await probeEndpoint(entry))
810
+ challenges.push(await probeEndpoint(entry, origin))
781
811
  preflights.push(await probePreflight(entry, origin))
782
812
  }
783
813
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x402-surface-check",
3
- "version": "0.2.13",
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": {