Skip to content

Commit 6882e19

Browse files
committed
Refine paid endpoint preflight findings
1 parent 008b669 commit 6882e19

5 files changed

Lines changed: 39 additions & 5 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ npx --yes x402-surface-check --endpoint --method POST https://x402.rpc.ankr.com/
2727
- Browser CORS allowance for `X-PAYMENT`
2828
- Over-broad public method surfaces
2929
- 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+
- Operational health/status endpoints, without treating expected free health checks as paid-route failures
3031
- Object-valued document metadata such as facilitator objects, without `[object Object]` report artifacts
3132

3233
## Public Proof
@@ -36,6 +37,8 @@ Recent public no-payment checks have found and verified real launch fixes:
3637
- TensorFeed: parameter-required premium routes moved behind canonical x402 V2 challenges, then verified clean. https://github.com/solana-foundation/pay-skills/pull/68#issuecomment-4455360068
3738
- x402jp: weather routes that returned 500 now return structured Base x402 challenges. https://github.com/solana-foundation/pay-skills/pull/58#issuecomment-4455401355
3839
- Spraay: resource echo and browser payment-header behavior verified clean. https://github.com/solana-foundation/pay-skills/pull/60#issuecomment-4455519760
40+
- 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+
- HYRE Agent: OpenAPI-declared prices found 10x below live 402 challenge prices. https://github.com/solana-foundation/pay-skills/pull/19#issuecomment-4455641258
3942
- Agent Trust Bench: live discovery URL and browser-compatibility notes for adversarial agent-payment resources. https://github.com/solana-foundation/pay-skills/pull/23#issuecomment-4455484414
4043

4144
Field notes and browser version: https://tateprograms.com/x402-surface-check.html

bin/x402-surface-check.mjs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -501,11 +501,21 @@ function looksLikePlaceholderPayTo(payTo) {
501501
return false
502502
}
503503

504+
function entryKey(entry) {
505+
return `${entry.method ?? 'POST'} ${entry.url}`
506+
}
507+
508+
function looksLikeOperationalHealthEndpoint(result) {
509+
const value = `${result.name ?? ''} ${new URL(result.url).pathname}`.toLowerCase()
510+
return /(^|[/_\s-])(health|healthz|ready|readiness|live|liveness|status)([/_\s-]|$)/.test(value)
511+
}
512+
504513
function findingList(documentResult, challengeResults, preflightResults, entries) {
505514
const document = documentResult.body.json ?? {}
506515
const findings = []
507516
const networks = valueList(document.networks)
508517
const challengeNetworks = new Set()
518+
const challengesByEntry = new Map(challengeResults.map(result => [entryKey(result), result]))
509519

510520
if (documentResult.status < 200 || documentResult.status >= 300) {
511521
findings.push(`P1 - Document returned HTTP ${documentResult.status}; expected a successful JSON response.`)
@@ -526,7 +536,9 @@ function findingList(documentResult, challengeResults, preflightResults, entries
526536

527537
if (result.status !== 402) {
528538
if (result.status >= 200 && result.status < 300) {
529-
findings.push(`P3 - ${result.name} returned ${result.status} without a payment challenge for a no-payment ${result.method ?? 'POST'} probe; document this as free/trial access or move the 402 challenge before content.`)
539+
if (!looksLikeOperationalHealthEndpoint(result)) {
540+
findings.push(`P3 - ${result.name} returned ${result.status} without a payment challenge for a no-payment ${result.method ?? 'POST'} probe; document this as free/trial access or move the 402 challenge before content.`)
541+
}
530542
}
531543
else if (result.status === 400 || result.status === 422) {
532544
findings.push(`P1 - ${result.name} returned validation HTTP ${result.status} before a payment challenge for a no-payment ${result.method ?? 'POST'} probe.`)
@@ -569,9 +581,14 @@ function findingList(documentResult, challengeResults, preflightResults, entries
569581
}
570582

571583
for (const result of preflightResults) {
584+
const challengeResult = challengesByEntry.get(entryKey(result))
585+
if (!challengeResult || !hasPaymentChallenge(challengeResult)) continue
572586
const allowed = result.headers['access-control-allow-headers'] ?? ''
573587
if (allowed !== '*' && !/x-payment/i.test(allowed)) {
574-
findings.push(`P1 - ${result.name} CORS preflight does not allow X-PAYMENT; observed allow headers: ${allowed || 'none'}.`)
588+
const observed = result.status >= 400
589+
? `HTTP ${result.status}; allow headers: ${allowed || 'none'}`
590+
: `allow headers: ${allowed || 'none'}`
591+
findings.push(`P1 - ${result.name} CORS preflight does not allow X-PAYMENT; observed ${observed}.`)
575592
}
576593
const allowedMethods = result.headers['access-control-allow-methods'] ?? ''
577594
if (/delete|put|patch/i.test(allowedMethods)) {

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "x402-surface-check",
3-
"version": "0.2.10",
3+
"version": "0.2.11",
44
"description": "No-payment x402 public-surface checker for manifests, OpenAPI specs, and HTTP 402 challenges.",
55
"type": "module",
66
"bin": {

test/smoke.mjs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ const server = createServer((request, response) => {
2222
info: { title: 'Fixture', version: '1.0.0' },
2323
servers: [{ url: serverUrl }],
2424
paths: {
25+
'/health': {
26+
get: {
27+
operationId: 'healthCheck',
28+
},
29+
},
2530
'/score/{wallet}': {
2631
get: {
2732
operationId: 'getScore',
@@ -102,6 +107,13 @@ const server = createServer((request, response) => {
102107
return
103108
}
104109

110+
if (request.url === '/health') {
111+
response.statusCode = 200
112+
response.setHeader('content-type', 'application/json')
113+
response.end(JSON.stringify({ ok: true }))
114+
return
115+
}
116+
105117
if (request.url === '/score/%7Bwallet%7D') {
106118
response.statusCode = 402
107119
response.setHeader('content-type', 'application/json')
@@ -309,6 +321,8 @@ try {
309321
assert.match(stdout, /getScore/)
310322
assert.match(stdout, /\$0\.001/)
311323
assert.match(stdout, /No obvious launch-readiness findings/)
324+
assert.doesNotMatch(stdout, /healthCheck returned 200 without a payment challenge/)
325+
assert.doesNotMatch(stdout, /healthCheck CORS preflight/)
312326

313327
const mismatch = await execFileAsync('node', [
314328
'bin/x402-surface-check.mjs',

0 commit comments

Comments
 (0)