x402-surface-check 0.2.16 → 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,7 @@ 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, JSON request-body examples, and local `$ref` request schemas 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
21
22
  - OpenAPI paid-operation prioritization, so docs and discovery routes do not consume the probe limit before payment-bearing operations
22
23
  - No-payment HTTP 402 challenge shape
23
24
  - x402 v1 and v2 price fields, including `accepts[]` and `schemes[]` challenge arrays
@@ -58,6 +59,8 @@ x402-surface-check --endpoint --method POST <paid-endpoint-url> [output.md]
58
59
 
59
60
  --endpoint Treat the URL as one paid endpoint instead of a discovery document
60
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
61
64
  --origin <url> Origin to use for browser-style CORS preflight
62
65
  --limit <n> Maximum endpoints to probe, default 6
63
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)
@@ -176,9 +205,13 @@ function resolveSchema(schema, document, seen = new Set()) {
176
205
  return resolveSchema(resolved, document, seen)
177
206
  }
178
207
 
179
- function exampleValue(schemaOrParameter, document) {
208
+ function exampleValue(schemaOrParameter, document, depth = 0) {
180
209
  if (!schemaOrParameter || typeof schemaOrParameter !== 'object') return undefined
181
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
+ }
182
215
  const value = schemaOrParameter.example
183
216
  ?? schema.const
184
217
  ?? schema.example
@@ -195,6 +228,25 @@ function exampleValue(schemaOrParameter, document) {
195
228
  if (schema.type === 'integer') return Number.isFinite(Number(schema.minimum)) ? Number(schema.minimum) : 1
196
229
  if (schema.type === 'number') return Number.isFinite(Number(schema.minimum)) ? Number(schema.minimum) : 1
197
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
+ }
198
250
  return undefined
199
251
  }
200
252
 
@@ -812,6 +864,7 @@ function formatMarkdown(report) {
812
864
  }
813
865
 
814
866
  async function runCheck(options) {
867
+ const directRequestBody = await directEndpointRequestBody(options)
815
868
  let sourceDocument = null
816
869
  let document = options.endpoint
817
870
  ? {
@@ -823,7 +876,7 @@ async function runCheck(options) {
823
876
  }
824
877
  : await fetchDocument(options.url)
825
878
  let entries = options.endpoint
826
- ? [{ 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 }]
827
880
  : (document.body.json ? endpointEntries(document.body.json, document.url, options.limit) : [])
828
881
 
829
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.16",
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": {