From cbc89b3435bfde87e3394f5386a13de11d9980dd Mon Sep 17 00:00:00 2001 From: Erik Olsson Date: Mon, 4 May 2026 14:05:37 +0200 Subject: [PATCH] server: enforce one API key per device at the schema level MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add unique index on api_keys.device_id. Without this, a regression in the /v1/auth/exchange redemption flow (which does DELETE + INSERT on api_keys keyed by device_id) could leave two rows for the same device — the exact corrupted state a recent audit identified as reachable via concurrent setup-token redemptions. With the index in place, the second INSERT violates the constraint and fails loudly instead of silently producing two long-lived API keys. No defensive cleanup in the migration: if rows violating the constraint exist, that is itself a signal worth investigating, not silently fixing. Auto-deleting "older" rows could just as easily revoke the legitimate user's key as the attacker's, depending on which redemption arrived first. api_keys is small (~one row per active device), so a non-CONCURRENT CREATE UNIQUE INDEX completes in milliseconds. Refresh docs/generated/db-schema.md to reflect the new index. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../drizzle/0016_dazzling_victor_mancha.sql | 1 + apps/server/drizzle/meta/0016_snapshot.json | 2313 +++++++++++++++++ apps/server/drizzle/meta/_journal.json | 7 + apps/server/src/db/schema.ts | 30 +- docs/generated/db-schema.md | 3 + 5 files changed, 2342 insertions(+), 12 deletions(-) create mode 100644 apps/server/drizzle/0016_dazzling_victor_mancha.sql create mode 100644 apps/server/drizzle/meta/0016_snapshot.json diff --git a/apps/server/drizzle/0016_dazzling_victor_mancha.sql b/apps/server/drizzle/0016_dazzling_victor_mancha.sql new file mode 100644 index 00000000..e32b3244 --- /dev/null +++ b/apps/server/drizzle/0016_dazzling_victor_mancha.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "api_keys_device_unique" ON "api_keys" USING btree ("device_id"); \ No newline at end of file diff --git a/apps/server/drizzle/meta/0016_snapshot.json b/apps/server/drizzle/meta/0016_snapshot.json new file mode 100644 index 00000000..347f810d --- /dev/null +++ b/apps/server/drizzle/meta/0016_snapshot.json @@ -0,0 +1,2313 @@ +{ + "id": "9b70c016-1a55-4f47-a080-9eefd77a2756", + "prevId": "76e66b47-d043-411c-9ce2-e416ab904fb8", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.agent_sessions": { + "name": "agent_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_login": { + "name": "user_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_activity": { + "name": "last_activity", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary_model": { + "name": "summary_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary_ts": { + "name": "summary_ts", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "agent_sessions_user_session_key": { + "name": "agent_sessions_user_session_key", + "columns": [ + { + "expression": "user_login", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_sessions_user_started_idx": { + "name": "agent_sessions_user_started_idx", + "columns": [ + { + "expression": "user_login", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_sessions_agent_started_idx": { + "name": "agent_sessions_agent_started_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "api_keys_device_unique": { + "name": "api_keys_device_unique", + "columns": [ + { + "expression": "device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_keys_device_id_devices_id_fk": { + "name": "api_keys_device_id_devices_id_fk", + "tableFrom": "api_keys", + "tableTo": "devices", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_keys_key_hash_unique": { + "name": "api_keys_key_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "key_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_messages": { + "name": "chat_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "thread_id": { + "name": "thread_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "turn_index": { + "name": "turn_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "answer": { + "name": "answer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "citations": { + "name": "citations", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "delegation": { + "name": "delegation", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "chat_messages_user_created_idx": { + "name": "chat_messages_user_created_idx", + "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": {} + }, + "chat_messages_thread_idx": { + "name": "chat_messages_thread_idx", + "columns": [ + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "turn_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_messages_user_id_users_id_fk": { + "name": "chat_messages_user_id_users_id_fk", + "tableFrom": "chat_messages", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_excluded_repos": { + "name": "device_excluded_repos", + "schema": "", + "columns": { + "device_id": { + "name": "device_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "repo_id": { + "name": "repo_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "device_excluded_repos_device_id_devices_id_fk": { + "name": "device_excluded_repos_device_id_devices_id_fk", + "tableFrom": "device_excluded_repos", + "tableTo": "devices", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "device_excluded_repos_repo_id_repos_id_fk": { + "name": "device_excluded_repos_repo_id_repos_id_fk", + "tableFrom": "device_excluded_repos", + "tableTo": "repos", + "columnsFrom": [ + "repo_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "device_excluded_repos_device_id_repo_id_pk": { + "name": "device_excluded_repos_device_id_repo_id_pk", + "columns": [ + "device_id", + "repo_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_repo_paths": { + "name": "device_repo_paths", + "schema": "", + "columns": { + "device_id": { + "name": "device_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "repo_id": { + "name": "repo_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "local_path": { + "name": "local_path", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "device_repo_paths_device_id_devices_id_fk": { + "name": "device_repo_paths_device_id_devices_id_fk", + "tableFrom": "device_repo_paths", + "tableTo": "devices", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "device_repo_paths_repo_id_repos_id_fk": { + "name": "device_repo_paths_repo_id_repos_id_fk", + "tableFrom": "device_repo_paths", + "tableTo": "repos", + "columnsFrom": [ + "repo_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "device_repo_paths_device_id_repo_id_pk": { + "name": "device_repo_paths_device_id_repo_id_pk", + "columns": [ + "device_id", + "repo_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.devices": { + "name": "devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_name": { + "name": "device_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "os": { + "name": "os", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "devices_user_name_unique": { + "name": "devices_user_name_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "device_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "devices_user_id_users_id_fk": { + "name": "devices_user_id_users_id_fk", + "tableFrom": "devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events": { + "name": "events", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "line_seq": { + "name": "line_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "project": { + "name": "project", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "raw_type": { + "name": "raw_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "turn_id": { + "name": "turn_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "call_id": { + "name": "call_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "ingested_at": { + "name": "ingested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "events_session_ts_idx": { + "name": "events_session_ts_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ts", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_user_project_ts_idx": { + "name": "events_user_project_ts_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ts", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_call_idx": { + "name": "events_call_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "call_id is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_turn_idx": { + "name": "events_turn_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "turn_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "turn_id is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_event_id_idx": { + "name": "events_event_id_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "event_id is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "events_session_id_sessions_session_id_fk": { + "name": "events_session_id_sessions_session_id_fk", + "tableFrom": "events", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "events_session_id_line_seq_pk": { + "name": "events_session_id_line_seq_pk", + "columns": [ + "session_id", + "line_seq" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeats": { + "name": "heartbeats", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pid": { + "name": "pid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "heartbeats_session_id_sessions_session_id_fk": { + "name": "heartbeats_session_id_sessions_session_id_fk", + "tableFrom": "heartbeats", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_authorization_codes": { + "name": "oauth_authorization_codes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "oauth_authorization_codes_code_hash_key": { + "name": "oauth_authorization_codes_code_hash_key", + "columns": [ + { + "expression": "code_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_authorization_codes_user_id_idx": { + "name": "oauth_authorization_codes_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_authorization_codes_client_id_idx": { + "name": "oauth_authorization_codes_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_authorization_codes_user_id_users_id_fk": { + "name": "oauth_authorization_codes_user_id_users_id_fk", + "tableFrom": "oauth_authorization_codes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_clients": { + "name": "oauth_clients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_kind": { + "name": "client_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "grant_types": { + "name": "grant_types", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "response_types": { + "name": "response_types", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "oauth_clients_client_id_key": { + "name": "oauth_clients_client_id_key", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_clients_kind_idx": { + "name": "oauth_clients_kind_idx", + "columns": [ + { + "expression": "client_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_tokens": { + "name": "oauth_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_hash": { + "name": "access_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token_hash": { + "name": "refresh_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_expires_at": { + "name": "access_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "refresh_expires_at": { + "name": "refresh_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "oauth_tokens_access_token_hash_key": { + "name": "oauth_tokens_access_token_hash_key", + "columns": [ + { + "expression": "access_token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_tokens_refresh_token_hash_key": { + "name": "oauth_tokens_refresh_token_hash_key", + "columns": [ + { + "expression": "refresh_token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_tokens_user_id_idx": { + "name": "oauth_tokens_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_tokens_client_id_idx": { + "name": "oauth_tokens_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_tokens_user_id_users_id_fk": { + "name": "oauth_tokens_user_id_users_id_fk", + "tableFrom": "oauth_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pull_requests": { + "name": "pull_requests", + "schema": "", + "columns": { + "repo_id": { + "name": "repo_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "head_ref": { + "name": "head_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_login": { + "name": "author_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "pull_requests_repo_head_ref_idx": { + "name": "pull_requests_repo_head_ref_idx", + "columns": [ + { + "expression": "repo_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "head_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pull_requests_repo_id_repos_id_fk": { + "name": "pull_requests_repo_id_repos_id_fk", + "tableFrom": "pull_requests", + "tableTo": "repos", + "columnsFrom": [ + "repo_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "pull_requests_repo_id_number_pk": { + "name": "pull_requests_repo_id_number_pk", + "columns": [ + "repo_id", + "number" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.refresh_tokens": { + "name": "refresh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "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": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "refresh_tokens_user_id_users_id_fk": { + "name": "refresh_tokens_user_id_users_id_fk", + "tableFrom": "refresh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "refresh_tokens_token_hash_unique": { + "name": "refresh_tokens_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "token_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.repos": { + "name": "repos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "github_id": { + "name": "github_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "full_name": { + "name": "full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private": { + "name": "private", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "repos_github_id_unique": { + "name": "repos_github_id_unique", + "nullsNotDistinct": false, + "columns": [ + "github_id" + ] + }, + "repos_full_name_unique": { + "name": "repos_full_name_unique", + "nullsNotDistinct": false, + "columns": [ + "full_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_insights": { + "name": "session_insights", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "analyzer_name": { + "name": "analyzer_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "analyzer_version": { + "name": "analyzer_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "input_line_seq": { + "name": "input_line_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tokens_in": { + "name": "tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "tokens_out": { + "name": "tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "tokens_cache_read": { + "name": "tokens_cache_read", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "cost_usd": { + "name": "cost_usd", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "analyzed_at": { + "name": "analyzed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "error_text": { + "name": "error_text", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_insights_analyzed_at_idx": { + "name": "session_insights_analyzed_at_idx", + "columns": [ + { + "expression": "analyzed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_insights_session_id_sessions_session_id_fk": { + "name": "session_insights_session_id_sessions_session_id_fk", + "tableFrom": "session_insights", + "tableTo": "sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_insights_session_id_analyzer_name_pk": { + "name": "session_insights_session_id_analyzer_name_pk", + "columns": [ + "session_id", + "analyzer_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project": { + "name": "project", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_id": { + "name": "repo_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_ts": { + "name": "first_ts", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_ts": { + "name": "last_ts", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "user_msgs": { + "name": "user_msgs", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "assistant_msgs": { + "name": "assistant_msgs", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "tool_calls": { + "name": "tool_calls", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "tool_errors": { + "name": "tool_errors", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "events": { + "name": "events", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "tokens_in": { + "name": "tokens_in", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "tokens_out": { + "name": "tokens_out", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "tokens_cache_read": { + "name": "tokens_cache_read", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "tokens_cache_write": { + "name": "tokens_cache_write", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "tokens_reasoning": { + "name": "tokens_reasoning", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_turn": { + "name": "in_turn", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "current_turn_id": { + "name": "current_turn_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_boundary_ts": { + "name": "last_boundary_ts", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "outstanding_tools": { + "name": "outstanding_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "last_user_prompt": { + "name": "last_user_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "top_files_read": { + "name": "top_files_read", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "top_files_edited": { + "name": "top_files_edited", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "top_files_written": { + "name": "top_files_written", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "tool_use_names": { + "name": "tool_use_names", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "queued": { + "name": "queued", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "recent_events": { + "name": "recent_events", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "recent_prompts": { + "name": "recent_prompts", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "server_line_seq": { + "name": "server_line_seq", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "prefix_hash": { + "name": "prefix_hash", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sessions_user_last_ts_idx": { + "name": "sessions_user_last_ts_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_ts", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_repo_last_ts_idx": { + "name": "sessions_repo_last_ts_idx", + "columns": [ + { + "expression": "repo_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_ts", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_device_id_devices_id_fk": { + "name": "sessions_device_id_devices_id_fk", + "tableFrom": "sessions", + "tableTo": "devices", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "sessions_repo_id_repos_id_fk": { + "name": "sessions_repo_id_repos_id_fk", + "tableFrom": "sessions", + "tableTo": "repos", + "columnsFrom": [ + "repo_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.setup_tokens": { + "name": "setup_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "redeemed": { + "name": "redeemed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "setup_tokens_user_id_users_id_fk": { + "name": "setup_tokens_user_id_users_id_fk", + "tableFrom": "setup_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "setup_tokens_token_unique": { + "name": "setup_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_repos": { + "name": "user_repos", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "repo_id": { + "name": "repo_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "user_repos_repo_id_idx": { + "name": "user_repos_repo_id_idx", + "columns": [ + { + "expression": "repo_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_repos_user_id_users_id_fk": { + "name": "user_repos_user_id_users_id_fk", + "tableFrom": "user_repos", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_repos_repo_id_repos_id_fk": { + "name": "user_repos_repo_id_repos_id_fk", + "tableFrom": "user_repos", + "tableTo": "repos", + "columnsFrom": [ + "repo_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_repos_user_id_repo_id_pk": { + "name": "user_repos_user_id_repo_id_pk", + "columns": [ + "user_id", + "repo_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "github_id": { + "name": "github_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "github_login": { + "name": "github_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_token": { + "name": "github_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_app_user_token": { + "name": "github_app_user_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_app_refresh_token": { + "name": "github_app_refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_app_token_expires_at": { + "name": "github_app_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "github_app_refresh_token_expires_at": { + "name": "github_app_refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "github_app_connected_at": { + "name": "github_app_connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "credentials_revoked_at": { + "name": "credentials_revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_github_id_unique": { + "name": "users_github_id_unique", + "nullsNotDistinct": false, + "columns": [ + "github_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/server/drizzle/meta/_journal.json b/apps/server/drizzle/meta/_journal.json index 0efbf84f..6c566a6a 100644 --- a/apps/server/drizzle/meta/_journal.json +++ b/apps/server/drizzle/meta/_journal.json @@ -113,6 +113,13 @@ "when": 1777445485607, "tag": "0015_mushy_mikhail_rasputin", "breakpoints": true + }, + { + "idx": 16, + "version": "7", + "when": 1777896187977, + "tag": "0016_dazzling_victor_mancha", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/server/src/db/schema.ts b/apps/server/src/db/schema.ts index 21ab33de..df47af88 100644 --- a/apps/server/src/db/schema.ts +++ b/apps/server/src/db/schema.ts @@ -70,18 +70,24 @@ export const devices = pgTable( (t) => [uniqueIndex("devices_user_name_unique").on(t.userId, t.deviceName)], ); -export const apiKeys = pgTable("api_keys", { - id: serial("id").primaryKey(), - userId: integer("user_id") - .references(() => users.id, { onDelete: "cascade" }) - .notNull(), - deviceId: integer("device_id") - .references(() => devices.id, { onDelete: "cascade" }) - .notNull(), - keyHash: text("key_hash").unique().notNull(), - lastUsedAt: timestamp("last_used_at", { withTimezone: true }), - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(), -}); +export const apiKeys = pgTable( + "api_keys", + { + id: serial("id").primaryKey(), + userId: integer("user_id") + .references(() => users.id, { onDelete: "cascade" }) + .notNull(), + deviceId: integer("device_id") + .references(() => devices.id, { onDelete: "cascade" }) + .notNull(), + keyHash: text("key_hash").unique().notNull(), + lastUsedAt: timestamp("last_used_at", { withTimezone: true }), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(), + }, + // One active API key per device. Schema-level guarantee so a future + // regression in the redemption flow can't leave two valid keys per device. + (t) => [uniqueIndex("api_keys_device_unique").on(t.deviceId)], +); export const setupTokens = pgTable("setup_tokens", { id: serial("id").primaryKey(), diff --git a/docs/generated/db-schema.md b/docs/generated/db-schema.md index 8941a3d0..30645e4a 100644 --- a/docs/generated/db-schema.md +++ b/docs/generated/db-schema.md @@ -42,6 +42,9 @@ Drizzle export: `apiKeys`. | `last_used_at` | `PgTimestamp` | — | | `created_at` | `PgTimestamp` | has default | +**Indexes:** +- `api_keys_device_unique` (unique index) on `(device_id)` + ## `chat_messages` Drizzle export: `chatMessages`.