x402-surface-check 0.2.32 → 0.2.34
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 +13 -1
- package/bin/x402-surface-check.mjs +41 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ It accepts an x402 manifest or OpenAPI URL, derives public endpoints, sends no-p
|
|
|
6
6
|
|
|
7
7
|
npm: https://www.npmjs.com/package/x402-surface-check
|
|
8
8
|
Attack-map field note: https://tateprograms.com/x402-attack-map-2026.html
|
|
9
|
+
Paid re-check or fix sprint: https://tateprograms.com/x402-fix-sprint.html
|
|
10
|
+
Machine-readable service catalog: https://tateprograms.com/services.json
|
|
9
11
|
|
|
10
12
|
```bash
|
|
11
13
|
npx --yes x402-surface-check https://api.example.com/.well-known/x402
|
|
@@ -36,7 +38,7 @@ npx --yes x402-surface-check --strict-proof https://api.example.com/openapi.json
|
|
|
36
38
|
- HTTPS resource URLs and stable resource metadata
|
|
37
39
|
- Resource binding across top-level `resource.url` and every accept leg, including localhost/private-development resource URLs that should not ship in production
|
|
38
40
|
- Timeout/expiry metadata on challenges, so payment capabilities have an explicit bounded freshness window
|
|
39
|
-
- Browser CORS allowance for the requesting origin
|
|
41
|
+
- Browser CORS allowance for the requesting origin, `X-PAYMENT`, and exposed challenge/session headers on the actual 402 response
|
|
40
42
|
- 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
|
|
41
43
|
- Optional strict proof/idempotency posture: mutating paid routes that do not advertise payment-identifier idempotency, and payment challenges that do not advertise signed offer/receipt evidence
|
|
42
44
|
- Payment-enforcement headers on `200` responses, so public telemetry/free-trial endpoints do not accidentally advertise enforced x402 while returning content before a challenge
|
|
@@ -65,6 +67,16 @@ Recent public no-payment checks have found and verified real launch fixes:
|
|
|
65
67
|
|
|
66
68
|
Field notes and browser version: https://tateprograms.com/x402-surface-check.html
|
|
67
69
|
|
|
70
|
+
## Paid Re-checks And Fix Sprints
|
|
71
|
+
|
|
72
|
+
If a public check finds a launch blocker and you want private follow-up, Tate Programs offers fixed-scope help:
|
|
73
|
+
|
|
74
|
+
- `$49` private re-check for one manifest, endpoint, OpenAPI file, or PR
|
|
75
|
+
- `$149` launch review with spend map and patch order
|
|
76
|
+
- `$299` small authorized fix sprint for one blocker such as browser-readable 402s, cache headers, canonical resource URLs, discovery docs, registry proof, or idempotency notes
|
|
77
|
+
|
|
78
|
+
Start with the scope page before paying: https://tateprograms.com/x402-fix-sprint.html
|
|
79
|
+
|
|
68
80
|
## Options
|
|
69
81
|
|
|
70
82
|
```bash
|
|
@@ -742,8 +742,10 @@ async function probeEndpoint(entry, origin) {
|
|
|
742
742
|
: JSON.stringify(entry.requestBody ?? {}),
|
|
743
743
|
})
|
|
744
744
|
const body = await readText(response)
|
|
745
|
+
const paymentRequiredHeader = response.headers.get('payment-required')
|
|
746
|
+
const xPaymentRequiredHeader = response.headers.get('x-payment-required')
|
|
745
747
|
const headerChallenge = parseEncodedChallenge(
|
|
746
|
-
|
|
748
|
+
paymentRequiredHeader ?? xPaymentRequiredHeader,
|
|
747
749
|
)
|
|
748
750
|
const authenticateChallenge = parsePaymentAuthenticate(response.headers.get('www-authenticate'))
|
|
749
751
|
?? parseX402Authenticate(response.headers.get('www-authenticate'))
|
|
@@ -766,6 +768,14 @@ async function probeEndpoint(entry, origin) {
|
|
|
766
768
|
status: response.status,
|
|
767
769
|
headers: Object.fromEntries(response.headers.entries()),
|
|
768
770
|
body,
|
|
771
|
+
bodyHadChallenge: bodyHasChallenge,
|
|
772
|
+
challengeHeaderName: paymentRequiredHeader
|
|
773
|
+
? 'payment-required'
|
|
774
|
+
: xPaymentRequiredHeader
|
|
775
|
+
? 'x-payment-required'
|
|
776
|
+
: authenticateChallenge
|
|
777
|
+
? 'www-authenticate'
|
|
778
|
+
: '',
|
|
769
779
|
}
|
|
770
780
|
}
|
|
771
781
|
|
|
@@ -1043,6 +1053,23 @@ function looksExplicitlyCacheable(headers = {}) {
|
|
|
1043
1053
|
return /\b(public|s-maxage|max-age\s*=)\b/i.test(policy)
|
|
1044
1054
|
}
|
|
1045
1055
|
|
|
1056
|
+
function headerListAllows(headerValue = '', requiredName = '') {
|
|
1057
|
+
if (!requiredName) return true
|
|
1058
|
+
const normalizedRequired = requiredName.toLowerCase()
|
|
1059
|
+
return String(headerValue).split(',')
|
|
1060
|
+
.map(item => item.trim().toLowerCase())
|
|
1061
|
+
.some(item => item === '*' || item === normalizedRequired)
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function missingExposedPaymentHeaders(result) {
|
|
1065
|
+
const expose = result.headers?.['access-control-expose-headers'] ?? ''
|
|
1066
|
+
const critical = [
|
|
1067
|
+
result.challengeHeaderName,
|
|
1068
|
+
result.headers?.['x-session-id'] !== undefined ? 'x-session-id' : '',
|
|
1069
|
+
].filter(Boolean)
|
|
1070
|
+
return critical.filter(name => !headerListAllows(expose, name))
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1046
1073
|
function entryKey(entry) {
|
|
1047
1074
|
return `${entry.method ?? 'POST'} ${entry.url}`
|
|
1048
1075
|
}
|
|
@@ -1121,6 +1148,19 @@ function findingList(documentResult, challengeResults, preflightResults, entries
|
|
|
1121
1148
|
if (!result.headers?.['access-control-allow-origin']) {
|
|
1122
1149
|
findings.push(`P1 - ${result.name} 402 challenge response does not allow the requesting origin; browser agents cannot read the payment requirements even if preflight succeeds.`)
|
|
1123
1150
|
}
|
|
1151
|
+
else {
|
|
1152
|
+
const hiddenHeaders = missingExposedPaymentHeaders(result)
|
|
1153
|
+
if (
|
|
1154
|
+
result.challengeHeaderName
|
|
1155
|
+
&& hiddenHeaders.includes(result.challengeHeaderName)
|
|
1156
|
+
&& !result.bodyHadChallenge
|
|
1157
|
+
) {
|
|
1158
|
+
findings.push(`P1 - ${result.name} carries payment requirements only in the ${result.challengeHeaderName} response header, but Access-Control-Expose-Headers does not expose it; browser agents cannot read the challenge after CORS succeeds.`)
|
|
1159
|
+
}
|
|
1160
|
+
if (hiddenHeaders.includes('x-session-id')) {
|
|
1161
|
+
findings.push(`P2 - ${result.name} returns x-session-id for the payment retry flow, but Access-Control-Expose-Headers does not expose it to browser agents.`)
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1124
1164
|
if (summary.resourceUrl.startsWith('http://') || summary.extraResource.startsWith('http://')) {
|
|
1125
1165
|
findings.push(`P1 - ${result.name} challenge uses a non-HTTPS resource URL: ${summary.resourceUrl || summary.extraResource}.`)
|
|
1126
1166
|
}
|