x402-surface-check 0.2.20 → 0.2.22
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 +45 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ 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
13
|
npx --yes x402-surface-check --endpoint --method POST --body '{"prompt":"price CPI"}' https://api.example.com/paid-post
|
|
14
|
+
npx --yes x402-surface-check --strict-cache https://api.example.com/openapi.json
|
|
14
15
|
```
|
|
15
16
|
|
|
16
17
|
## What It Checks
|
|
@@ -30,8 +31,9 @@ npx --yes x402-surface-check --endpoint --method POST --body '{"prompt":"price C
|
|
|
30
31
|
- Testnet or staging rails such as Base Sepolia and Solana devnet
|
|
31
32
|
- HTTPS resource URLs and stable resource metadata
|
|
32
33
|
- Browser CORS allowance for the requesting origin and `X-PAYMENT`, including the actual 402 challenge response
|
|
33
|
-
- Cache-Control posture on no-payment challenge responses, with warnings for explicitly cacheable payment gates
|
|
34
|
+
- Cache-Control posture on no-payment challenge responses, with warnings for explicitly cacheable payment gates and optional strict-cache findings for missing policy headers
|
|
34
35
|
- Grouped finding summaries for repeated route-wide issues, so large manifests keep the patch order readable
|
|
36
|
+
- Contextual reference guides for CORS, cache policy, Worker gates, resource echo, validation/auth ordering, and launch controls
|
|
35
37
|
- Over-broad public method surfaces
|
|
36
38
|
- Auth, validation, and free/trial responses that appear before a payment challenge, without piling on missing-field findings when no challenge was actually returned
|
|
37
39
|
- Operational health/status endpoints, without treating expected free health checks as paid-route failures
|
|
@@ -65,6 +67,7 @@ x402-surface-check --endpoint --method POST <paid-endpoint-url> [output.md]
|
|
|
65
67
|
--body-file <p> Read JSON request body for direct endpoint mode from a file
|
|
66
68
|
--origin <url> Origin to use for browser-style CORS preflight
|
|
67
69
|
--limit <n> Maximum endpoints to probe, default 6
|
|
70
|
+
--strict-cache Flag missing Cache-Control on no-payment 402 responses
|
|
68
71
|
--json Print JSON instead of Markdown
|
|
69
72
|
--help Show usage
|
|
70
73
|
--version Show package version
|
|
@@ -75,6 +78,7 @@ Environment variables are also supported:
|
|
|
75
78
|
```bash
|
|
76
79
|
X402_CHECK_ORIGIN=https://example.com x402-surface-check https://api.example.com/openapi.json
|
|
77
80
|
X402_CHECK_LIMIT=12 x402-surface-check https://api.example.com/.well-known/x402
|
|
81
|
+
X402_STRICT_CACHE=1 x402-surface-check https://api.example.com/.well-known/x402
|
|
78
82
|
```
|
|
79
83
|
|
|
80
84
|
## Scope
|
|
@@ -22,6 +22,7 @@ Options:
|
|
|
22
22
|
--body-file <p> Read JSON request body for direct endpoint mode from a file
|
|
23
23
|
--origin <url> Origin to use for browser-style CORS preflight
|
|
24
24
|
--limit <n> Maximum endpoints to probe, default ${defaultLimit}
|
|
25
|
+
--strict-cache Flag missing Cache-Control on no-payment 402 responses
|
|
25
26
|
--json Print JSON instead of Markdown
|
|
26
27
|
--help Show this help
|
|
27
28
|
--version Show package version
|
|
@@ -38,6 +39,7 @@ function parseArgs(argv) {
|
|
|
38
39
|
body: process.env.X402_CHECK_BODY,
|
|
39
40
|
bodyFile: process.env.X402_CHECK_BODY_FILE,
|
|
40
41
|
outputPath: '',
|
|
42
|
+
strictCache: process.env.X402_STRICT_CACHE === '1',
|
|
41
43
|
url: '',
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -55,6 +57,9 @@ function parseArgs(argv) {
|
|
|
55
57
|
else if (arg === '--endpoint') {
|
|
56
58
|
args.endpoint = true
|
|
57
59
|
}
|
|
60
|
+
else if (arg === '--strict-cache') {
|
|
61
|
+
args.strictCache = true
|
|
62
|
+
}
|
|
58
63
|
else if (arg === '--method') {
|
|
59
64
|
args.method = String(argv[index + 1] ?? '').toUpperCase()
|
|
60
65
|
index += 1
|
|
@@ -721,7 +726,7 @@ function looksLikeOperationalHealthEndpoint(result) {
|
|
|
721
726
|
return /(^|[/_\s-])(health|healthz|ready|readiness|live|liveness|status)([/_\s-]|$)/.test(value)
|
|
722
727
|
}
|
|
723
728
|
|
|
724
|
-
function findingList(documentResult, challengeResults, preflightResults, entries) {
|
|
729
|
+
function findingList(documentResult, challengeResults, preflightResults, entries, options = {}) {
|
|
725
730
|
const document = documentResult.body.json ?? {}
|
|
726
731
|
const findings = []
|
|
727
732
|
const networks = valueList(document.networks)
|
|
@@ -795,6 +800,9 @@ function findingList(documentResult, challengeResults, preflightResults, entries
|
|
|
795
800
|
if (looksExplicitlyCacheable(result.headers)) {
|
|
796
801
|
findings.push(`P2 - ${result.name} payment challenge response is explicitly cacheable (${cachePolicy(result.headers)}); paid routes should use no-store/private cache policy or bypass shared caches.`)
|
|
797
802
|
}
|
|
803
|
+
else if (options.strictCache && !cachePolicy(result.headers)) {
|
|
804
|
+
findings.push(`P3 - ${result.name} payment challenge response did not expose Cache-Control; for payment-gated routes, document or send no-store/private cache policy and confirm paid 200 responses are never shared-cacheable.`)
|
|
805
|
+
}
|
|
798
806
|
}
|
|
799
807
|
|
|
800
808
|
for (const result of preflightResults) {
|
|
@@ -853,6 +861,9 @@ function groupedFindingLabel(finding) {
|
|
|
853
861
|
if (/challenge advertises placeholder-looking payTo/.test(finding)) {
|
|
854
862
|
return 'P1 - Challenges advertise placeholder-looking payTo recipients.'
|
|
855
863
|
}
|
|
864
|
+
if (/payment challenge response did not expose Cache-Control/.test(finding)) {
|
|
865
|
+
return 'P3 - Payment challenge responses do not expose Cache-Control in strict cache mode.'
|
|
866
|
+
}
|
|
856
867
|
return null
|
|
857
868
|
}
|
|
858
869
|
|
|
@@ -867,6 +878,29 @@ function groupedFindingSummary(findings) {
|
|
|
867
878
|
.map(([label, count]) => `- ${count} endpoints: ${label}`)
|
|
868
879
|
}
|
|
869
880
|
|
|
881
|
+
function referenceGuides(findings) {
|
|
882
|
+
const guides = []
|
|
883
|
+
const add = (label, url) => {
|
|
884
|
+
if (!guides.some(guide => guide.url === url)) guides.push({ label, url })
|
|
885
|
+
}
|
|
886
|
+
const text = findings.join('\n')
|
|
887
|
+
if (/CORS|402 challenge response does not allow the requesting origin|X-PAYMENT/i.test(text)) {
|
|
888
|
+
add('x402 CORS Fix', 'https://tateprograms.com/x402-cors-fix.html')
|
|
889
|
+
add('Cloudflare x402 Worker Starter', 'https://tateprograms.com/cloudflare-x402-worker.html')
|
|
890
|
+
}
|
|
891
|
+
if (/cacheable|Cache-Control|cache policy|shared caches/i.test(text)) {
|
|
892
|
+
add('Cloudflare x402 Worker Starter', 'https://tateprograms.com/cloudflare-x402-worker.html')
|
|
893
|
+
add('x402 Attack Map 2026', 'https://tateprograms.com/x402-attack-map-2026.html')
|
|
894
|
+
}
|
|
895
|
+
if (/validation HTTP \d+ before a payment challenge|auth HTTP \d+ before a payment challenge|replay|idempotency/i.test(text)) {
|
|
896
|
+
add('x402 Launch Checklist', 'https://tateprograms.com/x402-launch-checklist.html')
|
|
897
|
+
}
|
|
898
|
+
if (/resource URL|resource echo|accepts\[0\]\.extra\.resource/i.test(text)) {
|
|
899
|
+
add('x402 Surface Check notes', 'https://tateprograms.com/x402-surface-check.html')
|
|
900
|
+
}
|
|
901
|
+
return guides.map(guide => `- ${guide.label}: ${guide.url}`)
|
|
902
|
+
}
|
|
903
|
+
|
|
870
904
|
function formatMarkdown(report) {
|
|
871
905
|
const document = report.document.body.json ?? {}
|
|
872
906
|
const challengeRows = report.challenges.map(result => {
|
|
@@ -880,6 +914,7 @@ function formatMarkdown(report) {
|
|
|
880
914
|
return `| ${result.name} | ${result.method ?? 'POST'} | ${result.status} | ${cachePolicy(result.headers) || '-'} |`
|
|
881
915
|
})
|
|
882
916
|
const findingSummary = groupedFindingSummary(report.findings)
|
|
917
|
+
const guides = referenceGuides(report.findings)
|
|
883
918
|
|
|
884
919
|
return [
|
|
885
920
|
'# x402 Public Surface Check',
|
|
@@ -929,6 +964,12 @@ function formatMarkdown(report) {
|
|
|
929
964
|
'',
|
|
930
965
|
...(report.findings.length ? report.findings.map(item => `- ${item}`) : ['- No obvious launch-readiness findings from the public no-payment probes.']),
|
|
931
966
|
'',
|
|
967
|
+
...(guides.length ? [
|
|
968
|
+
'## Reference Guides',
|
|
969
|
+
'',
|
|
970
|
+
...guides,
|
|
971
|
+
'',
|
|
972
|
+
] : []),
|
|
932
973
|
].join('\n')
|
|
933
974
|
}
|
|
934
975
|
|
|
@@ -983,7 +1024,9 @@ async function runCheck(options) {
|
|
|
983
1024
|
preflights,
|
|
984
1025
|
sourceDocument,
|
|
985
1026
|
}
|
|
986
|
-
report.findings = findingList(document, challenges, preflights, entries
|
|
1027
|
+
report.findings = findingList(document, challenges, preflights, entries, {
|
|
1028
|
+
strictCache: options.strictCache,
|
|
1029
|
+
})
|
|
987
1030
|
return report
|
|
988
1031
|
}
|
|
989
1032
|
|