x402-surface-check 0.2.12 → 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 +5 -3
- package/bin/x402-surface-check.mjs +80 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +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`, or
|
|
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
|
|
@@ -29,7 +30,7 @@ npx --yes x402-surface-check --endpoint --method POST https://x402.rpc.ankr.com/
|
|
|
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,8 @@ 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:
|
|
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
|
|
45
47
|
|
|
46
48
|
Field notes and browser version: https://tateprograms.com/x402-surface-check.html
|
|
47
49
|
|
|
@@ -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,12 +281,13 @@ 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 =
|
|
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
|
}
|
|
@@ -347,7 +422,9 @@ async function probeEndpoint(entry) {
|
|
|
347
422
|
accept: 'application/json',
|
|
348
423
|
'content-type': 'application/json',
|
|
349
424
|
},
|
|
350
|
-
body: method === 'GET' || method === 'HEAD'
|
|
425
|
+
body: method === 'GET' || method === 'HEAD'
|
|
426
|
+
? undefined
|
|
427
|
+
: JSON.stringify(entry.requestBody ?? {}),
|
|
351
428
|
})
|
|
352
429
|
const body = await readText(response)
|
|
353
430
|
const headerChallenge = parseEncodedChallenge(
|
|
@@ -395,7 +472,7 @@ async function probePreflight(entry, origin) {
|
|
|
395
472
|
}
|
|
396
473
|
|
|
397
474
|
function valueList(value) {
|
|
398
|
-
if (Array.isArray(value)) return value.map(
|
|
475
|
+
if (Array.isArray(value)) return value.map(displayMetadataValue)
|
|
399
476
|
if (value && typeof value === 'object') return Object.keys(value)
|
|
400
477
|
if (typeof value === 'string') return [value]
|
|
401
478
|
return []
|