Skip to content

Commit 7b0f94d

Browse files
committed
Add grouped finding summary
1 parent d5c3156 commit 7b0f94d

5 files changed

Lines changed: 97 additions & 4 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ npx --yes x402-surface-check --endpoint --method POST --body '{"prompt":"price C
3030
- Testnet or staging rails such as Base Sepolia and Solana devnet
3131
- HTTPS resource URLs and stable resource metadata
3232
- Browser CORS allowance for the requesting origin and `X-PAYMENT`, including the actual 402 challenge response
33+
- Grouped finding summaries for repeated route-wide issues, so large manifests keep the patch order readable
3334
- Over-broad public method surfaces
3435
- Auth, validation, and free/trial responses that appear before a payment challenge, without piling on missing-field findings when no challenge was actually returned
3536
- Operational health/status endpoints, without treating expected free health checks as paid-route failures

bin/x402-surface-check.mjs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,45 @@ function findingList(documentResult, challengeResults, preflightResults, entries
814814
return findings
815815
}
816816

817+
function groupedFindingLabel(finding) {
818+
if (/402 challenge response does not allow the requesting origin/.test(finding)) {
819+
return 'P1 - Actual 402 challenge responses do not allow the requesting origin; browser clients cannot read payment requirements.'
820+
}
821+
if (/CORS preflight does not allow the requesting origin/.test(finding)) {
822+
return 'P1 - CORS preflight does not allow the requesting origin.'
823+
}
824+
if (/CORS preflight does not allow X-PAYMENT/.test(finding)) {
825+
return 'P1 - CORS preflight does not allow X-PAYMENT.'
826+
}
827+
if (/challenge does not repeat the resource URL/.test(finding)) {
828+
return 'P2 - Challenge accept legs do not repeat the resource URL for reconciliation.'
829+
}
830+
if (/returned validation HTTP \d+ before a payment challenge/.test(finding)) {
831+
return 'P1 - Routes return validation before a payment challenge.'
832+
}
833+
if (/returned auth HTTP \d+ before a payment challenge/.test(finding)) {
834+
return 'P2 - Routes return auth before a payment challenge.'
835+
}
836+
if (/challenge advertises staging\/test network/.test(finding)) {
837+
return 'P2 - Challenges advertise staging or test networks.'
838+
}
839+
if (/challenge advertises placeholder-looking payTo/.test(finding)) {
840+
return 'P1 - Challenges advertise placeholder-looking payTo recipients.'
841+
}
842+
return null
843+
}
844+
845+
function groupedFindingSummary(findings) {
846+
const counts = new Map()
847+
for (const finding of findings) {
848+
const label = groupedFindingLabel(finding)
849+
if (label) counts.set(label, (counts.get(label) ?? 0) + 1)
850+
}
851+
return [...counts.entries()]
852+
.filter(([, count]) => count > 1)
853+
.map(([label, count]) => `- ${count} endpoints: ${label}`)
854+
}
855+
817856
function formatMarkdown(report) {
818857
const document = report.document.body.json ?? {}
819858
const challengeRows = report.challenges.map(result => {
@@ -823,6 +862,7 @@ function formatMarkdown(report) {
823862
const preflightRows = report.preflights.map(result => {
824863
return `| ${result.name} | ${result.method ?? 'POST'} | ${result.status} | ${result.headers['access-control-allow-origin'] ?? '-'} | ${result.headers['access-control-allow-headers'] ?? '-'} | ${result.headers['access-control-allow-methods'] ?? '-'} |`
825864
})
865+
const findingSummary = groupedFindingSummary(report.findings)
826866

827867
return [
828868
'# x402 Public Surface Check',
@@ -856,6 +896,12 @@ function formatMarkdown(report) {
856896
'| --- | --- | --- | --- | --- | --- |',
857897
...(preflightRows.length ? preflightRows : ['| - | - | - | - | - | - |']),
858898
'',
899+
...(findingSummary.length ? [
900+
'## Finding Summary',
901+
'',
902+
...findingSummary,
903+
'',
904+
] : []),
859905
'## Findings',
860906
'',
861907
...(report.findings.length ? report.findings.map(item => `- ${item}`) : ['- No obvious launch-readiness findings from the public no-payment probes.']),

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.17",
3+
"version": "0.2.18",
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: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { strict as assert } from 'node:assert'
66
const execFileAsync = promisify(execFile)
77

88
const server = createServer((request, response) => {
9-
if (request.method === 'OPTIONS' && request.url === '/no-origin') {
9+
if (request.method === 'OPTIONS' && (request.url === '/no-origin' || request.url === '/no-origin-2')) {
1010
response.statusCode = 204
1111
response.setHeader('access-control-allow-headers', 'content-type,x-payment')
1212
response.setHeader('access-control-allow-methods', 'GET, OPTIONS')
@@ -267,6 +267,23 @@ const server = createServer((request, response) => {
267267
return
268268
}
269269

270+
if (request.url === '/no-origin-list.json') {
271+
response.setHeader('content-type', 'application/json')
272+
response.end(JSON.stringify({
273+
x402Version: 2,
274+
endpoints: [{
275+
path: '/no-origin',
276+
method: 'GET',
277+
description: 'No CORS fixture one',
278+
}, {
279+
path: '/no-origin-2',
280+
method: 'GET',
281+
description: 'No CORS fixture two',
282+
}],
283+
}))
284+
return
285+
}
286+
270287
if (request.url === '/well-known.json') {
271288
response.setHeader('content-type', 'application/json')
272289
response.end(JSON.stringify({
@@ -642,6 +659,24 @@ const server = createServer((request, response) => {
642659
return
643660
}
644661

662+
if (request.url === '/no-origin-2') {
663+
response.statusCode = 402
664+
response.setHeader('content-type', 'application/json')
665+
response.end(JSON.stringify({
666+
x402Version: 2,
667+
error: 'Payment required',
668+
resource: { url: `${serverUrl}/no-origin-2` },
669+
accepts: [{
670+
scheme: 'exact',
671+
network: 'eip155:8453',
672+
amount: '20000',
673+
asset: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
674+
payTo: '0x549c82e6bfc54bdae9a2073744cbc2af5d1fc6d1',
675+
}],
676+
}))
677+
return
678+
}
679+
645680
if (request.url === '/legacy/v1') {
646681
response.statusCode = 402
647682
response.setHeader('content-type', 'application/json')
@@ -922,6 +957,17 @@ try {
922957
assert.match(noOrigin.stdout, /CORS preflight does not allow the requesting origin/)
923958
assert.match(noOrigin.stdout, /402 challenge response does not allow the requesting origin/)
924959

960+
const groupedNoOrigin = await execFileAsync('node', [
961+
'bin/x402-surface-check.mjs',
962+
`${serverUrl}/no-origin-list.json`,
963+
'--origin',
964+
'https://example.com',
965+
], { cwd: new URL('..', import.meta.url) })
966+
967+
assert.match(groupedNoOrigin.stdout, /## Finding Summary/)
968+
assert.match(groupedNoOrigin.stdout, /2 endpoints: P1 - Actual 402 challenge responses do not allow the requesting origin/)
969+
assert.match(groupedNoOrigin.stdout, /2 endpoints: P1 - CORS preflight does not allow the requesting origin/)
970+
925971
const needsParam = await execFileAsync('node', [
926972
'bin/x402-surface-check.mjs',
927973
'--endpoint',

0 commit comments

Comments
 (0)