Block freebuff waiting room for disallowed countries#522
Conversation
Greptile SummaryThis PR introduces an early country-block gate for the freebuff waiting-room endpoints ( Key changes:
Confidence Score: 4/5Safe to merge; no runtime bugs found and the core flow is correctly implemented end-to-end. The feature is well-designed with correct terminal-state handling in the CLI, proper auth-before-country ordering on the server, and good test coverage of the new code paths. The two P2 findings (redundant
Important Files Changed
Sequence DiagramsequenceDiagram
participant CLI
participant Session as POST /api/v1/freebuff/session
participant Country as free-mode-country.ts
participant Queue as Free Session Queue
CLI->>Session: POST (Bearer token)
Session->>Session: resolveUser() — auth check
alt Auth fails
Session-->>CLI: 401 Unauthorized
else Auth ok
Session->>Country: getCountryCode(req)
Country->>Country: cf-ipcountry header (or geoip fallback)
Country-->>Session: countryCode | null
alt Country resolved and not in allowlist
Session-->>CLI: 200 { status: country_blocked, countryCode }
CLI->>CLI: nextDelayMs returns null (terminal, stop polling)
CLI->>CLI: Render WaitingRoomScreen not available in your region
else Country null VPN/localhost or allowed
Session->>Queue: requestSession(userId)
Queue-->>Session: queued | active | disabled
Session-->>CLI: 200 { status: queued | active | disabled }
CLI->>CLI: Schedule GET poll
end
end
|
| export function isCountryAllowedForFreeMode(req: NextRequest): boolean | null { | ||
| const countryCode = getCountryCode(req) | ||
| if (!countryCode) return null | ||
| return FREE_MODE_ALLOWED_COUNTRIES.has(countryCode) | ||
| } |
There was a problem hiding this comment.
isCountryAllowedForFreeMode is exported but never imported
This helper was added to the shared module but nothing in the codebase actually imports it — both _handlers.ts and _post.ts call getCountryCode + FREE_MODE_ALLOWED_COUNTRIES.has() directly. Either wire up the callers to use this function (which would simplify countryBlockedResponse in _handlers.ts) or remove it to keep the public surface minimal.
| test('returns country_blocked without joining the queue for disallowed country', async () => { | ||
| const sessionDeps = makeSessionDeps() | ||
| const resp = await postFreebuffSession( | ||
| makeReq('ok', { cfCountry: 'FR' }), | ||
| makeDeps(sessionDeps, 'u1'), | ||
| ) | ||
| expect(resp.status).toBe(200) | ||
| const body = await resp.json() | ||
| expect(body.status).toBe('country_blocked') | ||
| expect(body.countryCode).toBe('FR') | ||
| expect(sessionDeps.rows.size).toBe(0) | ||
| }) | ||
|
|
||
| test('allows queue entry for allowed country', async () => { | ||
| const sessionDeps = makeSessionDeps() | ||
| const resp = await postFreebuffSession( | ||
| makeReq('ok', { cfCountry: 'US' }), | ||
| makeDeps(sessionDeps, 'u1'), | ||
| ) | ||
| const body = await resp.json() | ||
| expect(body.status).toBe('queued') | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Missing test for the null-country fail-open path
The new tests cover blocked (FR) and allowed (US) countries, but the shared module's stated contract — "null country (VPN/localhost) fails open" — is not exercised. A request without cf-ipcountry and no resolvable IP should pass through to queue admission. Given this is the main escape hatch for VPN/localhost users, a test that verifies postFreebuffSession creates a queued session when no country headers are present would round out the coverage.
test('allows queue entry when country cannot be determined (null/VPN)', async () => {
const sessionDeps = makeSessionDeps()
// No cfCountry → geoip returns null → fail open
const resp = await postFreebuffSession(makeReq('ok'), makeDeps(sessionDeps, 'u1'))
const body = await resp.json()
expect(body.status).toBe('queued')
})
Summary
Users from countries outside the free-mode allowlist were waiting through the entire freebuff queue only to be rejected at their first chat request. This PR short-circuits POST/GET
/api/v1/freebuff/sessionwith a new terminalcountry_blockedstatus so the CLI can skip the queue and render a clear "not available in your region" screen up front.The country-check logic (
FREE_MODE_ALLOWED_COUNTRIES,getCountryCode,extractClientIp) was lifted out ofchat/completions/_post.tsinto a sharedweb/src/server/free-mode-country.tsso both endpoints use the same detection (Cloudflarecf-ipcountryheader, falling back to geoip-lite). Null country (VPN/localhost) still fails open. Added handler tests covering the new block for both POST and GET.Test plan
bun test web/src/app/api/v1/freebuff/session/__tests__/session.test.ts— 10 passbun test web/src/app/api/v1/chat/completions/__tests__/completions.test.ts— 24 pass, unchangedtsc --noEmitpasses forweb,cli, andcommoncountry_blockedwithout creating a session row, and the CLI shows the warning screen