Conversation
Greptile SummaryThis PR fixes the freebuff VPN-block messaging by propagating Confidence Score: 5/5Safe to merge — all remaining findings are P2 style/simplification suggestions that do not affect correctness. The logic change (dropping hosting/service as blockers, refining is_anonymous handling) is well-tested with focused new tests and full typecheck. No P0/P1 issues found. No files require special attention.
|
| Filename | Overview |
|---|---|
| web/src/server/free-mode-country.ts | Core logic change: hosting and service signals no longer block allowed countries; is_anonymous now appends only when no blocking signal is already present, correctly preserving block behaviour for is_anonymous + service combos. Types consolidated to common package. |
| common/src/types/freebuff-session.ts | Added FreebuffCountryBlockReason and FreebuffIpPrivacySignal shared types; extended country_blocked status with optional countryBlockReason and ipPrivacySignals fields. |
| cli/src/hooks/use-freebuff-session.ts | markFreebuffSessionCountryBlocked widened to accept block reason + signals alongside country code; includes a now-unused string overload that adds type-cast complexity. |
| cli/src/utils/error-handling.ts | Added getCountryBlockFromFreeModeError to extract countryCode, countryBlockReason, and ipPrivacySignals from a free_mode_unavailable error; delegates to getCountryCodeFromFreeModeError, which re-runs the guard check. |
| cli/src/components/waiting-room-screen.tsx | New anonymous_network branch shows a VPN/Tor/proxy message with optional country code; ipPrivacySignals stored in session state but not rendered anywhere in the UI. |
| web/src/app/api/v1/freebuff/session/_handlers.ts | 403 country_blocked response now includes countryBlockReason and ipPrivacySignals from the country-access result. |
| web/src/app/api/v1/chat/completions/_post.ts | Chat completions 403 response surfaces countryBlockReason and ipPrivacySignals so the CLI can show an accurate message when the chat gate blocks after session admission. |
| cli/src/hooks/helpers/send-message.ts | Both handleRunCompletion and handleRunError switched from getCountryCodeFromFreeModeError to getCountryBlockFromFreeModeError, passing the full block object to markFreebuffSessionCountryBlocked. |
| web/src/server/tests/free-mode-country.test.ts | Two new tests: one verifying hosting/service-only IPs are allowed; one confirming is_anonymous + service still blocks. Good coverage of the new signal-filtering logic. |
| web/src/app/api/v1/freebuff/session/tests/session.test.ts | Existing session tests extended with countryBlockReason assertions for all three block scenarios. |
| web/src/app/api/v1/chat/completions/tests/completions.test.ts | Chat completions tests updated to assert countryBlockReason on the two existing block scenarios. |
Comments Outside Diff (2)
-
cli/src/hooks/use-freebuff-session.ts, line 326-353 (link)Dead string overload in
markFreebuffSessionCountryBlockedThe
stringbranch of theparamsunion is never hit by any caller after this PR. Both callers insend-message.tsnow pass an object, and there are no other call sites visible. The string-overload path adds complexity (the explicit cast toFreebuffCountryBlockReason | undefined/FreebuffIpPrivacySignal[]) without benefit. Simplifying to accept only the object form would let you drop thetypeof params === 'string'guard and the casts entirely:export function markFreebuffSessionCountryBlocked(params: { countryCode: string countryBlockReason?: FreebuffCountryBlockReason ipPrivacySignals?: FreebuffIpPrivacySignal[] }): void { if (!IS_FREEBUFF) return controller?.abort() controller?.apply({ status: 'country_blocked', ...params }) releaseFreebuffSlot().catch(() => {}) }
Prompt To Fix With AI
This is a comment left during a code review. Path: cli/src/hooks/use-freebuff-session.ts Line: 326-353 Comment: **Dead string overload in `markFreebuffSessionCountryBlocked`** The `string` branch of the `params` union is never hit by any caller after this PR. Both callers in `send-message.ts` now pass an object, and there are no other call sites visible. The string-overload path adds complexity (the explicit cast to `FreebuffCountryBlockReason | undefined` / `FreebuffIpPrivacySignal[]`) without benefit. Simplifying to accept only the object form would let you drop the `typeof params === 'string'` guard and the casts entirely: ```ts export function markFreebuffSessionCountryBlocked(params: { countryCode: string countryBlockReason?: FreebuffCountryBlockReason ipPrivacySignals?: FreebuffIpPrivacySignal[] }): void { if (!IS_FREEBUFF) return controller?.abort() controller?.apply({ status: 'country_blocked', ...params }) releaseFreebuffSlot().catch(() => {}) } ``` How can I resolve this? If you propose a fix, please make it concise.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
-
cli/src/components/waiting-room-screen.tsx, line 266-295 (link)ipPrivacySignalscarried to the client but never renderedipPrivacySignalsis returned by the server, stored in the session state, and defined in the shared type, but the UI message always reads "VPN, Tor, proxy, relay, or anonymized network traffic" regardless of which specific signals triggered the block. If the signals aren't going to be displayed individually, consider omitting them from the wire response and theFreebuffSessionServerResponsetype to reduce payload size and type surface area until they're actually needed.Prompt To Fix With AI
This is a comment left during a code review. Path: cli/src/components/waiting-room-screen.tsx Line: 266-295 Comment: **`ipPrivacySignals` carried to the client but never rendered** `ipPrivacySignals` is returned by the server, stored in the session state, and defined in the shared type, but the UI message always reads "VPN, Tor, proxy, relay, or anonymized network traffic" regardless of which specific signals triggered the block. If the signals aren't going to be displayed individually, consider omitting them from the wire response and the `FreebuffSessionServerResponse` type to reduce payload size and type surface area until they're actually needed. How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: cli/src/hooks/use-freebuff-session.ts
Line: 326-353
Comment:
**Dead string overload in `markFreebuffSessionCountryBlocked`**
The `string` branch of the `params` union is never hit by any caller after this PR. Both callers in `send-message.ts` now pass an object, and there are no other call sites visible. The string-overload path adds complexity (the explicit cast to `FreebuffCountryBlockReason | undefined` / `FreebuffIpPrivacySignal[]`) without benefit. Simplifying to accept only the object form would let you drop the `typeof params === 'string'` guard and the casts entirely:
```ts
export function markFreebuffSessionCountryBlocked(params: {
countryCode: string
countryBlockReason?: FreebuffCountryBlockReason
ipPrivacySignals?: FreebuffIpPrivacySignal[]
}): void {
if (!IS_FREEBUFF) return
controller?.abort()
controller?.apply({ status: 'country_blocked', ...params })
releaseFreebuffSlot().catch(() => {})
}
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: cli/src/utils/error-handling.ts
Line: 76-100
Comment:
**`isFreeModeUnavailableError` called twice**
`getCountryBlockFromFreeModeError` calls `isFreeModeUnavailableError(error)` and then delegates to `getCountryCodeFromFreeModeError(error)`, which calls it again. The two functions could be merged or the inner helper inlined to eliminate the double-check:
```ts
export const getCountryBlockFromFreeModeError = (
error: unknown,
): { countryCode: string; countryBlockReason?: string; ipPrivacySignals?: string[] } | null => {
if (!isFreeModeUnavailableError(error)) return null
const obj = error as { countryCode?: unknown; countryBlockReason?: unknown; ipPrivacySignals?: unknown }
const countryCode =
typeof obj.countryCode === 'string' && obj.countryCode.length > 0
? obj.countryCode
: 'UNKNOWN'
return {
countryCode,
countryBlockReason: typeof obj.countryBlockReason === 'string' ? obj.countryBlockReason : undefined,
ipPrivacySignals: Array.isArray(obj.ipPrivacySignals)
? obj.ipPrivacySignals.filter((s): s is string => typeof s === 'string')
: undefined,
}
}
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: cli/src/components/waiting-room-screen.tsx
Line: 266-295
Comment:
**`ipPrivacySignals` carried to the client but never rendered**
`ipPrivacySignals` is returned by the server, stored in the session state, and defined in the shared type, but the UI message always reads "VPN, Tor, proxy, relay, or anonymized network traffic" regardless of which specific signals triggered the block. If the signals aren't going to be displayed individually, consider omitting them from the wire response and the `FreebuffSessionServerResponse` type to reduce payload size and type surface area until they're actually needed.
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "Fix freebuff VPN block messaging" | Re-trigger Greptile
| export const getCountryBlockFromFreeModeError = ( | ||
| error: unknown, | ||
| ): { | ||
| countryCode: string | ||
| countryBlockReason?: string | ||
| ipPrivacySignals?: string[] | ||
| } | null => { | ||
| if (!isFreeModeUnavailableError(error)) return null | ||
| const countryCode = getCountryCodeFromFreeModeError(error) ?? 'UNKNOWN' | ||
| const countryBlockReason = (error as { countryBlockReason?: unknown }) | ||
| .countryBlockReason | ||
| const ipPrivacySignals = (error as { ipPrivacySignals?: unknown }) | ||
| .ipPrivacySignals | ||
|
|
||
| return { | ||
| countryCode, | ||
| countryBlockReason: | ||
| typeof countryBlockReason === 'string' ? countryBlockReason : undefined, | ||
| ipPrivacySignals: Array.isArray(ipPrivacySignals) | ||
| ? ipPrivacySignals.filter( | ||
| (signal): signal is string => typeof signal === 'string', | ||
| ) | ||
| : undefined, | ||
| } | ||
| } |
There was a problem hiding this comment.
isFreeModeUnavailableError called twice
getCountryBlockFromFreeModeError calls isFreeModeUnavailableError(error) and then delegates to getCountryCodeFromFreeModeError(error), which calls it again. The two functions could be merged or the inner helper inlined to eliminate the double-check:
export const getCountryBlockFromFreeModeError = (
error: unknown,
): { countryCode: string; countryBlockReason?: string; ipPrivacySignals?: string[] } | null => {
if (!isFreeModeUnavailableError(error)) return null
const obj = error as { countryCode?: unknown; countryBlockReason?: unknown; ipPrivacySignals?: unknown }
const countryCode =
typeof obj.countryCode === 'string' && obj.countryCode.length > 0
? obj.countryCode
: 'UNKNOWN'
return {
countryCode,
countryBlockReason: typeof obj.countryBlockReason === 'string' ? obj.countryBlockReason : undefined,
ipPrivacySignals: Array.isArray(obj.ipPrivacySignals)
? obj.ipPrivacySignals.filter((s): s is string => typeof s === 'string')
: undefined,
}
}Prompt To Fix With AI
This is a comment left during a code review.
Path: cli/src/utils/error-handling.ts
Line: 76-100
Comment:
**`isFreeModeUnavailableError` called twice**
`getCountryBlockFromFreeModeError` calls `isFreeModeUnavailableError(error)` and then delegates to `getCountryCodeFromFreeModeError(error)`, which calls it again. The two functions could be merged or the inner helper inlined to eliminate the double-check:
```ts
export const getCountryBlockFromFreeModeError = (
error: unknown,
): { countryCode: string; countryBlockReason?: string; ipPrivacySignals?: string[] } | null => {
if (!isFreeModeUnavailableError(error)) return null
const obj = error as { countryCode?: unknown; countryBlockReason?: unknown; ipPrivacySignals?: unknown }
const countryCode =
typeof obj.countryCode === 'string' && obj.countryCode.length > 0
? obj.countryCode
: 'UNKNOWN'
return {
countryCode,
countryBlockReason: typeof obj.countryBlockReason === 'string' ? obj.countryBlockReason : undefined,
ipPrivacySignals: Array.isArray(obj.ipPrivacySignals)
? obj.ipPrivacySignals.filter((s): s is string => typeof s === 'string')
: undefined,
}
}
```
How can I resolve this? If you propose a fix, please make it concise.
Fixes freebuff country-block messaging when a user is in an allowed country but blocked because their IP is detected as an anonymized network. The session and chat APIs now return countryBlockReason and ipPrivacySignals so the CLI can show an accurate VPN/proxy/Tor/relay message instead of saying US is unsupported. Hosting and service IPinfo signals are still surfaced for diagnostics but no longer block otherwise-allowed countries. Validated with focused free-mode country/session/chat tests plus full typecheck.