Skip to content

Commit e8b9af6

Browse files
authored
Harden Freebuff country gating (#558)
1 parent 5783b55 commit e8b9af6

15 files changed

Lines changed: 3784 additions & 76 deletions

File tree

common/src/types/freebuff-session.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export type FreebuffCountryBlockReason =
2727
| 'anonymous_network'
2828
| 'missing_client_ip'
2929
| 'unresolved_client_ip'
30+
| 'ip_privacy_lookup_failed'
3031

3132
export type FreebuffIpPrivacySignal =
3233
| 'anonymous'

docs/freebuff-waiting-room.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,13 @@ CREATE TABLE free_session (
6868
status free_session_status NOT NULL,
6969
active_instance_id text NOT NULL,
7070
model text NOT NULL,
71+
country_code text,
72+
cf_country text,
73+
geoip_country text,
74+
country_block_reason text,
75+
ip_privacy_signals text[],
76+
client_ip_hash text,
77+
country_checked_at timestamptz,
7178
queued_at timestamptz NOT NULL DEFAULT now(),
7279
admitted_at timestamptz,
7380
expires_at timestamptz,
@@ -87,6 +94,7 @@ Migrations: `packages/internal/src/db/migrations/0043_vengeful_boomer.sql` (init
8794
- **PK on `user_id`** is the structural enforcement of "one session per account". No app-logic race can produce two rows for one user.
8895
- **`active_instance_id`** rotates on every `POST /session` call. This is how we enforce one-CLI-at-a-time (see [Single-instance enforcement](#single-instance-enforcement)).
8996
- **`model` column.** Populated by the POST handler; determines which queue the row belongs to while queued and is fixed for the life of an active session. Switching models while an active session is live is rejected (`model_locked`, 409).
97+
- **Country/privacy columns.** Populated from the POST `/session` country gate so active-session audits can see the resolved country, Cloudflare country header, GeoIP fallback country, IPinfo privacy signals, and a keyed hash of the client IP. Raw IPs are not stored.
9098
- **All timestamps server-supplied.** The client never sends `queued_at`, `admitted_at`, or `expires_at` — they are either `DEFAULT now()` or computed server-side during admission.
9199
- **FK CASCADE on user delete** keeps the table clean without a background job.
92100

@@ -170,6 +178,8 @@ All endpoints authenticate via the standard `Authorization: Bearer <api-key>` or
170178
- Existing active+unexpired row, **different model** → reject with `model_locked` (HTTP 409); `active_instance_id` is **not** rotated so the other CLI stays valid. Client must DELETE the session before switching.
171179
- Existing active+expired row → reset to queued with fresh `queued_at` and the requested `model` (re-queue at back).
172180

181+
Before any of those state transitions, the handler requires a resolved allowlisted country and a successful IPinfo privacy check. IPinfo `anonymous`, `vpn`, `proxy`, `tor`, `relay`, `res_proxy`, `hosting`, and `service` signals are blocked; privacy lookup failures fail closed.
182+
173183
Response shapes:
174184

175185
```jsonc
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
ALTER TABLE "free_session" ADD COLUMN "country_code" text;--> statement-breakpoint
2+
ALTER TABLE "free_session" ADD COLUMN "cf_country" text;--> statement-breakpoint
3+
ALTER TABLE "free_session" ADD COLUMN "geoip_country" text;--> statement-breakpoint
4+
ALTER TABLE "free_session" ADD COLUMN "country_block_reason" text;--> statement-breakpoint
5+
ALTER TABLE "free_session" ADD COLUMN "ip_privacy_signals" text[];--> statement-breakpoint
6+
ALTER TABLE "free_session" ADD COLUMN "client_ip_hash" text;--> statement-breakpoint
7+
ALTER TABLE "free_session" ADD COLUMN "country_checked_at" timestamp with time zone;

0 commit comments

Comments
 (0)