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 +5 -1
- package/bin/x402-surface-check.mjs +107 -15
- package/package.json +1 -1
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
|
|
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
|
|
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')
|
|
169
|
-
|
|
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
|
-
|
|
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) {
|