x402-surface-check 0.2.23 → 0.2.24
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 +2 -0
- package/bin/x402-surface-check.mjs +25 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,6 +33,7 @@ npx --yes x402-surface-check --strict-cache https://api.example.com/openapi.json
|
|
|
33
33
|
- HTTPS resource URLs and stable resource metadata
|
|
34
34
|
- Browser CORS allowance for the requesting origin and `X-PAYMENT`, including the actual 402 challenge response
|
|
35
35
|
- Cache-Control posture on no-payment challenge responses, with warnings for explicitly cacheable payment gates and optional strict-cache findings for missing policy headers
|
|
36
|
+
- Payment-enforcement headers on `200` responses, so public telemetry/free-trial endpoints do not accidentally advertise enforced x402 while returning content before a challenge
|
|
36
37
|
- Grouped finding summaries for repeated route-wide issues, so large manifests keep the patch order readable
|
|
37
38
|
- Contextual reference guides for CORS, cache policy, Worker gates, resource echo, validation/auth ordering, and the May 2026 x402 attack-control map
|
|
38
39
|
- Over-broad public method surfaces
|
|
@@ -51,6 +52,7 @@ Recent public no-payment checks have found and verified real launch fixes:
|
|
|
51
52
|
- HYRE Agent: OpenAPI-declared prices found 10x below live 402 challenge prices. https://github.com/solana-foundation/pay-skills/pull/19#issuecomment-4455641258
|
|
52
53
|
- anchor-x402: multi-rail x402 challenges verified, with browser preflight blockers isolated before merge. https://github.com/solana-foundation/pay-skills/pull/47#issuecomment-4455678163
|
|
53
54
|
- Agent Trust Bench: three no-payment passes converged on zero findings after discovery, browser preflight, cache, and resource-echo fixes. https://github.com/solana-foundation/pay-skills/pull/23#issuecomment-4467597309
|
|
55
|
+
- SolSentry: x402 stats endpoint flagged for returning `200` content while headers advertised payment enforcement and browser preflight omitted `X-PAYMENT`. https://github.com/solsentry/solsentry-app/issues/2
|
|
54
56
|
- Solrouter: private LLM inference route verified with HTTPS resource-binding and price-alignment notes. https://github.com/solana-foundation/pay-skills/pull/39#issuecomment-4455800060
|
|
55
57
|
- Tetrac: Solana market-data payment gates verified, with browser payment-header preflight blocker isolated. https://github.com/solana-foundation/pay-skills/pull/32#issuecomment-4455923744
|
|
56
58
|
|
|
@@ -710,6 +710,24 @@ function cachePolicy(headers = {}) {
|
|
|
710
710
|
return headers['cache-control'] ?? headers.cacheControl ?? ''
|
|
711
711
|
}
|
|
712
712
|
|
|
713
|
+
function paymentSignalHeaders(headers = {}) {
|
|
714
|
+
return [
|
|
715
|
+
'x-payment-required',
|
|
716
|
+
'x-payment-enforce',
|
|
717
|
+
'x-price-usdc',
|
|
718
|
+
'x-payment-address',
|
|
719
|
+
'x-payment-network',
|
|
720
|
+
'x-payment-token',
|
|
721
|
+
'x-payment-protocol',
|
|
722
|
+
].filter(name => headers[name] !== undefined && headers[name] !== '')
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function advertisesPaymentEnforcement(headers = {}) {
|
|
726
|
+
const required = String(headers['x-payment-required'] ?? '').toLowerCase() === 'true'
|
|
727
|
+
const enforced = String(headers['x-payment-enforce'] ?? '').toLowerCase() === 'true'
|
|
728
|
+
return required || enforced || (required && paymentSignalHeaders(headers).length > 1)
|
|
729
|
+
}
|
|
730
|
+
|
|
713
731
|
function looksExplicitlyCacheable(headers = {}) {
|
|
714
732
|
const policy = cachePolicy(headers)
|
|
715
733
|
if (!policy) return false
|
|
@@ -755,6 +773,9 @@ function findingList(documentResult, challengeResults, preflightResults, entries
|
|
|
755
773
|
if (!looksLikeOperationalHealthEndpoint(result)) {
|
|
756
774
|
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.`)
|
|
757
775
|
}
|
|
776
|
+
if (advertisesPaymentEnforcement(result.headers)) {
|
|
777
|
+
findings.push(`P2 - ${result.name} returned ${result.status} content while payment headers advertise enforcement (${paymentSignalHeaders(result.headers).join(', ')}); either return a 402 before content or document this endpoint as public telemetry.`)
|
|
778
|
+
}
|
|
758
779
|
}
|
|
759
780
|
else if (result.status === 400 || result.status === 422) {
|
|
760
781
|
findings.push(`P1 - ${result.name} returned validation HTTP ${result.status} before a payment challenge for a no-payment ${result.method ?? 'POST'} probe.`)
|
|
@@ -807,7 +828,7 @@ function findingList(documentResult, challengeResults, preflightResults, entries
|
|
|
807
828
|
|
|
808
829
|
for (const result of preflightResults) {
|
|
809
830
|
const challengeResult = challengesByEntry.get(entryKey(result))
|
|
810
|
-
if (!challengeResult || !hasPaymentChallenge(challengeResult)) continue
|
|
831
|
+
if (!challengeResult || (!hasPaymentChallenge(challengeResult) && !advertisesPaymentEnforcement(challengeResult.headers))) continue
|
|
811
832
|
const allowedOrigin = result.headers['access-control-allow-origin'] ?? ''
|
|
812
833
|
if (!allowedOrigin) {
|
|
813
834
|
findings.push(`P1 - ${result.name} CORS preflight does not allow the requesting origin; observed allow-origin: none.`)
|
|
@@ -864,6 +885,9 @@ function groupedFindingLabel(finding) {
|
|
|
864
885
|
if (/payment challenge response did not expose Cache-Control/.test(finding)) {
|
|
865
886
|
return 'P3 - Payment challenge responses do not expose Cache-Control in strict cache mode.'
|
|
866
887
|
}
|
|
888
|
+
if (/content while payment headers advertise enforcement/.test(finding)) {
|
|
889
|
+
return 'P2 - Payment headers advertise enforcement on a 200 response.'
|
|
890
|
+
}
|
|
867
891
|
return null
|
|
868
892
|
}
|
|
869
893
|
|