x402-surface-check 0.2.11 → 0.2.13

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
@@ -15,6 +15,8 @@ npx --yes x402-surface-check --endpoint --method POST https://x402.rpc.ankr.com/
15
15
  ## What It Checks
16
16
 
17
17
  - Manifest endpoint discovery from `items[]`, `endpoints[]`, `x402Endpoints`, category arrays, resource strings, and OpenAPI paths
18
+ - Linked discovery documents via `discovery_url`, `discoveryUrl`, `resources_url`, `resourcesUrl`, or manifest-level OpenAPI links
19
+ - OpenAPI query/path examples and JSON request-body examples for safer no-payment probes
18
20
  - No-payment HTTP 402 challenge shape
19
21
  - x402 v1 and v2 price fields, including `accepts[]` and `schemes[]` challenge arrays
20
22
  - MPP `WWW-Authenticate: Payment` and x402 V2 `WWW-Authenticate: X402 requirements=...` challenges
@@ -28,7 +30,7 @@ npx --yes x402-surface-check --endpoint --method POST https://x402.rpc.ankr.com/
28
30
  - Over-broad public method surfaces
29
31
  - Auth, validation, and free/trial responses that appear before a payment challenge, without piling on missing-field findings when no challenge was actually returned
30
32
  - Operational health/status endpoints, without treating expected free health checks as paid-route failures
31
- - Object-valued document metadata such as facilitator objects, without `[object Object]` report artifacts
33
+ - Object-valued document metadata such as facilitator or network objects, without `[object Object]` report artifacts
32
34
 
33
35
  ## Public Proof
34
36
 
@@ -39,7 +41,9 @@ Recent public no-payment checks have found and verified real launch fixes:
39
41
  - Spraay: resource echo and browser payment-header behavior verified clean. https://github.com/solana-foundation/pay-skills/pull/60#issuecomment-4455519760
40
42
  - UZPROOF: schemes-style Solana x402 challenge and browser payment-header behavior verified clean. https://github.com/solana-foundation/pay-skills/pull/28#issuecomment-4455613892
41
43
  - HYRE Agent: OpenAPI-declared prices found 10x below live 402 challenge prices. https://github.com/solana-foundation/pay-skills/pull/19#issuecomment-4455641258
42
- - Agent Trust Bench: live discovery URL and browser-compatibility notes for adversarial agent-payment resources. https://github.com/solana-foundation/pay-skills/pull/23#issuecomment-4455484414
44
+ - 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
+ - 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
+ - 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
43
47
 
44
48
  Field notes and browser version: https://tateprograms.com/x402-surface-check.html
45
49
 
@@ -132,6 +132,16 @@ function endpointUrl(rawPath, baseUrl, sourceUrl) {
132
132
  return new URL(value, base).toString()
133
133
  }
134
134
 
135
+ function linkedDiscoveryUrl(document, sourceUrl) {
136
+ const rawUrl = document?.discovery_url
137
+ ?? document?.discoveryUrl
138
+ ?? document?.resources_url
139
+ ?? document?.resourcesUrl
140
+ ?? (/^(https?:\/\/|\/)/i.test(String(document?.openapi ?? '')) ? document.openapi : '')
141
+ if (typeof rawUrl !== 'string' || !rawUrl.trim()) return ''
142
+ return endpointUrl(rawUrl, documentBaseUrl(document, sourceUrl), sourceUrl)
143
+ }
144
+
135
145
  function operationExpectedPrice(operation) {
136
146
  const price = operation?.['x-payment-info']?.price
137
147
  ?? operation?.['x-payment']?.price
@@ -141,6 +151,79 @@ function operationExpectedPrice(operation) {
141
151
  return numeric === null ? null : numeric
142
152
  }
143
153
 
154
+ function exampleValue(schemaOrParameter) {
155
+ if (!schemaOrParameter || typeof schemaOrParameter !== 'object') return undefined
156
+ const schema = schemaOrParameter.schema ?? schemaOrParameter
157
+ const value = schemaOrParameter.example
158
+ ?? schema.example
159
+ ?? schema.default
160
+ ?? (Array.isArray(schema.enum) ? schema.enum[0] : undefined)
161
+ if (value !== undefined) return value
162
+ if (schema.type === 'string') return ''
163
+ if (schema.type === 'number' || schema.type === 'integer') return 0
164
+ if (schema.type === 'boolean') return false
165
+ return undefined
166
+ }
167
+
168
+ function mediaExample(media) {
169
+ if (!media || typeof media !== 'object') return undefined
170
+ if (media.example !== undefined) return media.example
171
+ const examples = media.examples && typeof media.examples === 'object'
172
+ ? Object.values(media.examples)
173
+ : []
174
+ const firstExample = examples.find(Boolean)
175
+ if (firstExample?.value !== undefined) return firstExample.value
176
+ if (firstExample?.externalValue) return undefined
177
+
178
+ const schema = media.schema
179
+ if (!schema || typeof schema !== 'object' || schema.type !== 'object') return undefined
180
+ const body = {}
181
+ const properties = schema.properties && typeof schema.properties === 'object'
182
+ ? schema.properties
183
+ : {}
184
+ const required = new Set(Array.isArray(schema.required) ? schema.required : Object.keys(properties))
185
+
186
+ for (const [name, property] of Object.entries(properties)) {
187
+ if (!required.has(name)) continue
188
+ const value = exampleValue(property)
189
+ if (value !== undefined) body[name] = value
190
+ }
191
+
192
+ return Object.keys(body).length ? body : undefined
193
+ }
194
+
195
+ function operationRequestBody(operation) {
196
+ const content = operation?.requestBody?.content
197
+ if (!content || typeof content !== 'object') return undefined
198
+ const media = content['application/json']
199
+ ?? content['application/*+json']
200
+ ?? Object.entries(content).find(([type]) => /json/i.test(type))?.[1]
201
+ return mediaExample(media)
202
+ }
203
+
204
+ function openApiProbeUrl(path, operation, baseUrl) {
205
+ const parameters = Array.isArray(operation?.parameters) ? operation.parameters : []
206
+ let resolvedPath = path
207
+ const searchParams = new URLSearchParams()
208
+
209
+ for (const parameter of parameters) {
210
+ const value = exampleValue(parameter)
211
+ if (value === undefined || value === '') continue
212
+ if (parameter.in === 'path') {
213
+ resolvedPath = resolvedPath.replaceAll(`{${parameter.name}}`, encodeURIComponent(String(value)))
214
+ }
215
+ else if (parameter.in === 'query') {
216
+ searchParams.set(parameter.name, String(value))
217
+ }
218
+ }
219
+
220
+ const url = path.startsWith('http') ? new URL(resolvedPath) : new URL(resolvedPath, baseUrl)
221
+ for (const [name, value] of searchParams.entries()) {
222
+ url.searchParams.set(name, value)
223
+ }
224
+ return url.toString()
225
+ }
226
+
144
227
  function endpointEntries(document, sourceUrl, limit) {
145
228
  const entries = []
146
229
  const baseUrl = documentBaseUrl(document, sourceUrl)
@@ -198,12 +281,13 @@ function endpointEntries(document, sourceUrl, limit) {
198
281
  for (const method of methods) {
199
282
  const operation = operations[method]
200
283
  if (!operation || typeof operation !== 'object') continue
201
- const url = path.startsWith('http') ? path : new URL(path, baseUrl).toString()
284
+ const url = openApiProbeUrl(path, operation, baseUrl)
202
285
  entries.push({
203
286
  name: operation.operationId ?? `${method.toUpperCase()} ${path}`,
204
287
  url,
205
288
  method: method.toUpperCase(),
206
289
  expectedPriceUsd: operationExpectedPrice(operation),
290
+ requestBody: operationRequestBody(operation),
207
291
  })
208
292
  }
209
293
  }
@@ -338,7 +422,9 @@ async function probeEndpoint(entry) {
338
422
  accept: 'application/json',
339
423
  'content-type': 'application/json',
340
424
  },
341
- body: method === 'GET' || method === 'HEAD' ? undefined : '{}',
425
+ body: method === 'GET' || method === 'HEAD'
426
+ ? undefined
427
+ : JSON.stringify(entry.requestBody ?? {}),
342
428
  })
343
429
  const body = await readText(response)
344
430
  const headerChallenge = parseEncodedChallenge(
@@ -386,7 +472,7 @@ async function probePreflight(entry, origin) {
386
472
  }
387
473
 
388
474
  function valueList(value) {
389
- if (Array.isArray(value)) return value.map(String)
475
+ if (Array.isArray(value)) return value.map(displayMetadataValue)
390
476
  if (value && typeof value === 'object') return Object.keys(value)
391
477
  if (typeof value === 'string') return [value]
392
478
  return []
@@ -620,6 +706,7 @@ function formatMarkdown(report) {
620
706
  return [
621
707
  '# x402 Public Surface Check',
622
708
  '',
709
+ ...(report.sourceDocument ? [`Source: ${report.sourceDocument.url}`] : []),
623
710
  `Document: ${report.document.url}`,
624
711
  `Checked: ${report.checkedAt}`,
625
712
  'Scope: manifest/OpenAPI parsing, no-payment endpoint probes, and browser-style CORS preflight. No payment headers or paid calls.',
@@ -656,7 +743,8 @@ function formatMarkdown(report) {
656
743
  }
657
744
 
658
745
  async function runCheck(options) {
659
- const document = options.endpoint
746
+ let sourceDocument = null
747
+ let document = options.endpoint
660
748
  ? {
661
749
  status: 200,
662
750
  ok: true,
@@ -665,9 +753,25 @@ async function runCheck(options) {
665
753
  body: { text: '{}', json: {} },
666
754
  }
667
755
  : await fetchDocument(options.url)
668
- const entries = options.endpoint
756
+ let entries = options.endpoint
669
757
  ? [{ name: new URL(options.url).pathname.split('/').filter(Boolean).at(-1) ?? options.url, url: options.url, method: options.method || 'POST' }]
670
758
  : (document.body.json ? endpointEntries(document.body.json, document.url, options.limit) : [])
759
+
760
+ if (!options.endpoint && entries.length === 0 && document.body.json) {
761
+ const discoveryUrl = linkedDiscoveryUrl(document.body.json, document.url)
762
+ if (discoveryUrl) {
763
+ const followedDocument = await fetchDocument(discoveryUrl)
764
+ const followedEntries = followedDocument.body.json
765
+ ? endpointEntries(followedDocument.body.json, followedDocument.url, options.limit)
766
+ : []
767
+ if (followedEntries.length > 0) {
768
+ sourceDocument = document
769
+ document = followedDocument
770
+ entries = followedEntries
771
+ }
772
+ }
773
+ }
774
+
671
775
  const origin = options.origin ?? new URL(document.url).origin
672
776
  const challenges = []
673
777
  const preflights = []
@@ -686,6 +790,7 @@ async function runCheck(options) {
686
790
  origin,
687
791
  challenges,
688
792
  preflights,
793
+ sourceDocument,
689
794
  }
690
795
  report.findings = findingList(document, challenges, preflights, entries)
691
796
  return report
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x402-surface-check",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "description": "No-payment x402 public-surface checker for manifests, OpenAPI specs, and HTTP 402 challenges.",
5
5
  "type": "module",
6
6
  "bin": {