x402-surface-check 0.2.11 → 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 +6 -2
- package/bin/x402-surface-check.mjs +110 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,6 +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`, `resourcesUrl`, or manifest-level OpenAPI links
|
|
19
|
+
- OpenAPI query/path examples and JSON request-body examples for safer no-payment probes
|
|
18
20
|
- No-payment HTTP 402 challenge shape
|
|
19
21
|
- x402 v1 and v2 price fields, including `accepts[]` and `schemes[]` challenge arrays
|
|
20
22
|
- MPP `WWW-Authenticate: Payment` and x402 V2 `WWW-Authenticate: X402 requirements=...` challenges
|
|
@@ -28,7 +30,7 @@ npx --yes x402-surface-check --endpoint --method POST https://x402.rpc.ankr.com/
|
|
|
28
30
|
- Over-broad public method surfaces
|
|
29
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
|
|
30
32
|
- Operational health/status endpoints, without treating expected free health checks as paid-route failures
|
|
31
|
-
- 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
|
|
32
34
|
|
|
33
35
|
## Public Proof
|
|
34
36
|
|
|
@@ -39,7 +41,9 @@ Recent public no-payment checks have found and verified real launch fixes:
|
|
|
39
41
|
- Spraay: resource echo and browser payment-header behavior verified clean. https://github.com/solana-foundation/pay-skills/pull/60#issuecomment-4455519760
|
|
40
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
|
|
41
43
|
- HYRE Agent: OpenAPI-declared prices found 10x below live 402 challenge prices. https://github.com/solana-foundation/pay-skills/pull/19#issuecomment-4455641258
|
|
42
|
-
-
|
|
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
|
|
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
|
|
43
47
|
|
|
44
48
|
Field notes and browser version: https://tateprograms.com/x402-surface-check.html
|
|
45
49
|
|
|
@@ -132,6 +132,16 @@ function endpointUrl(rawPath, baseUrl, sourceUrl) {
|
|
|
132
132
|
return new URL(value, base).toString()
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
function linkedDiscoveryUrl(document, sourceUrl) {
|
|
136
|
+
const rawUrl = document?.discovery_url
|
|
137
|
+
?? document?.discoveryUrl
|
|
138
|
+
?? document?.resources_url
|
|
139
|
+
?? document?.resourcesUrl
|
|
140
|
+
?? (/^(https?:\/\/|\/)/i.test(String(document?.openapi ?? '')) ? document.openapi : '')
|
|
141
|
+
if (typeof rawUrl !== 'string' || !rawUrl.trim()) return ''
|
|
142
|
+
return endpointUrl(rawUrl, documentBaseUrl(document, sourceUrl), sourceUrl)
|
|
143
|
+
}
|
|
144
|
+
|
|
135
145
|
function operationExpectedPrice(operation) {
|
|
136
146
|
const price = operation?.['x-payment-info']?.price
|
|
137
147
|
?? operation?.['x-payment']?.price
|
|
@@ -141,6 +151,79 @@ function operationExpectedPrice(operation) {
|
|
|
141
151
|
return numeric === null ? null : numeric
|
|
142
152
|
}
|
|
143
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
|
+
|
|
144
227
|
function endpointEntries(document, sourceUrl, limit) {
|
|
145
228
|
const entries = []
|
|
146
229
|
const baseUrl = documentBaseUrl(document, sourceUrl)
|
|
@@ -198,12 +281,13 @@ function endpointEntries(document, sourceUrl, limit) {
|
|
|
198
281
|
for (const method of methods) {
|
|
199
282
|
const operation = operations[method]
|
|
200
283
|
if (!operation || typeof operation !== 'object') continue
|
|
201
|
-
const url =
|
|
284
|
+
const url = openApiProbeUrl(path, operation, baseUrl)
|
|
202
285
|
entries.push({
|
|
203
286
|
name: operation.operationId ?? `${method.toUpperCase()} ${path}`,
|
|
204
287
|
url,
|
|
205
288
|
method: method.toUpperCase(),
|
|
206
289
|
expectedPriceUsd: operationExpectedPrice(operation),
|
|
290
|
+
requestBody: operationRequestBody(operation),
|
|
207
291
|
})
|
|
208
292
|
}
|
|
209
293
|
}
|
|
@@ -338,7 +422,9 @@ async function probeEndpoint(entry) {
|
|
|
338
422
|
accept: 'application/json',
|
|
339
423
|
'content-type': 'application/json',
|
|
340
424
|
},
|
|
341
|
-
body: method === 'GET' || method === 'HEAD'
|
|
425
|
+
body: method === 'GET' || method === 'HEAD'
|
|
426
|
+
? undefined
|
|
427
|
+
: JSON.stringify(entry.requestBody ?? {}),
|
|
342
428
|
})
|
|
343
429
|
const body = await readText(response)
|
|
344
430
|
const headerChallenge = parseEncodedChallenge(
|
|
@@ -386,7 +472,7 @@ async function probePreflight(entry, origin) {
|
|
|
386
472
|
}
|
|
387
473
|
|
|
388
474
|
function valueList(value) {
|
|
389
|
-
if (Array.isArray(value)) return value.map(
|
|
475
|
+
if (Array.isArray(value)) return value.map(displayMetadataValue)
|
|
390
476
|
if (value && typeof value === 'object') return Object.keys(value)
|
|
391
477
|
if (typeof value === 'string') return [value]
|
|
392
478
|
return []
|
|
@@ -620,6 +706,7 @@ function formatMarkdown(report) {
|
|
|
620
706
|
return [
|
|
621
707
|
'# x402 Public Surface Check',
|
|
622
708
|
'',
|
|
709
|
+
...(report.sourceDocument ? [`Source: ${report.sourceDocument.url}`] : []),
|
|
623
710
|
`Document: ${report.document.url}`,
|
|
624
711
|
`Checked: ${report.checkedAt}`,
|
|
625
712
|
'Scope: manifest/OpenAPI parsing, no-payment endpoint probes, and browser-style CORS preflight. No payment headers or paid calls.',
|
|
@@ -656,7 +743,8 @@ function formatMarkdown(report) {
|
|
|
656
743
|
}
|
|
657
744
|
|
|
658
745
|
async function runCheck(options) {
|
|
659
|
-
|
|
746
|
+
let sourceDocument = null
|
|
747
|
+
let document = options.endpoint
|
|
660
748
|
? {
|
|
661
749
|
status: 200,
|
|
662
750
|
ok: true,
|
|
@@ -665,9 +753,25 @@ async function runCheck(options) {
|
|
|
665
753
|
body: { text: '{}', json: {} },
|
|
666
754
|
}
|
|
667
755
|
: await fetchDocument(options.url)
|
|
668
|
-
|
|
756
|
+
let entries = options.endpoint
|
|
669
757
|
? [{ name: new URL(options.url).pathname.split('/').filter(Boolean).at(-1) ?? options.url, url: options.url, method: options.method || 'POST' }]
|
|
670
758
|
: (document.body.json ? endpointEntries(document.body.json, document.url, options.limit) : [])
|
|
759
|
+
|
|
760
|
+
if (!options.endpoint && entries.length === 0 && document.body.json) {
|
|
761
|
+
const discoveryUrl = linkedDiscoveryUrl(document.body.json, document.url)
|
|
762
|
+
if (discoveryUrl) {
|
|
763
|
+
const followedDocument = await fetchDocument(discoveryUrl)
|
|
764
|
+
const followedEntries = followedDocument.body.json
|
|
765
|
+
? endpointEntries(followedDocument.body.json, followedDocument.url, options.limit)
|
|
766
|
+
: []
|
|
767
|
+
if (followedEntries.length > 0) {
|
|
768
|
+
sourceDocument = document
|
|
769
|
+
document = followedDocument
|
|
770
|
+
entries = followedEntries
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
671
775
|
const origin = options.origin ?? new URL(document.url).origin
|
|
672
776
|
const challenges = []
|
|
673
777
|
const preflights = []
|
|
@@ -686,6 +790,7 @@ async function runCheck(options) {
|
|
|
686
790
|
origin,
|
|
687
791
|
challenges,
|
|
688
792
|
preflights,
|
|
793
|
+
sourceDocument,
|
|
689
794
|
}
|
|
690
795
|
report.findings = findingList(document, challenges, preflights, entries)
|
|
691
796
|
return report
|