Skip to content

Commit 0c80ab3

Browse files
committed
Address freebuff block review comments
1 parent 735a936 commit 0c80ab3

4 files changed

Lines changed: 120 additions & 55 deletions

File tree

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

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { exitFreebuffCleanly } from '../utils/freebuff-exit'
1717
import { getLogoAccentColor, getLogoBlockColor } from '../utils/theme-system'
1818

1919
import type { FreebuffSessionResponse } from '../types/freebuff-session'
20+
import type { FreebuffIpPrivacySignal } from '@codebuff/common/types/freebuff-session'
2021

2122
interface WaitingRoomScreenProps {
2223
session: FreebuffSessionResponse | null
@@ -55,6 +56,35 @@ const formatRetryAfter = (ms: number): string => {
5556
return rem === 0 ? `${hours}h` : `${hours}h ${rem}m`
5657
}
5758

59+
const PRIVACY_SIGNAL_LABELS: Partial<Record<FreebuffIpPrivacySignal, string>> =
60+
{
61+
anonymous: 'anonymized network',
62+
proxy: 'proxy',
63+
relay: 'relay',
64+
res_proxy: 'residential proxy',
65+
tor: 'Tor',
66+
vpn: 'VPN',
67+
}
68+
69+
const formatPrivacySignalList = (
70+
signals: FreebuffIpPrivacySignal[] | undefined,
71+
): string => {
72+
const labels = Array.from(
73+
new Set(
74+
signals
75+
?.map((signal) => PRIVACY_SIGNAL_LABELS[signal])
76+
.filter((label): label is string => Boolean(label)) ?? [],
77+
),
78+
)
79+
80+
if (labels.length === 0) {
81+
return 'VPN, Tor, proxy, relay, or anonymized network'
82+
}
83+
if (labels.length === 1) return labels[0]
84+
if (labels.length === 2) return `${labels[0]} or ${labels[1]}`
85+
return `${labels.slice(0, -1).join(', ')}, or ${labels[labels.length - 1]}`
86+
}
87+
5888
export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
5989
session,
6090
error,
@@ -265,8 +295,8 @@ export const WaitingRoomScreen: React.FC<WaitingRoomScreenProps> = ({
265295
<text style={{ fg: theme.muted, wrapMode: 'word' }}>
266296
{session.countryBlockReason === 'anonymous_network' ? (
267297
<>
268-
We detected VPN, Tor, proxy, relay, or anonymized network
269-
traffic
298+
We detected{' '}
299+
{formatPrivacySignalList(session.ipPrivacySignals)} traffic
270300
{session.countryCode === 'UNKNOWN' ? (
271301
''
272302
) : (

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

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -323,30 +323,14 @@ export function markFreebuffSessionSuperseded(): void {
323323
* Transitioning the session state here unmounts the Chat surface in favor of
324324
* the waiting-room's country_blocked message, so the user can't keep typing
325325
* and sending doomed requests. */
326-
export function markFreebuffSessionCountryBlocked(
327-
params:
328-
| string
329-
| {
330-
countryCode: string
331-
countryBlockReason?: string
332-
ipPrivacySignals?: string[]
333-
},
334-
): void {
326+
export function markFreebuffSessionCountryBlocked(params: {
327+
countryCode: string
328+
countryBlockReason?: FreebuffCountryBlockReason
329+
ipPrivacySignals?: FreebuffIpPrivacySignal[]
330+
}): void {
335331
if (!IS_FREEBUFF) return
336-
const next =
337-
typeof params === 'string'
338-
? { countryCode: params }
339-
: {
340-
countryCode: params.countryCode,
341-
countryBlockReason: params.countryBlockReason as
342-
| FreebuffCountryBlockReason
343-
| undefined,
344-
ipPrivacySignals: params.ipPrivacySignals as
345-
| FreebuffIpPrivacySignal[]
346-
| undefined,
347-
}
348332
controller?.abort()
349-
controller?.apply({ status: 'country_blocked', ...next })
333+
controller?.apply({ status: 'country_blocked', ...params })
350334
// Best-effort DELETE so we don't hold a waiting-room seat on a session the
351335
// server is already refusing to serve at chat time.
352336
releaseFreebuffSlot().catch(() => {})

cli/src/utils/__tests__/error-handling.test.ts

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { describe, test, expect } from 'bun:test'
33
import {
44
isOutOfCreditsError,
55
isFreeModeUnavailableError,
6+
getCountryBlockFromFreeModeError,
67
OUT_OF_CREDITS_MESSAGE,
78
FREE_MODE_UNAVAILABLE_MESSAGE,
89
createErrorMessage,
@@ -70,7 +71,11 @@ describe('error-handling', () => {
7071

7172
describe('isFreeModeUnavailableError', () => {
7273
test('returns true for error with statusCode 403 and error free_mode_unavailable', () => {
73-
const error = { statusCode: 403, error: 'free_mode_unavailable', message: 'Free mode is not available in your country.' }
74+
const error = {
75+
statusCode: 403,
76+
error: 'free_mode_unavailable',
77+
message: 'Free mode is not available in your country.',
78+
}
7479
expect(isFreeModeUnavailableError(error)).toBe(true)
7580
})
7681

@@ -80,12 +85,20 @@ describe('error-handling', () => {
8085
})
8186

8287
test('returns false for 403 with different error code', () => {
83-
const error = { statusCode: 403, error: 'account_suspended', message: 'Suspended' }
88+
const error = {
89+
statusCode: 403,
90+
error: 'account_suspended',
91+
message: 'Suspended',
92+
}
8493
expect(isFreeModeUnavailableError(error)).toBe(false)
8594
})
8695

8796
test('returns false for non-403 status with free_mode_unavailable error', () => {
88-
const error = { statusCode: 400, error: 'free_mode_unavailable', message: 'Bad request' }
97+
const error = {
98+
statusCode: 400,
99+
error: 'free_mode_unavailable',
100+
message: 'Bad request',
101+
}
89102
expect(isFreeModeUnavailableError(error)).toBe(false)
90103
})
91104

@@ -102,9 +115,51 @@ describe('error-handling', () => {
102115
})
103116
})
104117

118+
describe('getCountryBlockFromFreeModeError', () => {
119+
test('extracts country block details from free-mode unavailable errors', () => {
120+
const error = {
121+
statusCode: 403,
122+
error: 'free_mode_unavailable',
123+
countryCode: 'US',
124+
countryBlockReason: 'anonymous_network',
125+
ipPrivacySignals: ['vpn', 'hosting', 123],
126+
}
127+
128+
expect(getCountryBlockFromFreeModeError(error)).toEqual({
129+
countryCode: 'US',
130+
countryBlockReason: 'anonymous_network',
131+
ipPrivacySignals: ['vpn', 'hosting'],
132+
})
133+
})
134+
135+
test('defaults missing country code to UNKNOWN', () => {
136+
const error = {
137+
statusCode: 403,
138+
error: 'free_mode_unavailable',
139+
}
140+
141+
expect(getCountryBlockFromFreeModeError(error)).toEqual({
142+
countryCode: 'UNKNOWN',
143+
countryBlockReason: undefined,
144+
ipPrivacySignals: undefined,
145+
})
146+
})
147+
148+
test('returns null for non-free-mode errors', () => {
149+
expect(
150+
getCountryBlockFromFreeModeError({
151+
statusCode: 403,
152+
error: 'account_suspended',
153+
}),
154+
).toBe(null)
155+
})
156+
})
157+
105158
describe('FREE_MODE_UNAVAILABLE_MESSAGE', () => {
106159
test('mentions unavailability in country', () => {
107-
expect(FREE_MODE_UNAVAILABLE_MESSAGE.toLowerCase()).toContain('not available in your country')
160+
expect(FREE_MODE_UNAVAILABLE_MESSAGE.toLowerCase()).toContain(
161+
'not available in your country',
162+
)
108163
})
109164
})
110165

cli/src/utils/error-handling.ts

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { env } from '@codebuff/common/env'
22

33
import type { ChatMessage } from '../types/chat'
4+
import type {
5+
FreebuffCountryBlockReason,
6+
FreebuffIpPrivacySignal,
7+
} from '@codebuff/common/types/freebuff-session'
48

59
import { IS_FREEBUFF } from './constants'
610

@@ -57,43 +61,35 @@ export const isFreeModeUnavailableError = (error: unknown): boolean => {
5761
return false
5862
}
5963

60-
/**
61-
* Extract the detected countryCode off a free_mode_unavailable error, if the
62-
* server included one. Used to populate the country_blocked screen after the
63-
* chat-completions gate rejects a user whose session-level country check did
64-
* not catch the request first.
65-
*/
66-
export const getCountryCodeFromFreeModeError = (
67-
error: unknown,
68-
): string | null => {
69-
if (!isFreeModeUnavailableError(error)) return null
70-
const candidate = (error as { countryCode?: unknown }).countryCode
71-
return typeof candidate === 'string' && candidate.length > 0
72-
? candidate
73-
: null
74-
}
75-
7664
export const getCountryBlockFromFreeModeError = (
7765
error: unknown,
7866
): {
7967
countryCode: string
80-
countryBlockReason?: string
81-
ipPrivacySignals?: string[]
68+
countryBlockReason?: FreebuffCountryBlockReason
69+
ipPrivacySignals?: FreebuffIpPrivacySignal[]
8270
} | null => {
8371
if (!isFreeModeUnavailableError(error)) return null
84-
const countryCode = getCountryCodeFromFreeModeError(error) ?? 'UNKNOWN'
85-
const countryBlockReason = (error as { countryBlockReason?: unknown })
86-
.countryBlockReason
87-
const ipPrivacySignals = (error as { ipPrivacySignals?: unknown })
88-
.ipPrivacySignals
72+
const errorDetails = error as {
73+
countryCode?: unknown
74+
countryBlockReason?: unknown
75+
ipPrivacySignals?: unknown
76+
}
77+
const countryCode =
78+
typeof errorDetails.countryCode === 'string' &&
79+
errorDetails.countryCode.length > 0
80+
? errorDetails.countryCode
81+
: 'UNKNOWN'
8982

9083
return {
9184
countryCode,
9285
countryBlockReason:
93-
typeof countryBlockReason === 'string' ? countryBlockReason : undefined,
94-
ipPrivacySignals: Array.isArray(ipPrivacySignals)
95-
? ipPrivacySignals.filter(
96-
(signal): signal is string => typeof signal === 'string',
86+
typeof errorDetails.countryBlockReason === 'string'
87+
? (errorDetails.countryBlockReason as FreebuffCountryBlockReason)
88+
: undefined,
89+
ipPrivacySignals: Array.isArray(errorDetails.ipPrivacySignals)
90+
? errorDetails.ipPrivacySignals.filter(
91+
(signal): signal is FreebuffIpPrivacySignal =>
92+
typeof signal === 'string',
9793
)
9894
: undefined,
9995
}

0 commit comments

Comments
 (0)