x402-surface-check 0.2.12 → 0.2.14

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
18
- - Linked discovery documents via `discovery_url`, `discoveryUrl`, `resources_url`, or `resourcesUrl`
17
+ - Manifest endpoint discovery from `items[]`, `endpoints[]`, `resources[]`, `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
19
20
  - No-payment HTTP 402 challenge shape
20
21
  - x402 v1 and v2 price fields, including `accepts[]` and `schemes[]` challenge arrays
21
22
  - MPP `WWW-Authenticate: Payment` and x402 V2 `WWW-Authenticate: X402 requirements=...` challenges
@@ -25,11 +26,11 @@ npx --yes x402-surface-check --endpoint --method POST https://x402.rpc.ankr.com/
25
26
  - Placeholder recipients such as zero addresses and Solana system-program values
26
27
  - Testnet or staging rails such as Base Sepolia and Solana devnet
27
28
  - HTTPS resource URLs and stable resource metadata
28
- - Browser CORS allowance for `X-PAYMENT`
29
+ - Browser CORS allowance for the requesting origin and `X-PAYMENT`
29
30
  - Over-broad public method surfaces
30
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
31
32
  - Operational health/status endpoints, without treating expected free health checks as paid-route failures
32
- - 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
33
34
 
34
35
  ## Public Proof
35
36
 
@@ -41,7 +42,9 @@ Recent public no-payment checks have found and verified real launch fixes:
41
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
42
43
  - HYRE Agent: OpenAPI-declared prices found 10x below live 402 challenge prices. https://github.com/solana-foundation/pay-skills/pull/19#issuecomment-4455641258
43
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
44
- - 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
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
47
+ - 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
45
48
 
46
49
  Field notes and browser version: https://tateprograms.com/x402-surface-check.html
47
50
 
@@ -137,6 +137,7 @@ function linkedDiscoveryUrl(document, sourceUrl) {
137
137
  ?? document?.discoveryUrl
138
138
  ?? document?.resources_url
139
139
  ?? document?.resourcesUrl
140
+ ?? (/^(https?:\/\/|\/)/i.test(String(document?.openapi ?? '')) ? document.openapi : '')
140
141
  if (typeof rawUrl !== 'string' || !rawUrl.trim()) return ''
141
142
  return endpointUrl(rawUrl, documentBaseUrl(document, sourceUrl), sourceUrl)
142
143
  }
@@ -150,6 +151,79 @@ function operationExpectedPrice(operation) {
150
151
  return numeric === null ? null : numeric
151
152
  }
152
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
+
153
227
  function endpointEntries(document, sourceUrl, limit) {
154
228
  const entries = []
155
229
  const baseUrl = documentBaseUrl(document, sourceUrl)
@@ -207,29 +281,45 @@ function endpointEntries(document, sourceUrl, limit) {
207
281
  for (const method of methods) {
208
282
  const operation = operations[method]
209
283
  if (!operation || typeof operation !== 'object') continue
210
- const url = path.startsWith('http') ? path : new URL(path, baseUrl).toString()
284
+ const url = openApiProbeUrl(path, operation, baseUrl)
211
285
  entries.push({
212
286
  name: operation.operationId ?? `${method.toUpperCase()} ${path}`,
213
287
  url,
214
288
  method: method.toUpperCase(),
215
289
  expectedPriceUsd: operationExpectedPrice(operation),
290
+ requestBody: operationRequestBody(operation),
216
291
  })
217
292
  }
218
293
  }
219
294
  }
220
295
 
221
296
  for (const resource of document.resources ?? []) {
222
- if (typeof resource !== 'string') continue
223
- const match = resource.match(/^(GET|POST|PUT|PATCH|DELETE)\s+(\S+)/i)
224
- if (!match) continue
225
- const [, method, rawPath] = match
226
- const url = rawPath.startsWith('http')
227
- ? rawPath
228
- : new URL(rawPath, document.baseUrl ?? sourceUrl).toString()
297
+ if (typeof resource === 'string') {
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
+ entries.push({
305
+ name: rawPath.split('/').filter(Boolean).at(-1) ?? rawPath,
306
+ url,
307
+ method: method.toUpperCase(),
308
+ })
309
+ continue
310
+ }
311
+
312
+ if (!resource || typeof resource !== 'object') continue
313
+ const rawPath = resource.url ?? resource.endpoint ?? resource.resource ?? resource.path
314
+ if (!rawPath) continue
229
315
  entries.push({
230
- name: rawPath.split('/').filter(Boolean).at(-1) ?? rawPath,
231
- url,
232
- method: method.toUpperCase(),
316
+ name: resource.id
317
+ ?? resource.name
318
+ ?? resource.title
319
+ ?? String(resource.path ?? rawPath).split('/').filter(Boolean).at(-1)
320
+ ?? String(rawPath),
321
+ url: endpointUrl(rawPath, baseUrl, sourceUrl),
322
+ method: String(resource.method ?? 'GET').toUpperCase(),
233
323
  })
234
324
  }
235
325
 
@@ -347,7 +437,9 @@ async function probeEndpoint(entry) {
347
437
  accept: 'application/json',
348
438
  'content-type': 'application/json',
349
439
  },
350
- body: method === 'GET' || method === 'HEAD' ? undefined : '{}',
440
+ body: method === 'GET' || method === 'HEAD'
441
+ ? undefined
442
+ : JSON.stringify(entry.requestBody ?? {}),
351
443
  })
352
444
  const body = await readText(response)
353
445
  const headerChallenge = parseEncodedChallenge(
@@ -395,7 +487,7 @@ async function probePreflight(entry, origin) {
395
487
  }
396
488
 
397
489
  function valueList(value) {
398
- if (Array.isArray(value)) return value.map(String)
490
+ if (Array.isArray(value)) return value.map(displayMetadataValue)
399
491
  if (value && typeof value === 'object') return Object.keys(value)
400
492
  if (typeof value === 'string') return [value]
401
493
  return []
@@ -592,6 +684,10 @@ function findingList(documentResult, challengeResults, preflightResults, entries
592
684
  for (const result of preflightResults) {
593
685
  const challengeResult = challengesByEntry.get(entryKey(result))
594
686
  if (!challengeResult || !hasPaymentChallenge(challengeResult)) continue
687
+ const allowedOrigin = result.headers['access-control-allow-origin'] ?? ''
688
+ if (!allowedOrigin) {
689
+ findings.push(`P1 - ${result.name} CORS preflight does not allow the requesting origin; observed allow-origin: none.`)
690
+ }
595
691
  const allowed = result.headers['access-control-allow-headers'] ?? ''
596
692
  if (allowed !== '*' && !/x-payment/i.test(allowed)) {
597
693
  const observed = result.status >= 400
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x402-surface-check",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "No-payment x402 public-surface checker for manifests, OpenAPI specs, and HTTP 402 challenges.",
5
5
  "type": "module",
6
6
  "bin": {