x402-surface-check 0.2.26 → 0.2.27
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 +1 -0
- package/bin/x402-surface-check.mjs +56 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,7 @@ npx --yes x402-surface-check --strict-cache https://api.example.com/openapi.json
|
|
|
37
37
|
- Browser CORS allowance for the requesting origin and `X-PAYMENT`, including the actual 402 challenge response
|
|
38
38
|
- Cache-Control posture on no-payment challenge responses, with P1 warnings for explicitly cacheable payment gates and optional strict-cache findings for missing policy headers
|
|
39
39
|
- Payment-enforcement headers on `200` responses, so public telemetry/free-trial endpoints do not accidentally advertise enforced x402 while returning content before a challenge
|
|
40
|
+
- Credential-like query params and URL userinfo inside public registry/discovery endpoint URLs, reported with redacted values so provider API keys and tokens are not repeated in scan output
|
|
40
41
|
- Grouped finding summaries for repeated route-wide issues, so large manifests keep the patch order readable
|
|
41
42
|
- Contextual reference guides for CORS, cache policy, Worker gates, resource echo, validation/auth ordering, and the May 2026 x402 attack-control map
|
|
42
43
|
- Over-broad public method surfaces
|
|
@@ -811,6 +811,55 @@ function looksLikeLocalResourceUrl(value) {
|
|
|
811
811
|
}
|
|
812
812
|
}
|
|
813
813
|
|
|
814
|
+
const secretQueryParamPattern =
|
|
815
|
+
/^(?:access[_-]?token|api[_-]?key|auth|authorization|bearer|client[_-]?secret|code|key|password|private[_-]?key|secret|session|sig|signature|token|jwt)$/i
|
|
816
|
+
|
|
817
|
+
function redactedCredentialUrl(value) {
|
|
818
|
+
if (!/^https?:\/\//i.test(String(value ?? ''))) return null
|
|
819
|
+
try {
|
|
820
|
+
const url = new URL(value)
|
|
821
|
+
let changed = false
|
|
822
|
+
|
|
823
|
+
if (url.username || url.password) {
|
|
824
|
+
if (url.username) url.username = 'REDACTED'
|
|
825
|
+
if (url.password) url.password = 'REDACTED'
|
|
826
|
+
changed = true
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
for (const [name] of Array.from(url.searchParams.entries())) {
|
|
830
|
+
if (secretQueryParamPattern.test(name)) {
|
|
831
|
+
url.searchParams.set(name, 'REDACTED')
|
|
832
|
+
changed = true
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
return changed ? url.toString() : null
|
|
837
|
+
}
|
|
838
|
+
catch {
|
|
839
|
+
return null
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function publicUrlCredentialFindings(value, path = 'document', depth = 0) {
|
|
844
|
+
if (depth > 8 || value === null || value === undefined) return []
|
|
845
|
+
if (typeof value === 'string') {
|
|
846
|
+
const redacted = redactedCredentialUrl(value)
|
|
847
|
+
return redacted
|
|
848
|
+
? [`P2 - Public document exposes credential-like URL material at ${path}: ${redacted}. Move provider tokens, signatures, sessions, or API keys out of registry-visible endpoint URLs.`]
|
|
849
|
+
: []
|
|
850
|
+
}
|
|
851
|
+
if (Array.isArray(value)) {
|
|
852
|
+
return value.flatMap((item, index) => publicUrlCredentialFindings(item, `${path}[${index}]`, depth + 1))
|
|
853
|
+
}
|
|
854
|
+
if (typeof value === 'object') {
|
|
855
|
+
return Object.entries(value).flatMap(([key, item]) => {
|
|
856
|
+
const safeKey = /^[a-zA-Z_$][\w$-]*$/.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`
|
|
857
|
+
return publicUrlCredentialFindings(item, `${path}${safeKey}`, depth + 1)
|
|
858
|
+
})
|
|
859
|
+
}
|
|
860
|
+
return []
|
|
861
|
+
}
|
|
862
|
+
|
|
814
863
|
function cachePolicy(headers = {}) {
|
|
815
864
|
return headers['cache-control'] ?? headers.cacheControl ?? ''
|
|
816
865
|
}
|
|
@@ -863,6 +912,9 @@ function findingList(documentResult, challengeResults, preflightResults, entries
|
|
|
863
912
|
if (!documentResult.body.json) {
|
|
864
913
|
findings.push(`P1 - Document did not return parseable JSON; content begins: ${documentResult.body.text.slice(0, 80).replace(/\s+/g, ' ')}.`)
|
|
865
914
|
}
|
|
915
|
+
else {
|
|
916
|
+
findings.push(...publicUrlCredentialFindings(document))
|
|
917
|
+
}
|
|
866
918
|
|
|
867
919
|
if (entries.length === 0) {
|
|
868
920
|
findings.push('P1 - Document does not expose any manifest, OpenAPI, item, category, or resource endpoints for no-payment probes.')
|
|
@@ -1059,6 +1111,10 @@ function referenceGuides(findings) {
|
|
|
1059
1111
|
add('x402 Surface Check notes', 'https://tateprograms.com/x402-surface-check.html')
|
|
1060
1112
|
add('x402 Attack Map 2026', 'https://tateprograms.com/x402-attack-map-2026.html')
|
|
1061
1113
|
}
|
|
1114
|
+
if (/credential-like URL material|provider tokens|API keys|registry-visible endpoint URLs/i.test(text)) {
|
|
1115
|
+
add('x402 Metadata Filter', 'https://tateprograms.com/x402-metadata-filter.html')
|
|
1116
|
+
add('Agent Commerce Gate', 'https://tateprograms.com/agent-commerce-gate.html')
|
|
1117
|
+
}
|
|
1062
1118
|
return guides.map(guide => `- ${guide.label}: ${guide.url}`)
|
|
1063
1119
|
}
|
|
1064
1120
|
|