Skip to content

Commit 0f331ed

Browse files
committed
Limit VPN traffic instead of hard blocking
1 parent 229d03b commit 0f331ed

4 files changed

Lines changed: 45 additions & 24 deletions

File tree

docs/environment-variables.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
- Runtime/OS env: pass typed snapshots instead of reading `process.env` throughout the codebase.
88
- `IPINFO_TOKEN` is required; free-mode country gating uses it to check IPinfo privacy signals for VPN/proxy/Tor/relay/hosting traffic.
99
- `SPUR_TOKEN` is required; VPN/proxy/Tor/residential-proxy privacy signals use Spur Context API corroboration.
10-
- `SCAMALYTICS_API_KEY` is required; when IPinfo reports privacy or hosting/service signals, free-mode gating also checks Scamalytics for a fraud score and proxy/Tor/VPN evidence. In allowlisted countries, full access requires both Spur and Scamalytics to return clean follow-up results. Provider failures or ambiguous results fall back to limited access, and only Cloudflare Tor or strongly corroborated high-risk abuse is blocked entirely.
10+
- `SCAMALYTICS_API_KEY` is required; when IPinfo reports privacy or hosting/service signals, free-mode gating also checks Scamalytics for a fraud score and proxy/Tor/VPN evidence. In allowlisted countries, full access requires both Spur and Scamalytics to return clean follow-up results. Provider failures, ambiguous results, VPN/proxy/residential-proxy signals, and hosting/datacenter signals fall back to limited access. Only Cloudflare Tor or Tor corroborated by another provider is blocked entirely by the IP-intelligence gate.
1111
- `CODEBUFF_FULL_TELEMETRY=true` or `CODEBUFF_FULL_TELEMETRY_IDS=user-id,email@example.com`
1212
disables client analytics sampling for targeted debugging. Use sparingly because it can send full CLI log payloads.
1313

docs/freebuff-waiting-room.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ All endpoints authenticate via the standard `Authorization: Bearer <api-key>` or
181181
- Existing active+unexpired row, **different model** → reject with `model_locked` (HTTP 409); `active_instance_id` is **not** rotated so the other CLI stays valid. Client must DELETE the session before switching.
182182
- Existing active+expired row → reset to queued with fresh `queued_at` and the requested `model` (re-queue at back).
183183

184-
Before any of those state transitions, the handler requires a resolved country and IPinfo privacy classification. Unsupported countries enter limited Freebuff access. In allowlisted countries, IPinfo privacy/hosting/service signals trigger paid follow-up checks with Spur and Scamalytics. Full access is restored only when both follow-up providers return clean context; suspicious or failed follow-up checks fall back to limited access. The server records a 0-100 privacy risk score for observability/cache rows; named/recent IPinfo anonymizer observations raise that score, while generic Scamalytics third-party proxy labels do not override a low top-level Scamalytics score by themselves. Cloudflare Tor country detection and strongly corroborated high-risk VPN/proxy/Tor abuse remain hard blocks.
184+
Before any of those state transitions, the handler requires a resolved country and IPinfo privacy classification. Unsupported countries enter limited Freebuff access. In allowlisted countries, IPinfo privacy/hosting/service signals trigger paid follow-up checks with Spur and Scamalytics. Full access is restored only when both follow-up providers return clean context; suspicious or failed follow-up checks fall back to limited access. The server records a 0-100 privacy risk score for observability/cache rows; named/recent IPinfo anonymizer observations raise that score, while generic Scamalytics third-party proxy labels do not override a low top-level Scamalytics score by themselves. VPN, proxy, residential proxy, and hosting/datacenter signals limit access by default; only Cloudflare Tor country detection or Tor corroborated by another provider is hard-blocked by the IP-intelligence gate.
185185

186186
Response shapes:
187187

web/src/server/__tests__/free-mode-country.test.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ describe('free mode country access', () => {
320320
expect(shouldHardBlockFreeModeAccess(access)).toBe(false)
321321
})
322322

323-
test('hard-blocks high-confidence abuse when all providers corroborate hard signals', async () => {
323+
test('keeps corroborated high-score VPN/proxy traffic limited', async () => {
324324
const access = await getFreeModeCountryAccess(
325325
makeReq({
326326
'cf-ipcountry': 'US',
@@ -345,7 +345,40 @@ describe('free mode country access', () => {
345345

346346
expect(access.allowed).toBe(false)
347347
expect(access.blockReason).toBe('anonymous_network')
348-
expect(getFreeModeRiskScore(access)).toBe(95)
348+
expect(getFreeModeRiskScore(access)).toBe(90)
349+
expect(getFreeModePrivacyDecision(access)).toBe(
350+
'scamalytics_suspicious_limited',
351+
)
352+
expect(shouldHardBlockFreeModeAccess(access)).toBe(false)
353+
})
354+
355+
test('hard-blocks Tor when corroborated by another provider', async () => {
356+
const access = await getFreeModeCountryAccess(
357+
makeReq({
358+
'cf-ipcountry': 'US',
359+
'x-forwarded-for': '203.0.113.10',
360+
}),
361+
{
362+
ipinfoToken: 'test-token',
363+
spurToken: 'test-spur-token',
364+
lookupIpPrivacy: async () => ({
365+
signals: ['tor'],
366+
}),
367+
lookupSpurIpPrivacy: async () => ({
368+
signals: ['vpn'],
369+
}),
370+
lookupScamalyticsIpRisk: async () => ({
371+
signals: ['tor'],
372+
score: 90,
373+
risk: 'very high',
374+
}),
375+
},
376+
)
377+
378+
expect(access.allowed).toBe(false)
379+
expect(access.blockReason).toBe('anonymous_network')
380+
expect(getFreeModeRiskScore(access)).toBe(100)
381+
expect(getFreeModePrivacyDecision(access)).toBe('corroborated_block')
349382
expect(shouldHardBlockFreeModeAccess(access)).toBe(true)
350383
})
351384

web/src/server/free-mode-country.ts

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,6 @@ const scamalyticsPrivacyCache = new Map<
150150
>()
151151

152152
const SCAMALYTICS_DEFAULT_USER = 'codebuff'
153-
export const SCAMALYTICS_HIGH_RISK_SCORE = 75
154153
export const SCAMALYTICS_LIMITED_RISK_SCORE = 50
155154

156155
const FREE_MODE_LIMITED_PRIVACY_SIGNALS = new Set<FreeModeIpPrivacySignal>([
@@ -173,22 +172,18 @@ function hasTorPrivacySignal(
173172
return ipPrivacy?.signals.includes('tor') ?? false
174173
}
175174

176-
function hasStrongCorroboratedAbuse(
175+
function hasCorroboratedTorSignal(
177176
countryAccess: Partial<
178177
Pick<
179178
FreeModeCountryAccess,
180-
| 'ipPrivacy'
181-
| 'spurIpPrivacy'
182-
| 'scamalyticsIpPrivacy'
183-
| 'scamalyticsScore'
179+
'ipPrivacy' | 'spurIpPrivacy' | 'scamalyticsIpPrivacy'
184180
>
185181
>,
186182
): boolean {
187183
return (
188-
hasHardBlockedPrivacySignal(countryAccess.ipPrivacy) &&
189-
hasHardBlockedPrivacySignal(countryAccess.spurIpPrivacy) &&
190-
(hasHardBlockedPrivacySignal(countryAccess.scamalyticsIpPrivacy) ||
191-
(countryAccess.scamalyticsScore ?? 0) >= SCAMALYTICS_HIGH_RISK_SCORE)
184+
hasTorPrivacySignal(countryAccess.ipPrivacy) &&
185+
(hasTorPrivacySignal(countryAccess.spurIpPrivacy) ||
186+
hasTorPrivacySignal(countryAccess.scamalyticsIpPrivacy))
192187
)
193188
}
194189

@@ -282,7 +277,7 @@ export function getFreeModeRiskScore(
282277
if (typeof countryAccess.scamalyticsScore === 'number') {
283278
score = Math.max(score, countryAccess.scamalyticsScore)
284279
}
285-
if (hasStrongCorroboratedAbuse(countryAccess)) {
280+
if (hasCorroboratedTorSignal(countryAccess)) {
286281
score = Math.max(score, 95)
287282
}
288283

@@ -304,14 +299,7 @@ export function shouldHardBlockFreeModeAccess(
304299
): boolean {
305300
if (countryAccess.cfCountry === CLOUDFLARE_TOR_COUNTRY) return true
306301
if (countryAccess.blockReason !== 'anonymous_network') return false
307-
if (
308-
hasTorPrivacySignal(countryAccess.ipPrivacy) &&
309-
(hasTorPrivacySignal(countryAccess.spurIpPrivacy) ||
310-
hasTorPrivacySignal(countryAccess.scamalyticsIpPrivacy))
311-
) {
312-
return true
313-
}
314-
return hasStrongCorroboratedAbuse(countryAccess)
302+
return hasCorroboratedTorSignal(countryAccess)
315303
}
316304

317305
export function getFreeModePrivacyDecision(
@@ -341,7 +329,7 @@ export function getFreeModePrivacyDecision(
341329
return 'ipinfo_failed_limited'
342330
}
343331
if (countryAccess.blockReason === 'anonymous_network') {
344-
if (hasStrongCorroboratedAbuse(countryAccess)) {
332+
if (shouldHardBlockFreeModeAccess(countryAccess)) {
345333
return 'corroborated_block'
346334
}
347335
if (countryAccess.spurStatus === 'failed') {

0 commit comments

Comments
 (0)