x402-surface-check 0.2.15 → 0.2.17

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
@@ -10,6 +10,7 @@ npm: https://www.npmjs.com/package/x402-surface-check
10
10
  npx --yes x402-surface-check https://api.example.com/.well-known/x402
11
11
  npx --yes x402-surface-check https://api.example.com/openapi.json report.md
12
12
  npx --yes x402-surface-check --endpoint --method POST https://x402.rpc.ankr.com/eth
13
+ npx --yes x402-surface-check --endpoint --method POST --body '{"prompt":"price CPI"}' https://api.example.com/paid-post
13
14
  ```
14
15
 
15
16
  ## What It Checks
@@ -17,7 +18,8 @@ npx --yes x402-surface-check --endpoint --method POST https://x402.rpc.ankr.com/
17
18
  - Manifest endpoint discovery from `items[]`, `endpoints[]`, `resources[]`, `x402Endpoints`, category arrays, resource strings, and OpenAPI paths
18
19
  - Linked discovery documents via `discovery_url`, `discoveryUrl`, `resources_url`, `resourcesUrl`, or manifest-level OpenAPI links
19
20
  - OpenAPI `servers[]` base-path preservation, so `/paths` are probed through the documented gateway rather than the domain root
20
- - OpenAPI query/path examples and JSON request-body examples for safer no-payment probes
21
+ - 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
22
+ - OpenAPI paid-operation prioritization, so docs and discovery routes do not consume the probe limit before payment-bearing operations
21
23
  - No-payment HTTP 402 challenge shape
22
24
  - x402 v1 and v2 price fields, including `accepts[]` and `schemes[]` challenge arrays
23
25
  - MPP `WWW-Authenticate: Payment` and x402 V2 `WWW-Authenticate: X402 requirements=...` challenges
@@ -57,6 +59,8 @@ x402-surface-check --endpoint --method POST <paid-endpoint-url> [output.md]
57
59
 
58
60
  --endpoint Treat the URL as one paid endpoint instead of a discovery document
59
61
  --method <verb> HTTP method for direct endpoint mode, default POST
62
+ --body <json> JSON request body for direct endpoint mode
63
+ --body-file <p> Read JSON request body for direct endpoint mode from a file
60
64
  --origin <url> Origin to use for browser-style CORS preflight
61
65
  --limit <n> Maximum endpoints to probe, default 6
62
66
  --json Print JSON instead of Markdown
@@ -18,6 +18,8 @@ Usage:
18
18
  Options:
19
19
  --endpoint Treat the URL as one paid endpoint instead of a discovery document
20
20
  --method <verb> HTTP method for direct endpoint mode, default POST
21
+ --body <json> JSON request body for direct endpoint mode
22
+ --body-file <p> Read JSON request body for direct endpoint mode from a file
21
23
  --origin <url> Origin to use for browser-style CORS preflight
22
24
  --limit <n> Maximum endpoints to probe, default ${defaultLimit}
23
25
  --json Print JSON instead of Markdown
@@ -33,6 +35,8 @@ function parseArgs(argv) {
33
35
  limit: Number(process.env.X402_CHECK_LIMIT ?? defaultLimit),
34
36
  method: 'POST',
35
37
  origin: process.env.X402_CHECK_ORIGIN,
38
+ body: process.env.X402_CHECK_BODY,
39
+ bodyFile: process.env.X402_CHECK_BODY_FILE,
36
40
  outputPath: '',
37
41
  url: '',
38
42
  }
@@ -55,6 +59,14 @@ function parseArgs(argv) {
55
59
  args.method = String(argv[index + 1] ?? '').toUpperCase()
56
60
  index += 1
57
61
  }
62
+ else if (arg === '--body') {
63
+ args.body = argv[index + 1]
64
+ index += 1
65
+ }
66
+ else if (arg === '--body-file') {
67
+ args.bodyFile = argv[index + 1]
68
+ index += 1
69
+ }
58
70
  else if (arg === '--origin') {
59
71
  args.origin = argv[index + 1]
60
72
  index += 1
@@ -77,6 +89,23 @@ function parseArgs(argv) {
77
89
  return args
78
90
  }
79
91
 
92
+ async function directEndpointRequestBody(options) {
93
+ if (!options.endpoint) return undefined
94
+ if (options.body && options.bodyFile) {
95
+ throw new Error('Use either --body or --body-file, not both.')
96
+ }
97
+ const raw = options.bodyFile
98
+ ? await readFile(options.bodyFile, 'utf8')
99
+ : options.body
100
+ if (!raw) return undefined
101
+ try {
102
+ return JSON.parse(raw)
103
+ }
104
+ catch (error) {
105
+ throw new Error(`Request body must be valid JSON: ${error.message}`)
106
+ }
107
+ }
108
+
80
109
  function moneyFromAtomic(amount, decimals = 6) {
81
110
  if (amount === '' || amount === null || amount === undefined) return ''
82
111
  const numeric = Number(amount)
@@ -157,21 +186,71 @@ function operationExpectedPrice(operation) {
157
186
  return numeric === null ? null : numeric
158
187
  }
159
188
 
160
- function exampleValue(schemaOrParameter) {
189
+ function resolveLocalRef(ref, document) {
190
+ if (typeof ref !== 'string' || !ref.startsWith('#/')) return undefined
191
+ return ref
192
+ .slice(2)
193
+ .split('/')
194
+ .map(part => part.replaceAll('~1', '/').replaceAll('~0', '~'))
195
+ .reduce((value, part) => value?.[part], document)
196
+ }
197
+
198
+ function resolveSchema(schema, document, seen = new Set()) {
199
+ if (!schema || typeof schema !== 'object') return schema
200
+ if (!schema.$ref) return schema
201
+ if (seen.has(schema.$ref)) return schema
202
+ const resolved = resolveLocalRef(schema.$ref, document)
203
+ if (!resolved) return schema
204
+ seen.add(schema.$ref)
205
+ return resolveSchema(resolved, document, seen)
206
+ }
207
+
208
+ function exampleValue(schemaOrParameter, document, depth = 0) {
161
209
  if (!schemaOrParameter || typeof schemaOrParameter !== 'object') return undefined
162
- const schema = schemaOrParameter.schema ?? schemaOrParameter
210
+ const schema = resolveSchema(schemaOrParameter.schema ?? schemaOrParameter, document)
211
+ const composite = schema.oneOf ?? schema.anyOf ?? schema.allOf
212
+ if (Array.isArray(composite) && composite.length > 0) {
213
+ return exampleValue(composite[0], document, depth + 1)
214
+ }
163
215
  const value = schemaOrParameter.example
216
+ ?? schema.const
164
217
  ?? schema.example
165
218
  ?? schema.default
166
219
  ?? (Array.isArray(schema.enum) ? schema.enum[0] : undefined)
167
220
  if (value !== undefined) return value
168
- if (schema.type === 'string') return ''
169
- if (schema.type === 'number' || schema.type === 'integer') return 0
221
+ if (schema.type === 'string') {
222
+ if (schema.format === 'uri') return 'https://example.com'
223
+ if (schema.format === 'date-time') return '2026-01-01T00:00:00.000Z'
224
+ if (schema.format === 'date') return '2026-01-01'
225
+ if (Number(schema.minLength) > 0) return 'example'
226
+ return ''
227
+ }
228
+ if (schema.type === 'integer') return Number.isFinite(Number(schema.minimum)) ? Number(schema.minimum) : 1
229
+ if (schema.type === 'number') return Number.isFinite(Number(schema.minimum)) ? Number(schema.minimum) : 1
170
230
  if (schema.type === 'boolean') return false
231
+ if (schema.type === 'array') {
232
+ if (depth > 4) return []
233
+ const item = exampleValue(schema.items ?? {}, document, depth + 1)
234
+ return item === undefined ? [] : [item]
235
+ }
236
+ if (schema.type === 'object') {
237
+ if (depth > 4) return {}
238
+ const properties = schema.properties && typeof schema.properties === 'object'
239
+ ? schema.properties
240
+ : {}
241
+ const required = new Set(Array.isArray(schema.required) ? schema.required : Object.keys(properties))
242
+ const result = {}
243
+ for (const [key, property] of Object.entries(properties)) {
244
+ if (!required.has(key)) continue
245
+ const nestedValue = exampleValue(property, document, depth + 1)
246
+ if (nestedValue !== undefined) result[key] = nestedValue
247
+ }
248
+ return result
249
+ }
171
250
  return undefined
172
251
  }
173
252
 
174
- function mediaExample(media) {
253
+ function mediaExample(media, document) {
175
254
  if (!media || typeof media !== 'object') return undefined
176
255
  if (media.example !== undefined) return media.example
177
256
  const examples = media.examples && typeof media.examples === 'object'
@@ -181,7 +260,7 @@ function mediaExample(media) {
181
260
  if (firstExample?.value !== undefined) return firstExample.value
182
261
  if (firstExample?.externalValue) return undefined
183
262
 
184
- const schema = media.schema
263
+ const schema = resolveSchema(media.schema, document)
185
264
  if (!schema || typeof schema !== 'object' || schema.type !== 'object') return undefined
186
265
  const body = {}
187
266
  const properties = schema.properties && typeof schema.properties === 'object'
@@ -191,29 +270,35 @@ function mediaExample(media) {
191
270
 
192
271
  for (const [name, property] of Object.entries(properties)) {
193
272
  if (!required.has(name)) continue
194
- const value = exampleValue(property)
273
+ const value = exampleValue(property, document)
195
274
  if (value !== undefined) body[name] = value
196
275
  }
197
276
 
198
277
  return Object.keys(body).length ? body : undefined
199
278
  }
200
279
 
201
- function operationRequestBody(operation) {
280
+ function operationRequestBody(operation, document) {
202
281
  const content = operation?.requestBody?.content
203
282
  if (!content || typeof content !== 'object') return undefined
204
283
  const media = content['application/json']
205
284
  ?? content['application/*+json']
206
285
  ?? Object.entries(content).find(([type]) => /json/i.test(type))?.[1]
207
- return mediaExample(media)
286
+ return mediaExample(media, document)
287
+ }
288
+
289
+ function operationPaymentSignal(operation) {
290
+ if (operation?.['x-payment-info'] || operation?.['x-payment'] || operation?.['x-x402'] || operation?.payment) return 2
291
+ if (operation?.responses && Object.hasOwn(operation.responses, '402')) return 1
292
+ return 0
208
293
  }
209
294
 
210
- function openApiProbeUrl(path, operation, baseUrl) {
295
+ function openApiProbeUrl(path, operation, baseUrl, document) {
211
296
  const parameters = Array.isArray(operation?.parameters) ? operation.parameters : []
212
297
  let resolvedPath = path
213
298
  const searchParams = new URLSearchParams()
214
299
 
215
300
  for (const parameter of parameters) {
216
- const value = exampleValue(parameter)
301
+ const value = exampleValue(parameter, document)
217
302
  if (value === undefined || value === '') continue
218
303
  if (parameter.in === 'path') {
219
304
  resolvedPath = resolvedPath.replaceAll(`{${parameter.name}}`, encodeURIComponent(String(value)))
@@ -282,22 +367,28 @@ function endpointEntries(document, sourceUrl, limit) {
282
367
 
283
368
  if (document.openapi && document.paths && typeof document.paths === 'object') {
284
369
  const baseUrl = openApiServerBaseUrl(document, sourceUrl)
370
+ const openApiEntries = []
285
371
 
286
372
  for (const [path, operations] of Object.entries(document.paths)) {
287
373
  if (!operations || typeof operations !== 'object') continue
288
374
  for (const method of methods) {
289
375
  const operation = operations[method]
290
376
  if (!operation || typeof operation !== 'object') continue
291
- const url = openApiProbeUrl(path, operation, baseUrl)
292
- entries.push({
377
+ const url = openApiProbeUrl(path, operation, baseUrl, document)
378
+ openApiEntries.push({
293
379
  name: operation.operationId ?? `${method.toUpperCase()} ${path}`,
294
380
  url,
295
381
  method: method.toUpperCase(),
296
382
  expectedPriceUsd: operationExpectedPrice(operation),
297
- requestBody: operationRequestBody(operation),
383
+ requestBody: operationRequestBody(operation, document),
384
+ paymentSignal: operationPaymentSignal(operation),
298
385
  })
299
386
  }
300
387
  }
388
+
389
+ entries.push(...openApiEntries
390
+ .sort((a, b) => b.paymentSignal - a.paymentSignal)
391
+ .map(({ paymentSignal, ...entry }) => entry))
301
392
  }
302
393
 
303
394
  for (const resource of document.resources ?? []) {
@@ -773,6 +864,7 @@ function formatMarkdown(report) {
773
864
  }
774
865
 
775
866
  async function runCheck(options) {
867
+ const directRequestBody = await directEndpointRequestBody(options)
776
868
  let sourceDocument = null
777
869
  let document = options.endpoint
778
870
  ? {
@@ -784,7 +876,7 @@ async function runCheck(options) {
784
876
  }
785
877
  : await fetchDocument(options.url)
786
878
  let entries = options.endpoint
787
- ? [{ name: new URL(options.url).pathname.split('/').filter(Boolean).at(-1) ?? options.url, url: options.url, method: options.method || 'POST' }]
879
+ ? [{ name: new URL(options.url).pathname.split('/').filter(Boolean).at(-1) ?? options.url, url: options.url, method: options.method || 'POST', requestBody: directRequestBody }]
788
880
  : (document.body.json ? endpointEntries(document.body.json, document.url, options.limit) : [])
789
881
 
790
882
  if (!options.endpoint && entries.length === 0 && document.body.json) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x402-surface-check",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
4
4
  "description": "No-payment x402 public-surface checker for manifests, OpenAPI specs, and HTTP 402 challenges.",
5
5
  "type": "module",
6
6
  "bin": {