From 0f2487be0c2e965f482c1600bb5abdb274d29d94 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Tue, 12 May 2026 13:00:15 -0700 Subject: [PATCH] Cache free-mode country access --- .../src/db/migrations/0051_easy_sersi.sql | 18 + .../src/db/migrations/meta/0051_snapshot.json | 3316 +++++++++++++++++ .../src/db/migrations/meta/_journal.json | 7 + packages/internal/src/db/schema.ts | 48 + .../completions/__tests__/completions.test.ts | 101 +- web/src/app/api/v1/chat/completions/_post.ts | 21 +- .../free-mode-country-access-cache.test.ts | 170 + .../server/free-mode-country-access-cache.ts | 190 + web/src/server/free-mode-country.ts | 4 +- 9 files changed, 3828 insertions(+), 47 deletions(-) create mode 100644 packages/internal/src/db/migrations/0051_easy_sersi.sql create mode 100644 packages/internal/src/db/migrations/meta/0051_snapshot.json create mode 100644 web/src/server/__tests__/free-mode-country-access-cache.test.ts create mode 100644 web/src/server/free-mode-country-access-cache.ts diff --git a/packages/internal/src/db/migrations/0051_easy_sersi.sql b/packages/internal/src/db/migrations/0051_easy_sersi.sql new file mode 100644 index 0000000000..caa8eb2892 --- /dev/null +++ b/packages/internal/src/db/migrations/0051_easy_sersi.sql @@ -0,0 +1,18 @@ +CREATE TABLE "free_mode_country_access_cache" ( + "user_id" text NOT NULL, + "client_ip_hash" text NOT NULL, + "allowed" boolean NOT NULL, + "country_code" text, + "cf_country" text, + "geoip_country" text, + "country_block_reason" text, + "ip_privacy_signals" text[], + "checked_at" timestamp with time zone NOT NULL, + "expires_at" timestamp with time zone NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "free_mode_country_access_cache_user_id_client_ip_hash_pk" PRIMARY KEY("user_id","client_ip_hash") +); +--> statement-breakpoint +ALTER TABLE "free_mode_country_access_cache" ADD CONSTRAINT "free_mode_country_access_cache_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "idx_free_mode_country_cache_expires_at" ON "free_mode_country_access_cache" USING btree ("expires_at"); \ No newline at end of file diff --git a/packages/internal/src/db/migrations/meta/0051_snapshot.json b/packages/internal/src/db/migrations/meta/0051_snapshot.json new file mode 100644 index 0000000000..fee986ea24 --- /dev/null +++ b/packages/internal/src/db/migrations/meta/0051_snapshot.json @@ -0,0 +1,3316 @@ +{ + "id": "ead7b227-50a8-4758-8dbf-a5a402606f64", + "prevId": "4c7aa6ac-8afc-4c2c-b0a4-2bbfcde731b8", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "name": "account_provider_providerAccountId_pk", + "columns": ["provider", "providerAccountId"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ad_impression": { + "name": "ad_impression", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'gravity'" + }, + "ad_text": { + "name": "ad_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cta": { + "name": "cta", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "favicon": { + "name": "favicon", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "click_url": { + "name": "click_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "imp_url": { + "name": "imp_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "extra_pixels": { + "name": "extra_pixels", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "payout": { + "name": "payout", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "credits_granted": { + "name": "credits_granted", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "grant_operation_id": { + "name": "grant_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "served_at": { + "name": "served_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "impression_fired_at": { + "name": "impression_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "clicked_at": { + "name": "clicked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_ad_impression_user": { + "name": "idx_ad_impression_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "served_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ad_impression_imp_url": { + "name": "idx_ad_impression_imp_url", + "columns": [ + { + "expression": "imp_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ad_impression_user_id_user_id_fk": { + "name": "ad_impression_user_id_user_id_fk", + "tableFrom": "ad_impression", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ad_impression_imp_url_unique": { + "name": "ad_impression_imp_url_unique", + "nullsNotDistinct": false, + "columns": ["imp_url"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_config": { + "name": "agent_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "publisher_id": { + "name": "publisher_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "major": { + "name": "major", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 1) AS INTEGER)", + "type": "stored" + } + }, + "minor": { + "name": "minor", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 2) AS INTEGER)", + "type": "stored" + } + }, + "patch": { + "name": "patch", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 3) AS INTEGER)", + "type": "stored" + } + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_agent_config_publisher": { + "name": "idx_agent_config_publisher", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_config_publisher_id_publisher_id_fk": { + "name": "agent_config_publisher_id_publisher_id_fk", + "tableFrom": "agent_config", + "tableTo": "publisher", + "columnsFrom": ["publisher_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "agent_config_publisher_id_id_version_pk": { + "name": "agent_config_publisher_id_id_version_pk", + "columns": ["publisher_id", "id", "version"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_run": { + "name": "agent_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "publisher_id": { + "name": "publisher_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(agent_id, '/', 1)\n ELSE NULL\n END", + "type": "stored" + } + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(split_part(agent_id, '/', 2), '@', 1)\n ELSE agent_id\n END", + "type": "stored" + } + }, + "agent_version": { + "name": "agent_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(agent_id, '@', 2)\n ELSE NULL\n END", + "type": "stored" + } + }, + "ancestor_run_ids": { + "name": "ancestor_run_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "root_run_id": { + "name": "root_run_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN array_length(ancestor_run_ids, 1) >= 1 THEN ancestor_run_ids[1] ELSE id END", + "type": "stored" + } + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN array_length(ancestor_run_ids, 1) >= 1 THEN ancestor_run_ids[array_length(ancestor_run_ids, 1)] ELSE NULL END", + "type": "stored" + } + }, + "depth": { + "name": "depth", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "COALESCE(array_length(ancestor_run_ids, 1), 1)", + "type": "stored" + } + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN completed_at IS NOT NULL THEN EXTRACT(EPOCH FROM (completed_at - created_at)) * 1000 ELSE NULL END::integer", + "type": "stored" + } + }, + "total_steps": { + "name": "total_steps", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "direct_credits": { + "name": "direct_credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_credits": { + "name": "total_credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "status": { + "name": "status", + "type": "agent_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_agent_run_user_id": { + "name": "idx_agent_run_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_parent": { + "name": "idx_agent_run_parent", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_root": { + "name": "idx_agent_run_root", + "columns": [ + { + "expression": "root_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_agent_id": { + "name": "idx_agent_run_agent_id", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_publisher": { + "name": "idx_agent_run_publisher", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_status": { + "name": "idx_agent_run_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_ancestors_gin": { + "name": "idx_agent_run_ancestors_gin", + "columns": [ + { + "expression": "ancestor_run_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_agent_run_completed_publisher_agent": { + "name": "idx_agent_run_completed_publisher_agent", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_recent": { + "name": "idx_agent_run_completed_recent", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_version": { + "name": "idx_agent_run_completed_version", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_user": { + "name": "idx_agent_run_completed_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_run_user_id_user_id_fk": { + "name": "agent_run_user_id_user_id_fk", + "tableFrom": "agent_run", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_step": { + "name": "agent_step", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_number": { + "name": "step_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN completed_at IS NOT NULL THEN EXTRACT(EPOCH FROM (completed_at - created_at)) * 1000 ELSE NULL END::integer", + "type": "stored" + } + }, + "credits": { + "name": "credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "child_run_ids": { + "name": "child_run_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "spawned_count": { + "name": "spawned_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "array_length(child_run_ids, 1)", + "type": "stored" + } + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "agent_step_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'completed'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_step_number_per_run": { + "name": "unique_step_number_per_run", + "columns": [ + { + "expression": "agent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "step_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_step_run_id": { + "name": "idx_agent_step_run_id", + "columns": [ + { + "expression": "agent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_step_children_gin": { + "name": "idx_agent_step_children_gin", + "columns": [ + { + "expression": "child_run_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "agent_step_agent_run_id_agent_run_id_fk": { + "name": "agent_step_agent_run_id_agent_run_id_fk", + "tableFrom": "agent_step", + "tableTo": "agent_run", + "columnsFrom": ["agent_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credit_ledger": { + "name": "credit_ledger", + "schema": "", + "columns": { + "operation_id": { + "name": "operation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal": { + "name": "principal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "grant_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_credit_ledger_active_balance": { + "name": "idx_credit_ledger_active_balance", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "balance", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"credit_ledger\".\"balance\" != 0 AND \"credit_ledger\".\"expires_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credit_ledger_org": { + "name": "idx_credit_ledger_org", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credit_ledger_subscription": { + "name": "idx_credit_ledger_subscription", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credit_ledger_user_id_user_id_fk": { + "name": "credit_ledger_user_id_user_id_fk", + "tableFrom": "credit_ledger", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credit_ledger_org_id_org_id_fk": { + "name": "credit_ledger_org_id_org_id_fk", + "tableFrom": "credit_ledger", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.encrypted_api_keys": { + "name": "encrypted_api_keys", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "api_key_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "encrypted_api_keys_user_id_user_id_fk": { + "name": "encrypted_api_keys_user_id_user_id_fk", + "tableFrom": "encrypted_api_keys", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "encrypted_api_keys_user_id_type_pk": { + "name": "encrypted_api_keys_user_id_type_pk", + "columns": ["user_id", "type"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fingerprint": { + "name": "fingerprint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sig_hash": { + "name": "sig_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.free_mode_country_access_cache": { + "name": "free_mode_country_access_cache", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_ip_hash": { + "name": "client_ip_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed": { + "name": "allowed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "country_code": { + "name": "country_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cf_country": { + "name": "cf_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "geoip_country": { + "name": "geoip_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_block_reason": { + "name": "country_block_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_privacy_signals": { + "name": "ip_privacy_signals", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "checked_at": { + "name": "checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_free_mode_country_cache_expires_at": { + "name": "idx_free_mode_country_cache_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "free_mode_country_access_cache_user_id_user_id_fk": { + "name": "free_mode_country_access_cache_user_id_user_id_fk", + "tableFrom": "free_mode_country_access_cache", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "free_mode_country_access_cache_user_id_client_ip_hash_pk": { + "name": "free_mode_country_access_cache_user_id_client_ip_hash_pk", + "columns": ["user_id", "client_ip_hash"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.free_session": { + "name": "free_session", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "status": { + "name": "status", + "type": "free_session_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "active_instance_id": { + "name": "active_instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "country_code": { + "name": "country_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cf_country": { + "name": "cf_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "geoip_country": { + "name": "geoip_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_block_reason": { + "name": "country_block_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_privacy_signals": { + "name": "ip_privacy_signals", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "client_ip_hash": { + "name": "client_ip_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_checked_at": { + "name": "country_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "admitted_at": { + "name": "admitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_free_session_queue": { + "name": "idx_free_session_queue", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_free_session_expiry": { + "name": "idx_free_session_expiry", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "free_session_user_id_user_id_fk": { + "name": "free_session_user_id_user_id_fk", + "tableFrom": "free_session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.free_session_admit": { + "name": "free_session_admit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "admitted_at": { + "name": "admitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "session_units": { + "name": "session_units", + "type": "numeric(3, 1)", + "primaryKey": false, + "notNull": true, + "default": "'1.0'" + } + }, + "indexes": { + "idx_free_session_admit_user_model_time": { + "name": "idx_free_session_admit_user_model_time", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "admitted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "free_session_admit_user_id_user_id_fk": { + "name": "free_session_admit_user_id_user_id_fk", + "tableFrom": "free_session_admit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_eval_results": { + "name": "git_eval_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "cost_mode": { + "name": "cost_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reasoner_model": { + "name": "reasoner_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_model": { + "name": "agent_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.limit_override": { + "name": "limit_override", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credits_per_block": { + "name": "credits_per_block", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "block_duration_hours": { + "name": "block_duration_hours", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "weekly_credit_limit": { + "name": "weekly_credit_limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "limit_override_user_id_user_id_fk": { + "name": "limit_override_user_id_user_id_fk", + "tableFrom": "limit_override", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message": { + "name": "message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_request_id": { + "name": "client_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_message": { + "name": "last_message", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "\"message\".\"request\" -> -1", + "type": "stored" + } + }, + "reasoning_text": { + "name": "reasoning_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response": { + "name": "response", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_read_input_tokens": { + "name": "cache_read_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "numeric(100, 20)", + "primaryKey": false, + "notNull": true + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "byok": { + "name": "byok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ttft_ms": { + "name": "ttft_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "message_user_id_idx": { + "name": "message_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_finished_at_user_id_idx": { + "name": "message_finished_at_user_id_idx", + "columns": [ + { + "expression": "finished_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_org_id_idx": { + "name": "message_org_id_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_org_id_finished_at_idx": { + "name": "message_org_id_finished_at_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "finished_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_user_id_user_id_fk": { + "name": "message_user_id_user_id_fk", + "tableFrom": "message", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "message_org_id_org_id_fk": { + "name": "message_org_id_org_id_fk", + "tableFrom": "message", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org": { + "name": "org", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_period_start": { + "name": "current_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_end": { + "name": "current_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_topup_enabled": { + "name": "auto_topup_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_topup_threshold": { + "name": "auto_topup_threshold", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auto_topup_amount": { + "name": "auto_topup_amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credit_limit": { + "name": "credit_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "billing_alerts": { + "name": "billing_alerts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "usage_alerts": { + "name": "usage_alerts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "weekly_reports": { + "name": "weekly_reports", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "org_owner_id_user_id_fk": { + "name": "org_owner_id_user_id_fk", + "tableFrom": "org", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "org_slug_unique": { + "name": "org_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + }, + "org_stripe_customer_id_unique": { + "name": "org_stripe_customer_id_unique", + "nullsNotDistinct": false, + "columns": ["stripe_customer_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_feature": { + "name": "org_feature", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_org_feature_active": { + "name": "idx_org_feature_active", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_feature_org_id_org_id_fk": { + "name": "org_feature_org_id_org_id_fk", + "tableFrom": "org_feature", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "org_feature_org_id_feature_pk": { + "name": "org_feature_org_id_feature_pk", + "columns": ["org_id", "feature"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_invite": { + "name": "org_invite", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "org_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_by": { + "name": "accepted_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_org_invite_token": { + "name": "idx_org_invite_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_invite_email": { + "name": "idx_org_invite_email", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_invite_expires": { + "name": "idx_org_invite_expires", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_invite_org_id_org_id_fk": { + "name": "org_invite_org_id_org_id_fk", + "tableFrom": "org_invite", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_invite_invited_by_user_id_fk": { + "name": "org_invite_invited_by_user_id_fk", + "tableFrom": "org_invite", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "org_invite_accepted_by_user_id_fk": { + "name": "org_invite_accepted_by_user_id_fk", + "tableFrom": "org_invite", + "tableTo": "user", + "columnsFrom": ["accepted_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "org_invite_token_unique": { + "name": "org_invite_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_member": { + "name": "org_member", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "org_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "org_member_org_id_org_id_fk": { + "name": "org_member_org_id_org_id_fk", + "tableFrom": "org_member", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_member_user_id_user_id_fk": { + "name": "org_member_user_id_user_id_fk", + "tableFrom": "org_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "org_member_org_id_user_id_pk": { + "name": "org_member_org_id_user_id_pk", + "columns": ["org_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_repo": { + "name": "org_repo", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_name": { + "name": "repo_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_owner": { + "name": "repo_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_by": { + "name": "approved_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_org_repo_active": { + "name": "idx_org_repo_active", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_repo_unique": { + "name": "idx_org_repo_unique", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_repo_org_id_org_id_fk": { + "name": "org_repo_org_id_org_id_fk", + "tableFrom": "org_repo", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_repo_approved_by_user_id_fk": { + "name": "org_repo_approved_by_user_id_fk", + "tableFrom": "org_repo", + "tableTo": "user", + "columnsFrom": ["approved_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.publisher": { + "name": "publisher", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "publisher_user_id_user_id_fk": { + "name": "publisher_user_id_user_id_fk", + "tableFrom": "publisher", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "publisher_org_id_org_id_fk": { + "name": "publisher_org_id_org_id_fk", + "tableFrom": "publisher", + "tableTo": "org", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "publisher_created_by_user_id_fk": { + "name": "publisher_created_by_user_id_fk", + "tableFrom": "publisher", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "publisher_single_owner": { + "name": "publisher_single_owner", + "value": "(\"publisher\".\"user_id\" IS NOT NULL AND \"publisher\".\"org_id\" IS NULL) OR\n (\"publisher\".\"user_id\" IS NULL AND \"publisher\".\"org_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.referral": { + "name": "referral", + "schema": "", + "columns": { + "referrer_id": { + "name": "referrer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referred_id": { + "name": "referred_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "referral_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_legacy": { + "name": "is_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "referral_referrer_id_user_id_fk": { + "name": "referral_referrer_id_user_id_fk", + "tableFrom": "referral", + "tableTo": "user", + "columnsFrom": ["referrer_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "referral_referred_id_user_id_fk": { + "name": "referral_referred_id_user_id_fk", + "tableFrom": "referral", + "tableTo": "user", + "columnsFrom": ["referred_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "referral_referrer_id_referred_id_pk": { + "name": "referral_referrer_id_referred_id_pk", + "columns": ["referrer_id", "referred_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "fingerprint_id": { + "name": "fingerprint_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_auth_hash": { + "name": "cli_auth_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "session_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'web'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_cli_auth_code_idx": { + "name": "session_cli_auth_code_idx", + "columns": [ + { + "expression": "fingerprint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cli_auth_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"session\".\"cli_auth_hash\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_fingerprint_id_fingerprint_id_fk": { + "name": "session_fingerprint_id_fingerprint_id_fk", + "tableFrom": "session", + "tableTo": "fingerprint", + "columnsFrom": ["fingerprint_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_price_id": { + "name": "stripe_price_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scheduled_tier": { + "name": "scheduled_tier", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "subscription_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_subscription_customer": { + "name": "idx_subscription_customer", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_subscription_user": { + "name": "idx_subscription_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_subscription_status": { + "name": "idx_subscription_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"subscription\".\"status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "subscription_user_id_user_id_fk": { + "name": "subscription_user_id_user_id_fk", + "tableFrom": "subscription", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_failure": { + "name": "sync_failure", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_sync_failure_retry": { + "name": "idx_sync_failure_retry", + "columns": [ + { + "expression": "retry_count", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sync_failure\".\"retry_count\" < 5", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_quota_reset": { + "name": "next_quota_reset", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now() + INTERVAL '1 month'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "referral_code": { + "name": "referral_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'ref-' || gen_random_uuid()" + }, + "referral_limit": { + "name": "referral_limit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "discord_id": { + "name": "discord_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_topup_enabled": { + "name": "auto_topup_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_topup_threshold": { + "name": "auto_topup_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auto_topup_amount": { + "name": "auto_topup_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "fallback_to_a_la_carte": { + "name": "fallback_to_a_la_carte", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_stripe_customer_id_unique": { + "name": "user_stripe_customer_id_unique", + "nullsNotDistinct": false, + "columns": ["stripe_customer_id"] + }, + "user_referral_code_unique": { + "name": "user_referral_code_unique", + "nullsNotDistinct": false, + "columns": ["referral_code"] + }, + "user_discord_id_unique": { + "name": "user_discord_id_unique", + "nullsNotDistinct": false, + "columns": ["discord_id"] + }, + "user_handle_unique": { + "name": "user_handle_unique", + "nullsNotDistinct": false, + "columns": ["handle"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verificationToken": { + "name": "verificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": ["identifier", "token"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.referral_status": { + "name": "referral_status", + "schema": "public", + "values": ["pending", "completed"] + }, + "public.agent_run_status": { + "name": "agent_run_status", + "schema": "public", + "values": ["running", "completed", "failed", "cancelled"] + }, + "public.agent_step_status": { + "name": "agent_step_status", + "schema": "public", + "values": ["running", "completed", "skipped"] + }, + "public.api_key_type": { + "name": "api_key_type", + "schema": "public", + "values": ["anthropic", "gemini", "openai"] + }, + "public.free_session_status": { + "name": "free_session_status", + "schema": "public", + "values": ["queued", "active"] + }, + "public.grant_type": { + "name": "grant_type", + "schema": "public", + "values": [ + "free", + "referral", + "referral_legacy", + "subscription", + "purchase", + "admin", + "organization", + "ad" + ] + }, + "public.org_role": { + "name": "org_role", + "schema": "public", + "values": ["owner", "admin", "member"] + }, + "public.session_type": { + "name": "session_type", + "schema": "public", + "values": ["web", "pat", "cli"] + }, + "public.subscription_status": { + "name": "subscription_status", + "schema": "public", + "values": [ + "incomplete", + "incomplete_expired", + "trialing", + "active", + "past_due", + "canceled", + "unpaid", + "paused" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/internal/src/db/migrations/meta/_journal.json b/packages/internal/src/db/migrations/meta/_journal.json index 6dcc930048..d45dbc4759 100644 --- a/packages/internal/src/db/migrations/meta/_journal.json +++ b/packages/internal/src/db/migrations/meta/_journal.json @@ -358,6 +358,13 @@ "when": 1777936763321, "tag": "0050_overrated_stellaris", "breakpoints": true + }, + { + "idx": 51, + "version": "7", + "when": 1778611718988, + "tag": "0051_easy_sersi", + "breakpoints": true } ] } diff --git a/packages/internal/src/db/schema.ts b/packages/internal/src/db/schema.ts index 79357c2b61..e6c1d013c2 100644 --- a/packages/internal/src/db/schema.ts +++ b/packages/internal/src/db/schema.ts @@ -909,6 +909,54 @@ export const freeSession = pgTable( ], ) +/** + * Shared cache for free-mode country/privacy decisions. Raw IP addresses are + * never persisted; client_ip_hash is HMAC-SHA256 with the server auth secret. + */ +export const freeModeCountryAccessCache = pgTable( + 'free_mode_country_access_cache', + { + user_id: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + client_ip_hash: text('client_ip_hash').notNull(), + allowed: boolean('allowed').notNull(), + country_code: text('country_code'), + cf_country: text('cf_country'), + geoip_country: text('geoip_country'), + country_block_reason: text( + 'country_block_reason', + ).$type(), + ip_privacy_signals: text('ip_privacy_signals') + .array() + .$type(), + checked_at: timestamp('checked_at', { + mode: 'date', + withTimezone: true, + }).notNull(), + expires_at: timestamp('expires_at', { + mode: 'date', + withTimezone: true, + }).notNull(), + created_at: timestamp('created_at', { + mode: 'date', + withTimezone: true, + }) + .notNull() + .defaultNow(), + updated_at: timestamp('updated_at', { + mode: 'date', + withTimezone: true, + }) + .notNull() + .defaultNow(), + }, + (table) => [ + primaryKey({ columns: [table.user_id, table.client_ip_hash] }), + index('idx_free_mode_country_cache_expires_at').on(table.expires_at), + ], +) + /** * Audit log of every admission — one row per queued→active transition. Used * to track shared premium-session usage for Freebuff's 5 sessions per Pacific diff --git a/web/src/app/api/v1/chat/completions/__tests__/completions.test.ts b/web/src/app/api/v1/chat/completions/__tests__/completions.test.ts index 8bf708487e..b8bec42579 100644 --- a/web/src/app/api/v1/chat/completions/__tests__/completions.test.ts +++ b/web/src/app/api/v1/chat/completions/__tests__/completions.test.ts @@ -12,6 +12,7 @@ import { import { openCodeZenModels } from '@codebuff/common/constants/model-config' import { postChatCompletions } from '../_post' import { resetFreeModeRateLimits } from '../free-mode-rate-limiter' +import { getFreeModeCountryAccess } from '@/server/free-mode-country' import type { TrackEventFn } from '@codebuff/common/types/contracts/analytics' import type { InsertMessageBigqueryFn } from '@codebuff/common/types/contracts/bigquery' @@ -86,6 +87,18 @@ describe('/api/v1/chat/completions POST endpoint', () => { // path so downstream logic proceeds normally. const mockCheckSessionAdmissibleAllow = async () => ({ ok: true, reason: 'disabled' }) as const + const mockResolveFreeModeCountryAccess = async ( + _userId: string, + req: Parameters[0], + options: Parameters[1], + ) => getFreeModeCountryAccess(req, options) + const postChatCompletionsForTest = ( + params: Parameters[0], + ) => + postChatCompletions({ + resolveFreeModeCountryAccess: mockResolveFreeModeCountryAccess, + ...params, + }) const allowedFreeModeHeaders = (apiKey: string) => ({ Authorization: `Bearer ${apiKey}`, @@ -289,7 +302,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -317,7 +330,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -347,7 +360,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -375,7 +388,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -406,7 +419,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -439,7 +452,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -474,7 +487,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -509,7 +522,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -548,7 +561,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -591,7 +604,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -633,7 +646,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -671,7 +684,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -713,7 +726,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -755,7 +768,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -818,7 +831,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -910,7 +923,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -1027,7 +1040,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -1084,7 +1097,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -1123,7 +1136,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -1159,7 +1172,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -1197,7 +1210,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -1233,7 +1246,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -1252,7 +1265,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }) it('rejects the Gemini thinker subagent when the session gate rejects it', async () => { - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req: new NextRequest('http://localhost:3000/api/v1/chat/completions', { method: 'POST', headers: allowedFreeModeHeaders('test-api-key-new-free-gemini'), @@ -1300,7 +1313,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { return { limited: false as const } }) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req: new NextRequest( 'http://localhost:3000/api/v1/chat/completions', { @@ -1387,8 +1400,10 @@ describe('/api/v1/chat/completions POST endpoint', () => { checkFreeModeRateLimit: checkFreeModeRateLimitForTest, }) - const firstResponse = await postChatCompletions(createPostParams()) - const limitedResponse = await postChatCompletions(createPostParams()) + const firstResponse = + await postChatCompletionsForTest(createPostParams()) + const limitedResponse = + await postChatCompletionsForTest(createPostParams()) expect(firstResponse.status).toBe(200) expect(limitedResponse.status).toBe(429) @@ -1419,7 +1434,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -1456,7 +1471,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -1494,7 +1509,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -1530,7 +1545,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -1569,7 +1584,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -1614,7 +1629,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -1675,7 +1690,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { fallbackToALaCarte: false, })) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req: createValidRequest(), getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -1732,7 +1747,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req: freeModeRequest, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -1768,7 +1783,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { fallbackToALaCarte: false, })) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req: createValidRequest(), getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -1808,7 +1823,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { fallbackToALaCarte: true, })) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req: createValidRequest(), getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -1843,7 +1858,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { fallbackToALaCarte: false, })) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req: createValidRequest(), getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -1873,7 +1888,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { fallbackToALaCarte: false, })) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req: createValidRequest(), getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -1901,7 +1916,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { fallbackToALaCarte: false, })) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req: createValidRequest(), getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -1936,7 +1951,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { async () => weeklyLimitError, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req: createValidRequest(), getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -2001,7 +2016,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -2037,7 +2052,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, @@ -2073,7 +2088,7 @@ describe('/api/v1/chat/completions POST endpoint', () => { }, ) - const response = await postChatCompletions({ + const response = await postChatCompletionsForTest({ req, getUserInfoFromApiKey: mockGetUserInfoFromApiKey, logger: mockLogger, diff --git a/web/src/app/api/v1/chat/completions/_post.ts b/web/src/app/api/v1/chat/completions/_post.ts index 6a61be1739..2adaea3dde 100644 --- a/web/src/app/api/v1/chat/completions/_post.ts +++ b/web/src/app/api/v1/chat/completions/_post.ts @@ -85,9 +85,13 @@ import { OpenRouterError, } from '@/llm-api/openrouter' import { checkSessionAdmissible } from '@/server/free-session/public-api' -import { getFreeModeCountryAccess } from '@/server/free-mode-country' +import { getCachedFreeModeCountryAccess } from '@/server/free-mode-country-access-cache' import type { SessionGateResult } from '@/server/free-session/public-api' +import type { + FreeModeCountryAccess, + FreeModeCountryAccessOptions, +} from '@/server/free-mode-country' import { extractApiKeyFromHeader } from '@/util/auth' import { withDefaultProperties } from '@codebuff/common/analytics' import { checkFreeModeRateLimit as defaultCheckFreeModeRateLimit } from './free-mode-rate-limiter' @@ -130,6 +134,11 @@ export const formatQuotaResetCountdown = ( export type CheckSessionAdmissibleFn = typeof checkSessionAdmissible export type CheckFreeModeRateLimitFn = typeof defaultCheckFreeModeRateLimit +export type ResolveFreeModeCountryAccessFn = ( + userId: string, + req: NextRequest, + options: FreeModeCountryAccessOptions, +) => Promise const FREEBUFF_SUCCESS_SAMPLE_RATE = 0.01 @@ -174,6 +183,9 @@ export async function postChatCompletions(params: { /** Optional override for the free-mode rate limiter. Tests inject this to * avoid coupling to process-global limiter state. */ checkFreeModeRateLimit?: CheckFreeModeRateLimitFn + /** Optional override for country/cache checks. Tests inject this to avoid + * coupling to Postgres-backed cache state. */ + resolveFreeModeCountryAccess?: ResolveFreeModeCountryAccessFn }) { const { req, @@ -187,9 +199,14 @@ export async function postChatCompletions(params: { getUserPreferences, checkSessionAdmissible: checkSession = checkSessionAdmissible, checkFreeModeRateLimit = defaultCheckFreeModeRateLimit, + resolveFreeModeCountryAccess, } = params let { logger } = params let { trackEvent } = params + const resolveCountryAccess: ResolveFreeModeCountryAccessFn = + resolveFreeModeCountryAccess ?? + ((userId, req, options) => + getCachedFreeModeCountryAccess({ userId, req, options, logger })) try { // Parse request body @@ -470,7 +487,7 @@ export async function postChatCompletions(params: { isFreeModeRequest && (!freeModeSessionGate || freeModeSessionGate.reason === 'disabled') ) { - const countryAccess = await getFreeModeCountryAccess(req, { + const countryAccess = await resolveCountryAccess(userId, req, { fetch, ipinfoToken: env.IPINFO_TOKEN, ipHashSecret: env.NEXTAUTH_SECRET, diff --git a/web/src/server/__tests__/free-mode-country-access-cache.test.ts b/web/src/server/__tests__/free-mode-country-access-cache.test.ts new file mode 100644 index 0000000000..7fd16cd690 --- /dev/null +++ b/web/src/server/__tests__/free-mode-country-access-cache.test.ts @@ -0,0 +1,170 @@ +import { describe, expect, mock, test } from 'bun:test' +import { NextRequest } from 'next/server' + +import { + expiresAtForCountryAccess, + FREE_MODE_COUNTRY_CACHE_ALLOWED_TTL_MS, + FREE_MODE_COUNTRY_CACHE_ANONYMOUS_NETWORK_TTL_MS, + FREE_MODE_COUNTRY_CACHE_COUNTRY_NOT_ALLOWED_TTL_MS, + FREE_MODE_COUNTRY_CACHE_TRANSIENT_BLOCK_TTL_MS, + getCachedFreeModeCountryAccess, +} from '../free-mode-country-access-cache' +import { hashClientIp } from '../free-mode-country' + +import type { FreeModeCountryAccess } from '../free-mode-country' +import type { FreeModeCountryAccessCacheStore } from '../free-mode-country-access-cache' + +const now = new Date('2026-05-12T12:00:00Z') +const userId = 'user-123' +const ipHashSecret = 'test-secret' +const clientIp = '203.0.113.10' +const clientIpHash = hashClientIp(clientIp, ipHashSecret)! + +function makeReq(headers: Record = {}): NextRequest { + return new NextRequest('http://localhost:3000/api/v1/chat/completions', { + headers, + }) +} + +function allowedAccess(): FreeModeCountryAccess { + return { + allowed: true, + countryCode: 'US', + blockReason: null, + cfCountry: 'US', + geoipCountry: null, + ipPrivacy: { signals: [] }, + hasClientIp: true, + clientIpHash, + } +} + +describe('free mode country access cache', () => { + test('uses a fresh cached country decision without calling IPinfo', async () => { + const cached = allowedAccess() + const cacheStore: FreeModeCountryAccessCacheStore = { + get: mock(async () => cached), + set: mock(async () => {}), + } + const fetch = mock(async () => { + throw new Error('IPinfo should not be called on cache hit') + }) as unknown as typeof globalThis.fetch + + const access = await getCachedFreeModeCountryAccess({ + userId, + req: makeReq({ + 'cf-ipcountry': 'US', + 'cf-connecting-ip': clientIp, + }), + options: { + fetch, + ipinfoToken: 'test-token', + ipHashSecret, + }, + cacheStore, + now, + }) + + expect(access).toBe(cached) + expect(cacheStore.get).toHaveBeenCalledWith({ + userId, + clientIpHash, + cfCountry: 'US', + now, + }) + expect(cacheStore.set).not.toHaveBeenCalled() + expect(fetch).not.toHaveBeenCalled() + }) + + test('stores a fresh country decision after a cache miss', async () => { + const stored: FreeModeCountryAccess[] = [] + const cacheStore: FreeModeCountryAccessCacheStore = { + get: mock(async () => null), + set: mock(async ({ access }) => { + stored.push(access) + }), + } + const fetch = mock(async () => + Response.json({}), + ) as unknown as typeof globalThis.fetch + + const access = await getCachedFreeModeCountryAccess({ + userId, + req: makeReq({ + 'cf-ipcountry': 'US', + 'cf-connecting-ip': clientIp, + }), + options: { + fetch, + ipinfoToken: 'test-token', + ipHashSecret, + }, + cacheStore, + now, + }) + + expect(access.allowed).toBe(true) + expect(access.countryCode).toBe('US') + expect(stored[0]).toEqual(access) + expect(fetch).toHaveBeenCalledTimes(1) + }) + + test('refreshes when the cache store reports a stale entry', async () => { + const stale = allowedAccess() + const staleRefreshIp = '203.0.113.11' + const cacheStore: FreeModeCountryAccessCacheStore = { + get: mock(async ({ now: cacheNow }) => + cacheNow.getTime() < now.getTime() ? stale : null, + ), + set: mock(async () => {}), + } + const fetch = mock(async () => + Response.json({}), + ) as unknown as typeof globalThis.fetch + + const access = await getCachedFreeModeCountryAccess({ + userId, + req: makeReq({ + 'cf-ipcountry': 'US', + 'cf-connecting-ip': staleRefreshIp, + }), + options: { + fetch, + ipinfoToken: 'test-token', + ipHashSecret, + }, + cacheStore, + now, + }) + + expect(access.allowed).toBe(true) + expect(cacheStore.set).toHaveBeenCalled() + expect(fetch).toHaveBeenCalledTimes(1) + }) + + test('uses shorter TTLs for VPN and transient blocks than country blocks', () => { + const base = allowedAccess() + + expect(expiresAtForCountryAccess(base, now).getTime() - now.getTime()).toBe( + FREE_MODE_COUNTRY_CACHE_ALLOWED_TTL_MS, + ) + expect( + expiresAtForCountryAccess( + { ...base, allowed: false, blockReason: 'anonymous_network' }, + now, + ).getTime() - now.getTime(), + ).toBe(FREE_MODE_COUNTRY_CACHE_ANONYMOUS_NETWORK_TTL_MS) + expect( + expiresAtForCountryAccess( + { ...base, allowed: false, blockReason: 'country_not_allowed' }, + now, + ).getTime() - now.getTime(), + ).toBe(FREE_MODE_COUNTRY_CACHE_COUNTRY_NOT_ALLOWED_TTL_MS) + expect( + expiresAtForCountryAccess( + { ...base, allowed: false, blockReason: 'ip_privacy_lookup_failed' }, + now, + ).getTime() - now.getTime(), + ).toBe(FREE_MODE_COUNTRY_CACHE_TRANSIENT_BLOCK_TTL_MS) + }) +}) diff --git a/web/src/server/free-mode-country-access-cache.ts b/web/src/server/free-mode-country-access-cache.ts new file mode 100644 index 0000000000..877eba316b --- /dev/null +++ b/web/src/server/free-mode-country-access-cache.ts @@ -0,0 +1,190 @@ +import { db } from '@codebuff/internal/db' +import * as schema from '@codebuff/internal/db/schema' +import { getErrorObject } from '@codebuff/common/util/error' +import { and, eq, gt, isNull } from 'drizzle-orm' + +import { + extractClientIp, + getFreeModeCountryAccess, + hashClientIp, + IPINFO_PRIVACY_CACHE_TTL_MS, +} from './free-mode-country' + +import type { + FreeModeCountryAccess, + FreeModeCountryAccessOptions, +} from './free-mode-country' +import type { Logger } from '@codebuff/common/types/contracts/logger' + +export const FREE_MODE_COUNTRY_CACHE_ALLOWED_TTL_MS = + IPINFO_PRIVACY_CACHE_TTL_MS +export const FREE_MODE_COUNTRY_CACHE_ANONYMOUS_NETWORK_TTL_MS = 15 * 60 * 1000 +export const FREE_MODE_COUNTRY_CACHE_COUNTRY_NOT_ALLOWED_TTL_MS = + 6 * 60 * 60 * 1000 +export const FREE_MODE_COUNTRY_CACHE_TRANSIENT_BLOCK_TTL_MS = 5 * 60 * 1000 + +export type FreeModeCountryAccessCacheStore = { + get(params: { + userId: string + clientIpHash: string + cfCountry: string | null + now: Date + }): Promise + set(params: { + userId: string + access: FreeModeCountryAccess + now: Date + }): Promise +} + +export function expiresAtForCountryAccess( + access: FreeModeCountryAccess, + now: Date, +): Date { + let ttlMs = FREE_MODE_COUNTRY_CACHE_TRANSIENT_BLOCK_TTL_MS + if (access.allowed) { + ttlMs = FREE_MODE_COUNTRY_CACHE_ALLOWED_TTL_MS + } else if (access.blockReason === 'anonymous_network') { + ttlMs = FREE_MODE_COUNTRY_CACHE_ANONYMOUS_NETWORK_TTL_MS + } else if (access.blockReason === 'country_not_allowed') { + ttlMs = FREE_MODE_COUNTRY_CACHE_COUNTRY_NOT_ALLOWED_TTL_MS + } + return new Date(now.getTime() + ttlMs) +} + +function countryAccessFromCacheRow( + row: typeof schema.freeModeCountryAccessCache.$inferSelect, +): FreeModeCountryAccess { + return { + allowed: row.allowed, + countryCode: row.country_code, + blockReason: row.country_block_reason, + cfCountry: row.cf_country, + geoipCountry: row.geoip_country, + ipPrivacy: row.ip_privacy_signals + ? { signals: row.ip_privacy_signals } + : null, + hasClientIp: true, + clientIpHash: row.client_ip_hash, + } +} + +export const dbFreeModeCountryAccessCacheStore: FreeModeCountryAccessCacheStore = + { + async get({ userId, clientIpHash, cfCountry, now }) { + const row = await db.query.freeModeCountryAccessCache.findFirst({ + where: and( + eq(schema.freeModeCountryAccessCache.user_id, userId), + eq(schema.freeModeCountryAccessCache.client_ip_hash, clientIpHash), + cfCountry === null + ? isNull(schema.freeModeCountryAccessCache.cf_country) + : eq(schema.freeModeCountryAccessCache.cf_country, cfCountry), + gt(schema.freeModeCountryAccessCache.expires_at, now), + ), + }) + if (!row) return null + return countryAccessFromCacheRow(row) + }, + + async set({ userId, access, now }) { + if (!access.clientIpHash) return + + const expiresAt = expiresAtForCountryAccess(access, now) + await db + .insert(schema.freeModeCountryAccessCache) + .values({ + user_id: userId, + client_ip_hash: access.clientIpHash, + allowed: access.allowed, + country_code: access.countryCode, + cf_country: access.cfCountry, + geoip_country: access.geoipCountry, + country_block_reason: access.blockReason, + ip_privacy_signals: access.ipPrivacy?.signals ?? null, + checked_at: now, + expires_at: expiresAt, + created_at: now, + updated_at: now, + }) + .onConflictDoUpdate({ + target: [ + schema.freeModeCountryAccessCache.user_id, + schema.freeModeCountryAccessCache.client_ip_hash, + ], + set: { + allowed: access.allowed, + country_code: access.countryCode, + cf_country: access.cfCountry, + geoip_country: access.geoipCountry, + country_block_reason: access.blockReason, + ip_privacy_signals: access.ipPrivacy?.signals ?? null, + checked_at: now, + expires_at: expiresAt, + updated_at: now, + }, + }) + }, + } + +export async function getCachedFreeModeCountryAccess(params: { + userId: string + req: Parameters[0] + options: FreeModeCountryAccessOptions + cacheStore?: FreeModeCountryAccessCacheStore + logger?: Logger + now?: Date +}): Promise { + const { + userId, + req, + options, + cacheStore = dbFreeModeCountryAccessCacheStore, + logger, + now = new Date(), + } = params + const cfCountry = req.headers.get('cf-ipcountry')?.toUpperCase() ?? null + const clientIp = extractClientIp(req) + const clientIpHash = hashClientIp(clientIp, options.ipHashSecret) + + if (clientIpHash) { + try { + const cached = await cacheStore.get({ + userId, + clientIpHash, + cfCountry, + now, + }) + if (cached) return cached + } catch (error) { + logger?.warn( + { + userId, + clientIpHash, + error: getErrorObject(error), + }, + 'Free mode country access cache read failed', + ) + // Cache failures should not make free-mode availability depend on DB + // health; fall back to the direct country/privacy check. + } + } + + const access = await getFreeModeCountryAccess(req, options) + if (access.clientIpHash) { + try { + await cacheStore.set({ userId, access, now }) + } catch (error) { + logger?.warn( + { + userId, + clientIpHash: access.clientIpHash, + error: getErrorObject(error), + }, + 'Free mode country access cache write failed', + ) + // Best-effort cache write. The direct country/privacy result is still + // authoritative for this request. + } + } + return access +} diff --git a/web/src/server/free-mode-country.ts b/web/src/server/free-mode-country.ts index d586a55eb0..6a64f6b321 100644 --- a/web/src/server/free-mode-country.ts +++ b/web/src/server/free-mode-country.ts @@ -60,7 +60,7 @@ export type LookupIpPrivacyFn = ( ip: string, ) => Promise -type FreeModeCountryAccessOptions = { +export type FreeModeCountryAccessOptions = { lookupIpPrivacy?: LookupIpPrivacyFn fetch?: typeof globalThis.fetch ipinfoToken: string @@ -113,7 +113,7 @@ export function extractClientIp(req: NextRequest): string | undefined { return undefined } -function hashClientIp( +export function hashClientIp( clientIp: string | undefined, secret: string | undefined, ): string | null {