Skip to content

Fix freebuff VPN block messaging#555

Merged
jahooma merged 3 commits intomainfrom
jahooma/fix-us-vpn-block
Apr 27, 2026
Merged

Fix freebuff VPN block messaging#555
jahooma merged 3 commits intomainfrom
jahooma/fix-us-vpn-block

Conversation

@jahooma
Copy link
Copy Markdown
Contributor

@jahooma jahooma commented Apr 27, 2026

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.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 27, 2026

Greptile Summary

This PR fixes the freebuff VPN-block messaging by propagating countryBlockReason and ipPrivacySignals from the server (both the session and chat APIs) through to the CLI's waiting-room screen, so users on allowed-country IPs that are detected as VPN/proxy/Tor see an accurate message instead of "US is unsupported." It also narrows the IP-privacy blocking logic so that hosting and service signals alone no longer block an otherwise-allowed country, while preserving block behaviour for is_anonymous paired with only non-blocking signals.

Confidence Score: 5/5

Safe 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.

Important Files Changed

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)

  1. cli/src/hooks/use-freebuff-session.ts, line 326-353 (link)

    P2 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:

    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!

  2. cli/src/components/waiting-room-screen.tsx, line 266-295 (link)

    P2 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.

    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

Comment on lines +76 to +100
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,
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

@jahooma jahooma merged commit 1b922dc into main Apr 27, 2026
33 of 34 checks passed
@jahooma jahooma deleted the jahooma/fix-us-vpn-block branch April 27, 2026 07:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant