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 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)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x402-surface-check",
3
- "version": "0.2.24",
3
+ "version": "0.2.25",
4
4
  "description": "No-payment x402 public-surface checker for manifests, OpenAPI specs, and HTTP 402 challenges.",
5
5
  "type": "module",
6
6
  "bin": {