Skip to content

Commit 35819f6

Browse files
authored
Block unverifiable free-mode countries (#551)
1 parent 6dfbb3b commit 35819f6

10 files changed

Lines changed: 741 additions & 400 deletions

File tree

cli/src/components/waiting-room-screen.tsx

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -221,13 +221,13 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
221221
<span fg={theme.muted}> / {session.queueDepth}</span>
222222
</text>
223223
<text style={{ fg: theme.muted, alignSelf: 'flex-start' }}>
224-
<span>Wait </span>
224+
<span>Wait </span>
225225
{session.position === 1
226226
? 'any moment now'
227227
: formatWait(session.estimatedWaitMs)}
228228
</text>
229229
<text style={{ fg: theme.muted, alignSelf: 'flex-start' }}>
230-
<span>Elapsed </span>
230+
<span>Elapsed </span>
231231
{formatElapsed(elapsedMs)}
232232
</text>
233233
{/* Per-model session quota (e.g. GLM 5.1 caps at 5/20h). Only
@@ -237,7 +237,8 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
237237
<text style={{ fg: theme.muted, alignSelf: 'flex-start' }}>
238238
<span>Sessions </span>
239239
<span fg={theme.foreground}>
240-
{session.rateLimit.recentCount} / {session.rateLimit.limit}
240+
{session.rateLimit.recentCount} /{' '}
241+
{session.rateLimit.limit}
241242
</span>
242243
<span> used in last {session.rateLimit.windowHours}h</span>
243244
</text>
@@ -262,10 +263,20 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
262263
⚠ Free mode isn't available in your region
263264
</text>
264265
<text style={{ fg: theme.muted, wrapMode: 'word' }}>
265-
We detected your location as{' '}
266-
<span fg={theme.foreground}>{session.countryCode}</span>,
267-
which is outside the countries where freebuff is currently
268-
offered. Press Ctrl+C to exit.
266+
{session.countryCode === 'UNKNOWN' ? (
267+
<>
268+
We couldn't verify an eligible location for this request.
269+
VPN, Tor, proxy, or unknown-location traffic can't use
270+
freebuff. Press Ctrl+C to exit.
271+
</>
272+
) : (
273+
<>
274+
We detected your location as{' '}
275+
<span fg={theme.foreground}>{session.countryCode}</span>,
276+
which is outside the countries where freebuff is currently
277+
offered. Press Ctrl+C to exit.
278+
</>
279+
)}
269280
</text>
270281
</>
271282
)}
@@ -279,8 +290,9 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
279290
⚠ Account unavailable
280291
</text>
281292
<text style={{ fg: theme.muted, wrapMode: 'word' }}>
282-
This account has been suspended and can't use freebuff. If you think this is a
283-
mistake, contact support@codebuff.com. Press Ctrl+C to exit.
293+
This account has been suspended and can't use freebuff. If you
294+
think this is a mistake, contact support@codebuff.com. Press
295+
Ctrl+C to exit.
284296
</text>
285297
</>
286298
)}

cli/src/hooks/use-freebuff-session.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ const playAdmissionSound = () => {
3838
}
3939

4040
const sessionEndpoint = (): string => {
41-
const base = (env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'https://codebuff.com').replace(/\/$/, '')
41+
const base = (
42+
env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'https://codebuff.com'
43+
).replace(/\/$/, '')
4244
return `${base}/api/v1/freebuff/session`
4345
}
4446

@@ -73,10 +75,13 @@ async function callSession(
7375
// generic error and back off on the 10s error-retry cadence instead of
7476
// tight-polling an unrecognized 200 body.
7577
if (resp.status === 403) {
76-
const body = (await resp.json().catch(() => null)) as
77-
| FreebuffSessionResponse
78-
| null
79-
if (body && (body.status === 'country_blocked' || body.status === 'banned')) {
78+
const body = (await resp
79+
.json()
80+
.catch(() => null)) as FreebuffSessionResponse | null
81+
if (
82+
body &&
83+
(body.status === 'country_blocked' || body.status === 'banned')
84+
) {
8085
return body
8186
}
8287
}
@@ -85,9 +90,9 @@ async function callSession(
8590
// Surface model-switch conflicts and temporary model availability closures
8691
// as non-throw states.
8792
if (resp.status === 409 && method === 'POST') {
88-
const body = (await resp.json().catch(() => null)) as
89-
| FreebuffSessionResponse
90-
| null
93+
const body = (await resp
94+
.json()
95+
.catch(() => null)) as FreebuffSessionResponse | null
9196
if (
9297
body &&
9398
(body.status === 'model_locked' || body.status === 'model_unavailable')
@@ -101,9 +106,9 @@ async function callSession(
101106
// status (rather than 200) keeps older CLIs in their error path so they
102107
// back off instead of tight-polling an unrecognized 200 body.
103108
if (resp.status === 429 && method === 'POST') {
104-
const body = (await resp.json().catch(() => null)) as
105-
| FreebuffSessionResponse
106-
| null
109+
const body = (await resp
110+
.json()
111+
.catch(() => null)) as FreebuffSessionResponse | null
107112
if (body && body.status === 'rate_limited') {
108113
return body
109114
}
@@ -190,9 +195,7 @@ export function getFreebuffInstanceId(): string | undefined {
190195
* holding (queued, active, or in the post-expiry grace window with a live
191196
* instance id). DELETE only matters in those states; otherwise we'd fire a
192197
* spurious request the server has nothing to act on. */
193-
function shouldReleaseSlot(
194-
current: FreebuffSessionResponse | null,
195-
): boolean {
198+
function shouldReleaseSlot(current: FreebuffSessionResponse | null): boolean {
196199
if (!current) return false
197200
return (
198201
current.status === 'queued' ||
@@ -312,7 +315,7 @@ export function markFreebuffSessionSuperseded(): void {
312315

313316
/** Flip into the terminal `country_blocked` state from outside the poll loop.
314317
* Used when the chat-completions gate rejects on country even though the
315-
* session-level country check had failed open (null detection → admitted).
318+
* session-level country check did not catch the request first.
316319
* Transitioning the session state here unmounts the Chat surface in favor of
317320
* the waiting-room's country_blocked message, so the user can't keep typing
318321
* and sending doomed requests. */

cli/src/utils/error-handling.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ export const isFreeModeUnavailableError = (error: unknown): boolean => {
6060
/**
6161
* Extract the detected countryCode off a free_mode_unavailable error, if the
6262
* server included one. Used to populate the country_blocked screen after the
63-
* chat-completions gate rejects a user whose session-level country check had
64-
* previously failed open (null country detection → admitted → now blocked).
63+
* chat-completions gate rejects a user whose session-level country check did
64+
* not catch the request first.
6565
*/
6666
export const getCountryCodeFromFreeModeError = (
6767
error: unknown,

common/src/types/freebuff-session.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,12 @@ export type FreebuffSessionServerResponse =
9898
status: 'superseded'
9999
}
100100
| {
101-
/** Request originated from a country outside the free-mode allowlist.
101+
/** Request originated outside the free-mode allowlist, or from an
102+
* unknown/anonymized location that cannot be trusted for free mode.
102103
* Returned before queue admission so users don't wait through the
103104
* room only to be rejected on their first chat request. Terminal —
104105
* CLI stops polling and shows a "not available in your country"
105-
* screen. `countryCode` is the resolved country for display. */
106+
* screen. `countryCode` is the resolved country, or UNKNOWN. */
106107
status: 'country_blocked'
107108
countryCode: string
108109
}

0 commit comments

Comments
 (0)