From a87977241ce2c5c36d8b6fac5c59a444044a006b Mon Sep 17 00:00:00 2001 From: Christopher Date: Wed, 28 Jan 2026 06:07:03 -0800 Subject: [PATCH] Refactor chat encryption to use sodium, update API Replaces legacy chat encryption modules with new sodium-based implementation. Adds and updates API routes for chat key management and debugging, introduces new hooks and scripts for sodium chat, and updates related database schema and configuration. Removes old crypto modules and updates dependencies accordingly. --- drizzle/meta/0011_snapshot.json | 4799 ++++++++++++++++ drizzle/meta/0012_snapshot.json | 4805 +++++++++++++++++ drizzle/meta/_journal.json | 14 + next.config.ts | 3 +- package-lock.json | 35 + package.json | 1 + scripts/clear-chat-keys.ts | 16 + .../.well-known/synapsis/chat/[did]/route.ts | 35 +- src/app/api/chat/keys/route.ts | 161 +- src/app/api/chat/send/route.ts | 224 +- src/app/api/debug/check-chat-keys/route.ts | 50 + src/app/api/debug/check-keys/route.ts | 19 + src/app/api/debug/clear-chat-keys/route.ts | 18 + src/app/api/search/route.ts | 19 +- src/app/api/swarm/chat/inbox/route.ts | 11 +- src/app/chat/page.tsx | 410 +- src/db/schema.ts | 7 + src/lib/contexts/AuthContext.tsx | 21 +- src/lib/crypto/chat-storage.ts | 236 - src/lib/crypto/e2e-chat.ts | 104 - src/lib/crypto/e2ee.ts | 247 - src/lib/crypto/message-cache.ts | 0 src/lib/crypto/ratchet.ts | 390 -- src/lib/crypto/sodium-chat.ts | 228 + src/lib/crypto/user-signing.ts | 10 + src/lib/hooks/useChatEncryption.ts | 634 --- src/lib/hooks/useSodiumChat.ts | 204 + src/lib/hooks/useUserIdentity.ts | 52 +- 28 files changed, 10701 insertions(+), 2052 deletions(-) create mode 100644 drizzle/meta/0011_snapshot.json create mode 100644 drizzle/meta/0012_snapshot.json create mode 100644 scripts/clear-chat-keys.ts create mode 100644 src/app/api/debug/check-chat-keys/route.ts create mode 100644 src/app/api/debug/check-keys/route.ts create mode 100644 src/app/api/debug/clear-chat-keys/route.ts delete mode 100644 src/lib/crypto/chat-storage.ts delete mode 100644 src/lib/crypto/e2e-chat.ts delete mode 100644 src/lib/crypto/e2ee.ts create mode 100644 src/lib/crypto/message-cache.ts delete mode 100644 src/lib/crypto/ratchet.ts create mode 100644 src/lib/crypto/sodium-chat.ts delete mode 100644 src/lib/hooks/useChatEncryption.ts create mode 100644 src/lib/hooks/useSodiumChat.ts diff --git a/drizzle/meta/0011_snapshot.json b/drizzle/meta/0011_snapshot.json new file mode 100644 index 0000000..8b6da28 --- /dev/null +++ b/drizzle/meta/0011_snapshot.json @@ -0,0 +1,4799 @@ +{ + "id": "9c5ad998-ee99-4677-894a-21969787a20c", + "prevId": "d1ed0ac7-89ac-4e64-b931-e1b148e49f62", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.blocks": { + "name": "blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "blocked_user_id": { + "name": "blocked_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "blocks_user_idx": { + "name": "blocks_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "blocks_blocked_user_idx": { + "name": "blocks_blocked_user_idx", + "columns": [ + { + "expression": "blocked_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "blocks_user_id_users_id_fk": { + "name": "blocks_user_id_users_id_fk", + "tableFrom": "blocks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "blocks_blocked_user_id_users_id_fk": { + "name": "blocks_blocked_user_id_users_id_fk", + "tableFrom": "blocks", + "tableTo": "users", + "columnsFrom": [ + "blocked_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_activity_logs": { + "name": "bot_activity_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bot_id": { + "name": "bot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bot_activity_logs_bot_idx": { + "name": "bot_activity_logs_bot_idx", + "columns": [ + { + "expression": "bot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_activity_logs_action_idx": { + "name": "bot_activity_logs_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_activity_logs_created_idx": { + "name": "bot_activity_logs_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_activity_logs_bot_id_bots_id_fk": { + "name": "bot_activity_logs_bot_id_bots_id_fk", + "tableFrom": "bot_activity_logs", + "tableTo": "bots", + "columnsFrom": [ + "bot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_content_items": { + "name": "bot_content_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "source_id": { + "name": "source_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "fetched_at": { + "name": "fetched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_processed": { + "name": "is_processed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "interest_score": { + "name": "interest_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "interest_reason": { + "name": "interest_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "bot_content_items_source_idx": { + "name": "bot_content_items_source_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_content_items_processed_idx": { + "name": "bot_content_items_processed_idx", + "columns": [ + { + "expression": "is_processed", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_content_items_external_idx": { + "name": "bot_content_items_external_idx", + "columns": [ + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_content_items_source_id_bot_content_sources_id_fk": { + "name": "bot_content_items_source_id_bot_content_sources_id_fk", + "tableFrom": "bot_content_items", + "tableTo": "bot_content_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bot_content_items_post_id_posts_id_fk": { + "name": "bot_content_items_post_id_posts_id_fk", + "tableFrom": "bot_content_items", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_content_sources": { + "name": "bot_content_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bot_id": { + "name": "bot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subreddit": { + "name": "subreddit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_encrypted": { + "name": "api_key_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "keywords": { + "name": "keywords", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_fetch_at": { + "name": "last_fetch_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consecutive_errors": { + "name": "consecutive_errors", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bot_content_sources_bot_idx": { + "name": "bot_content_sources_bot_idx", + "columns": [ + { + "expression": "bot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_content_sources_type_idx": { + "name": "bot_content_sources_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_content_sources_bot_id_bots_id_fk": { + "name": "bot_content_sources_bot_id_bots_id_fk", + "tableFrom": "bot_content_sources", + "tableTo": "bots", + "columnsFrom": [ + "bot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_mentions": { + "name": "bot_mentions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bot_id": { + "name": "bot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_processed": { + "name": "is_processed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "response_post_id": { + "name": "response_post_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_remote": { + "name": "is_remote", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "remote_actor_url": { + "name": "remote_actor_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bot_mentions_bot_idx": { + "name": "bot_mentions_bot_idx", + "columns": [ + { + "expression": "bot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_mentions_processed_idx": { + "name": "bot_mentions_processed_idx", + "columns": [ + { + "expression": "is_processed", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_mentions_created_idx": { + "name": "bot_mentions_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_mentions_bot_id_bots_id_fk": { + "name": "bot_mentions_bot_id_bots_id_fk", + "tableFrom": "bot_mentions", + "tableTo": "bots", + "columnsFrom": [ + "bot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bot_mentions_post_id_posts_id_fk": { + "name": "bot_mentions_post_id_posts_id_fk", + "tableFrom": "bot_mentions", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bot_mentions_author_id_users_id_fk": { + "name": "bot_mentions_author_id_users_id_fk", + "tableFrom": "bot_mentions", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bot_mentions_response_post_id_posts_id_fk": { + "name": "bot_mentions_response_post_id_posts_id_fk", + "tableFrom": "bot_mentions", + "tableTo": "posts", + "columnsFrom": [ + "response_post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_rate_limits": { + "name": "bot_rate_limits", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bot_id": { + "name": "bot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "window_type": { + "name": "window_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "post_count": { + "name": "post_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reply_count": { + "name": "reply_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bot_rate_limits_bot_window_idx": { + "name": "bot_rate_limits_bot_window_idx", + "columns": [ + { + "expression": "bot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_rate_limits_bot_id_bots_id_fk": { + "name": "bot_rate_limits_bot_id_bots_id_fk", + "tableFrom": "bot_rate_limits", + "tableTo": "bots", + "columnsFrom": [ + "bot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bots": { + "name": "bots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "personality_config": { + "name": "personality_config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "llm_provider": { + "name": "llm_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "llm_model": { + "name": "llm_model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "llm_api_key_encrypted": { + "name": "llm_api_key_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_config": { + "name": "schedule_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autonomous_mode": { + "name": "autonomous_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_suspended": { + "name": "is_suspended", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "suspension_reason": { + "name": "suspension_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_post_at": { + "name": "last_post_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bots_user_id_idx": { + "name": "bots_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bots_owner_id_idx": { + "name": "bots_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bots_active_idx": { + "name": "bots_active_idx", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bots_user_id_users_id_fk": { + "name": "bots_user_id_users_id_fk", + "tableFrom": "bots", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bots_owner_id_users_id_fk": { + "name": "bots_owner_id_users_id_fk", + "tableFrom": "bots", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_conversations": { + "name": "chat_conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'direct'" + }, + "participant1_id": { + "name": "participant1_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant2_handle": { + "name": "participant2_handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_message_preview": { + "name": "last_message_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chat_conversations_participant1_idx": { + "name": "chat_conversations_participant1_idx", + "columns": [ + { + "expression": "participant1_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_conversations_last_message_idx": { + "name": "chat_conversations_last_message_idx", + "columns": [ + { + "expression": "last_message_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_conversations_unique": { + "name": "chat_conversations_unique", + "columns": [ + { + "expression": "participant1_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "participant2_handle", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_conversations_participant1_id_users_id_fk": { + "name": "chat_conversations_participant1_id_users_id_fk", + "tableFrom": "chat_conversations", + "tableTo": "users", + "columnsFrom": [ + "participant1_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_device_bundles": { + "name": "chat_device_bundles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registration_id": { + "name": "registration_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "identity_key": { + "name": "identity_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signed_pre_key": { + "name": "signed_pre_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kyber_pre_key": { + "name": "kyber_pre_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signature": { + "name": "signature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chat_bundles_user_idx": { + "name": "chat_bundles_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_bundles_did_idx": { + "name": "chat_bundles_did_idx", + "columns": [ + { + "expression": "did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_bundles_device_unique": { + "name": "chat_bundles_device_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_device_bundles_user_id_users_id_fk": { + "name": "chat_device_bundles_user_id_users_id_fk", + "tableFrom": "chat_device_bundles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_inbox": { + "name": "chat_inbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "recipient_did": { + "name": "recipient_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient_device_id": { + "name": "recipient_device_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sender_did": { + "name": "sender_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "envelope_json": { + "name": "envelope_json", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_read": { + "name": "is_read", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "chat_inbox_recipient_idx": { + "name": "chat_inbox_recipient_idx", + "columns": [ + { + "expression": "recipient_did", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recipient_device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_inbox_created_idx": { + "name": "chat_inbox_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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()" + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sender_handle": { + "name": "sender_handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sender_display_name": { + "name": "sender_display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sender_avatar_url": { + "name": "sender_avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sender_node_domain": { + "name": "sender_node_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_content": { + "name": "encrypted_content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sender_encrypted_content": { + "name": "sender_encrypted_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sender_chat_public_key": { + "name": "sender_chat_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "swarm_message_id": { + "name": "swarm_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "read_at": { + "name": "read_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chat_messages_conversation_idx": { + "name": "chat_messages_conversation_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_messages_created_idx": { + "name": "chat_messages_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_messages_swarm_id_idx": { + "name": "chat_messages_swarm_id_idx", + "columns": [ + { + "expression": "swarm_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_messages_conversation_id_chat_conversations_id_fk": { + "name": "chat_messages_conversation_id_chat_conversations_id_fk", + "tableFrom": "chat_messages", + "tableTo": "chat_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "chat_messages_swarm_message_id_unique": { + "name": "chat_messages_swarm_message_id_unique", + "nullsNotDistinct": false, + "columns": [ + "swarm_message_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_one_time_keys": { + "name": "chat_one_time_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "bundle_id": { + "name": "bundle_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chat_otk_bundle_idx": { + "name": "chat_otk_bundle_idx", + "columns": [ + { + "expression": "bundle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_otk_unique": { + "name": "chat_otk_unique", + "columns": [ + { + "expression": "bundle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_one_time_keys_user_id_users_id_fk": { + "name": "chat_one_time_keys_user_id_users_id_fk", + "tableFrom": "chat_one_time_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_one_time_keys_bundle_id_chat_device_bundles_id_fk": { + "name": "chat_one_time_keys_bundle_id_chat_device_bundles_id_fk", + "tableFrom": "chat_one_time_keys", + "tableTo": "chat_device_bundles", + "columnsFrom": [ + "bundle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_typing_indicators": { + "name": "chat_typing_indicators", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_handle": { + "name": "user_handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chat_typing_conversation_idx": { + "name": "chat_typing_conversation_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_typing_expires_idx": { + "name": "chat_typing_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_typing_unique": { + "name": "chat_typing_unique", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_handle", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_typing_indicators_conversation_id_chat_conversations_id_fk": { + "name": "chat_typing_indicators_conversation_id_chat_conversations_id_fk", + "tableFrom": "chat_typing_indicators", + "tableTo": "chat_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.follows": { + "name": "follows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "follower_id": { + "name": "follower_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "following_id": { + "name": "following_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ap_id": { + "name": "ap_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pending": { + "name": "pending", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "follows_follower_idx": { + "name": "follows_follower_idx", + "columns": [ + { + "expression": "follower_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "follows_following_idx": { + "name": "follows_following_idx", + "columns": [ + { + "expression": "following_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "follows_follower_id_users_id_fk": { + "name": "follows_follower_id_users_id_fk", + "tableFrom": "follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "follows_following_id_users_id_fk": { + "name": "follows_following_id_users_id_fk", + "tableFrom": "follows", + "tableTo": "users", + "columnsFrom": [ + "following_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "follows_ap_id_unique": { + "name": "follows_ap_id_unique", + "nullsNotDistinct": false, + "columns": [ + "ap_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.handle_registry": { + "name": "handle_registry", + "schema": "", + "columns": { + "handle": { + "name": "handle", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "node_domain": { + "name": "node_domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registered_at": { + "name": "registered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "handle_registry_updated_idx": { + "name": "handle_registry_updated_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.likes": { + "name": "likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ap_id": { + "name": "ap_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "likes_user_post_idx": { + "name": "likes_user_post_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "likes_user_id_users_id_fk": { + "name": "likes_user_id_users_id_fk", + "tableFrom": "likes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "likes_post_id_posts_id_fk": { + "name": "likes_post_id_posts_id_fk", + "tableFrom": "likes", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "likes_ap_id_unique": { + "name": "likes_ap_id_unique", + "nullsNotDistinct": false, + "columns": [ + "ap_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "alt_text": { + "name": "alt_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "media_user_idx": { + "name": "media_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_post_idx": { + "name": "media_post_idx", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "media_user_id_users_id_fk": { + "name": "media_user_id_users_id_fk", + "tableFrom": "media", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "media_post_id_posts_id_fk": { + "name": "media_post_id_posts_id_fk", + "tableFrom": "media", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.muted_nodes": { + "name": "muted_nodes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "node_domain": { + "name": "node_domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "muted_nodes_user_idx": { + "name": "muted_nodes_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "muted_nodes_domain_idx": { + "name": "muted_nodes_domain_idx", + "columns": [ + { + "expression": "node_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "muted_nodes_user_id_users_id_fk": { + "name": "muted_nodes_user_id_users_id_fk", + "tableFrom": "muted_nodes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mutes": { + "name": "mutes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "muted_user_id": { + "name": "muted_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mutes_user_idx": { + "name": "mutes_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mutes_muted_user_idx": { + "name": "mutes_muted_user_idx", + "columns": [ + { + "expression": "muted_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mutes_user_id_users_id_fk": { + "name": "mutes_user_id_users_id_fk", + "tableFrom": "mutes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mutes_muted_user_id_users_id_fk": { + "name": "mutes_muted_user_id_users_id_fk", + "tableFrom": "mutes", + "tableTo": "users", + "columnsFrom": [ + "muted_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.nodes": { + "name": "nodes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "long_description": { + "name": "long_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rules": { + "name": "rules", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banner_url": { + "name": "banner_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "favicon_url": { + "name": "favicon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#FFFFFF'" + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key_encrypted": { + "name": "private_key_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_nsfw": { + "name": "is_nsfw", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "turnstile_site_key": { + "name": "turnstile_site_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "turnstile_secret_key": { + "name": "turnstile_secret_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "nodes_domain_unique": { + "name": "nodes_domain_unique", + "nullsNotDistinct": false, + "columns": [ + "domain" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_handle": { + "name": "actor_handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_display_name": { + "name": "actor_display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_avatar_url": { + "name": "actor_avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_node_domain": { + "name": "actor_node_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "post_content": { + "name": "post_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "read_at": { + "name": "read_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notifications_user_idx": { + "name": "notifications_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_created_idx": { + "name": "notifications_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_user_id_users_id_fk": { + "name": "notifications_user_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_actor_id_users_id_fk": { + "name": "notifications_actor_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_post_id_posts_id_fk": { + "name": "notifications_post_id_posts_id_fk", + "tableFrom": "notifications", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.posts": { + "name": "posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "bot_id": { + "name": "bot_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reply_to_id": { + "name": "reply_to_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repost_of_id": { + "name": "repost_of_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "swarm_reply_to_id": { + "name": "swarm_reply_to_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "swarm_reply_to_content": { + "name": "swarm_reply_to_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "swarm_reply_to_author": { + "name": "swarm_reply_to_author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "likes_count": { + "name": "likes_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reposts_count": { + "name": "reposts_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "replies_count": { + "name": "replies_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_nsfw": { + "name": "is_nsfw", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_removed": { + "name": "is_removed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "removed_by": { + "name": "removed_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "removed_reason": { + "name": "removed_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ap_id": { + "name": "ap_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ap_url": { + "name": "ap_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_url": { + "name": "link_preview_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_title": { + "name": "link_preview_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_description": { + "name": "link_preview_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_image": { + "name": "link_preview_image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "posts_user_id_idx": { + "name": "posts_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_bot_id_idx": { + "name": "posts_bot_id_idx", + "columns": [ + { + "expression": "bot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_created_at_idx": { + "name": "posts_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_reply_to_idx": { + "name": "posts_reply_to_idx", + "columns": [ + { + "expression": "reply_to_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_removed_idx": { + "name": "posts_removed_idx", + "columns": [ + { + "expression": "is_removed", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_nsfw_idx": { + "name": "posts_nsfw_idx", + "columns": [ + { + "expression": "is_nsfw", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "posts_user_id_users_id_fk": { + "name": "posts_user_id_users_id_fk", + "tableFrom": "posts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "posts_bot_id_bots_id_fk": { + "name": "posts_bot_id_bots_id_fk", + "tableFrom": "posts", + "tableTo": "bots", + "columnsFrom": [ + "bot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "posts_removed_by_users_id_fk": { + "name": "posts_removed_by_users_id_fk", + "tableFrom": "posts", + "tableTo": "users", + "columnsFrom": [ + "removed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "posts_ap_id_unique": { + "name": "posts_ap_id_unique", + "nullsNotDistinct": false, + "columns": [ + "ap_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.remote_followers": { + "name": "remote_followers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_url": { + "name": "actor_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inbox_url": { + "name": "inbox_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shared_inbox_url": { + "name": "shared_inbox_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "activity_id": { + "name": "activity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "remote_followers_user_idx": { + "name": "remote_followers_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_followers_actor_idx": { + "name": "remote_followers_actor_idx", + "columns": [ + { + "expression": "actor_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_followers_user_actor_unique": { + "name": "remote_followers_user_actor_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "remote_followers_user_id_users_id_fk": { + "name": "remote_followers_user_id_users_id_fk", + "tableFrom": "remote_followers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.remote_follows": { + "name": "remote_follows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "follower_id": { + "name": "follower_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_actor_url": { + "name": "target_actor_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inbox_url": { + "name": "inbox_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "activity_id": { + "name": "activity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "remote_follows_follower_idx": { + "name": "remote_follows_follower_idx", + "columns": [ + { + "expression": "follower_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_follows_target_idx": { + "name": "remote_follows_target_idx", + "columns": [ + { + "expression": "target_handle", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "remote_follows_follower_id_users_id_fk": { + "name": "remote_follows_follower_id_users_id_fk", + "tableFrom": "remote_follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.remote_identity_cache": { + "name": "remote_identity_cache", + "schema": "", + "columns": { + "did": { + "name": "did", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fetched_at": { + "name": "fetched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.remote_likes": { + "name": "remote_likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_handle": { + "name": "actor_handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_node_domain": { + "name": "actor_node_domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "remote_likes_post_idx": { + "name": "remote_likes_post_idx", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_likes_actor_idx": { + "name": "remote_likes_actor_idx", + "columns": [ + { + "expression": "actor_handle", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_node_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_likes_unique": { + "name": "remote_likes_unique", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_handle", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_node_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "remote_likes_post_id_posts_id_fk": { + "name": "remote_likes_post_id_posts_id_fk", + "tableFrom": "remote_likes", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.remote_posts": { + "name": "remote_posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "ap_id": { + "name": "ap_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_handle": { + "name": "author_handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_actor_url": { + "name": "author_actor_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_display_name": { + "name": "author_display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_avatar_url": { + "name": "author_avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "link_preview_url": { + "name": "link_preview_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_title": { + "name": "link_preview_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_description": { + "name": "link_preview_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_image": { + "name": "link_preview_image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "media_json": { + "name": "media_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fetched_at": { + "name": "fetched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "remote_posts_author_idx": { + "name": "remote_posts_author_idx", + "columns": [ + { + "expression": "author_handle", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_posts_published_idx": { + "name": "remote_posts_published_idx", + "columns": [ + { + "expression": "published_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_posts_ap_id_idx": { + "name": "remote_posts_ap_id_idx", + "columns": [ + { + "expression": "ap_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "remote_posts_ap_id_unique": { + "name": "remote_posts_ap_id_unique", + "nullsNotDistinct": false, + "columns": [ + "ap_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.remote_reposts": { + "name": "remote_reposts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_handle": { + "name": "actor_handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_node_domain": { + "name": "actor_node_domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "remote_reposts_post_idx": { + "name": "remote_reposts_post_idx", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_reposts_actor_idx": { + "name": "remote_reposts_actor_idx", + "columns": [ + { + "expression": "actor_handle", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_node_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_reposts_unique": { + "name": "remote_reposts_unique", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_handle", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_node_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "remote_reposts_post_id_posts_id_fk": { + "name": "remote_reposts_post_id_posts_id_fk", + "tableFrom": "remote_reposts", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reports": { + "name": "reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "reporter_id": { + "name": "reporter_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolution_note": { + "name": "resolution_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reports_status_idx": { + "name": "reports_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reports_target_idx": { + "name": "reports_target_idx", + "columns": [ + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reports_reporter_idx": { + "name": "reports_reporter_idx", + "columns": [ + { + "expression": "reporter_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reports_reporter_id_users_id_fk": { + "name": "reports_reporter_id_users_id_fk", + "tableFrom": "reports", + "tableTo": "users", + "columnsFrom": [ + "reporter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "reports_resolved_by_users_id_fk": { + "name": "reports_resolved_by_users_id_fk", + "tableFrom": "reports", + "tableTo": "users", + "columnsFrom": [ + "resolved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sessions_token_idx": { + "name": "sessions_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + { + "expression": "user_id", + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.signed_action_dedupe": { + "name": "signed_action_dedupe", + "schema": "", + "columns": { + "action_id": { + "name": "action_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "signed_action_dedupe_created_idx": { + "name": "signed_action_dedupe_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.swarm_nodes": { + "name": "swarm_nodes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_count": { + "name": "user_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "post_count": { + "name": "post_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_nsfw": { + "name": "is_nsfw", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "discovered_via": { + "name": "discovered_via", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trust_score": { + "name": "trust_score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "swarm_nodes_domain_idx": { + "name": "swarm_nodes_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "swarm_nodes_active_idx": { + "name": "swarm_nodes_active_idx", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "swarm_nodes_last_seen_idx": { + "name": "swarm_nodes_last_seen_idx", + "columns": [ + { + "expression": "last_seen_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "swarm_nodes_trust_idx": { + "name": "swarm_nodes_trust_idx", + "columns": [ + { + "expression": "trust_score", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "swarm_nodes_nsfw_idx": { + "name": "swarm_nodes_nsfw_idx", + "columns": [ + { + "expression": "is_nsfw", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "swarm_nodes_domain_unique": { + "name": "swarm_nodes_domain_unique", + "nullsNotDistinct": false, + "columns": [ + "domain" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.swarm_seeds": { + "name": "swarm_seeds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_contact_at": { + "name": "last_contact_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "swarm_seeds_enabled_idx": { + "name": "swarm_seeds_enabled_idx", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "swarm_seeds_priority_idx": { + "name": "swarm_seeds_priority_idx", + "columns": [ + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "swarm_seeds_domain_unique": { + "name": "swarm_seeds_domain_unique", + "nullsNotDistinct": false, + "columns": [ + "domain" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.swarm_sync_log": { + "name": "swarm_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "remote_domain": { + "name": "remote_domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nodes_received": { + "name": "nodes_received", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "nodes_sent": { + "name": "nodes_sent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "handles_received": { + "name": "handles_received", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "handles_sent": { + "name": "handles_sent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "swarm_sync_log_remote_idx": { + "name": "swarm_sync_log_remote_idx", + "columns": [ + { + "expression": "remote_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "swarm_sync_log_created_idx": { + "name": "swarm_sync_log_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "header_url": { + "name": "header_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key_encrypted": { + "name": "private_key_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_public_key": { + "name": "chat_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "chat_private_key_encrypted": { + "name": "chat_private_key_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "node_id": { + "name": "node_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_bot": { + "name": "is_bot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bot_owner_id": { + "name": "bot_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_nsfw": { + "name": "is_nsfw", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "nsfw_enabled": { + "name": "nsfw_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "age_verified_at": { + "name": "age_verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_suspended": { + "name": "is_suspended", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "suspension_reason": { + "name": "suspension_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_silenced": { + "name": "is_silenced", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "silence_reason": { + "name": "silence_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "silenced_at": { + "name": "silenced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "moved_to": { + "name": "moved_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "moved_from": { + "name": "moved_from", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "migrated_at": { + "name": "migrated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "followers_count": { + "name": "followers_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "following_count": { + "name": "following_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "posts_count": { + "name": "posts_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_handle_idx": { + "name": "users_handle_idx", + "columns": [ + { + "expression": "handle", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_did_idx": { + "name": "users_did_idx", + "columns": [ + { + "expression": "did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_suspended_idx": { + "name": "users_suspended_idx", + "columns": [ + { + "expression": "is_suspended", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_silenced_idx": { + "name": "users_silenced_idx", + "columns": [ + { + "expression": "is_silenced", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_is_bot_idx": { + "name": "users_is_bot_idx", + "columns": [ + { + "expression": "is_bot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_bot_owner_idx": { + "name": "users_bot_owner_idx", + "columns": [ + { + "expression": "bot_owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_nsfw_idx": { + "name": "users_nsfw_idx", + "columns": [ + { + "expression": "is_nsfw", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_node_id_nodes_id_fk": { + "name": "users_node_id_nodes_id_fk", + "tableFrom": "users", + "tableTo": "nodes", + "columnsFrom": [ + "node_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_bot_owner_id_users_id_fk": { + "name": "users_bot_owner_id_users_id_fk", + "tableFrom": "users", + "tableTo": "users", + "columnsFrom": [ + "bot_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_did_unique": { + "name": "users_did_unique", + "nullsNotDistinct": false, + "columns": [ + "did" + ] + }, + "users_handle_unique": { + "name": "users_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0012_snapshot.json b/drizzle/meta/0012_snapshot.json new file mode 100644 index 0000000..85d6a46 --- /dev/null +++ b/drizzle/meta/0012_snapshot.json @@ -0,0 +1,4805 @@ +{ + "id": "02a4734f-64b6-4c6e-bc9b-fced92eb2843", + "prevId": "9c5ad998-ee99-4677-894a-21969787a20c", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.blocks": { + "name": "blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "blocked_user_id": { + "name": "blocked_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "blocks_user_idx": { + "name": "blocks_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "blocks_blocked_user_idx": { + "name": "blocks_blocked_user_idx", + "columns": [ + { + "expression": "blocked_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "blocks_user_id_users_id_fk": { + "name": "blocks_user_id_users_id_fk", + "tableFrom": "blocks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "blocks_blocked_user_id_users_id_fk": { + "name": "blocks_blocked_user_id_users_id_fk", + "tableFrom": "blocks", + "tableTo": "users", + "columnsFrom": [ + "blocked_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_activity_logs": { + "name": "bot_activity_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bot_id": { + "name": "bot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bot_activity_logs_bot_idx": { + "name": "bot_activity_logs_bot_idx", + "columns": [ + { + "expression": "bot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_activity_logs_action_idx": { + "name": "bot_activity_logs_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_activity_logs_created_idx": { + "name": "bot_activity_logs_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_activity_logs_bot_id_bots_id_fk": { + "name": "bot_activity_logs_bot_id_bots_id_fk", + "tableFrom": "bot_activity_logs", + "tableTo": "bots", + "columnsFrom": [ + "bot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_content_items": { + "name": "bot_content_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "source_id": { + "name": "source_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "fetched_at": { + "name": "fetched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_processed": { + "name": "is_processed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "interest_score": { + "name": "interest_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "interest_reason": { + "name": "interest_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "bot_content_items_source_idx": { + "name": "bot_content_items_source_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_content_items_processed_idx": { + "name": "bot_content_items_processed_idx", + "columns": [ + { + "expression": "is_processed", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_content_items_external_idx": { + "name": "bot_content_items_external_idx", + "columns": [ + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_content_items_source_id_bot_content_sources_id_fk": { + "name": "bot_content_items_source_id_bot_content_sources_id_fk", + "tableFrom": "bot_content_items", + "tableTo": "bot_content_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bot_content_items_post_id_posts_id_fk": { + "name": "bot_content_items_post_id_posts_id_fk", + "tableFrom": "bot_content_items", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_content_sources": { + "name": "bot_content_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bot_id": { + "name": "bot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subreddit": { + "name": "subreddit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_encrypted": { + "name": "api_key_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "keywords": { + "name": "keywords", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_fetch_at": { + "name": "last_fetch_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consecutive_errors": { + "name": "consecutive_errors", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bot_content_sources_bot_idx": { + "name": "bot_content_sources_bot_idx", + "columns": [ + { + "expression": "bot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_content_sources_type_idx": { + "name": "bot_content_sources_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_content_sources_bot_id_bots_id_fk": { + "name": "bot_content_sources_bot_id_bots_id_fk", + "tableFrom": "bot_content_sources", + "tableTo": "bots", + "columnsFrom": [ + "bot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_mentions": { + "name": "bot_mentions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bot_id": { + "name": "bot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_processed": { + "name": "is_processed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "response_post_id": { + "name": "response_post_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_remote": { + "name": "is_remote", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "remote_actor_url": { + "name": "remote_actor_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bot_mentions_bot_idx": { + "name": "bot_mentions_bot_idx", + "columns": [ + { + "expression": "bot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_mentions_processed_idx": { + "name": "bot_mentions_processed_idx", + "columns": [ + { + "expression": "is_processed", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bot_mentions_created_idx": { + "name": "bot_mentions_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_mentions_bot_id_bots_id_fk": { + "name": "bot_mentions_bot_id_bots_id_fk", + "tableFrom": "bot_mentions", + "tableTo": "bots", + "columnsFrom": [ + "bot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bot_mentions_post_id_posts_id_fk": { + "name": "bot_mentions_post_id_posts_id_fk", + "tableFrom": "bot_mentions", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bot_mentions_author_id_users_id_fk": { + "name": "bot_mentions_author_id_users_id_fk", + "tableFrom": "bot_mentions", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bot_mentions_response_post_id_posts_id_fk": { + "name": "bot_mentions_response_post_id_posts_id_fk", + "tableFrom": "bot_mentions", + "tableTo": "posts", + "columnsFrom": [ + "response_post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_rate_limits": { + "name": "bot_rate_limits", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bot_id": { + "name": "bot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "window_type": { + "name": "window_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "post_count": { + "name": "post_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reply_count": { + "name": "reply_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bot_rate_limits_bot_window_idx": { + "name": "bot_rate_limits_bot_window_idx", + "columns": [ + { + "expression": "bot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_rate_limits_bot_id_bots_id_fk": { + "name": "bot_rate_limits_bot_id_bots_id_fk", + "tableFrom": "bot_rate_limits", + "tableTo": "bots", + "columnsFrom": [ + "bot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bots": { + "name": "bots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "personality_config": { + "name": "personality_config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "llm_provider": { + "name": "llm_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "llm_model": { + "name": "llm_model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "llm_api_key_encrypted": { + "name": "llm_api_key_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_config": { + "name": "schedule_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autonomous_mode": { + "name": "autonomous_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_suspended": { + "name": "is_suspended", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "suspension_reason": { + "name": "suspension_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_post_at": { + "name": "last_post_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bots_user_id_idx": { + "name": "bots_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bots_owner_id_idx": { + "name": "bots_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bots_active_idx": { + "name": "bots_active_idx", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bots_user_id_users_id_fk": { + "name": "bots_user_id_users_id_fk", + "tableFrom": "bots", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bots_owner_id_users_id_fk": { + "name": "bots_owner_id_users_id_fk", + "tableFrom": "bots", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_conversations": { + "name": "chat_conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'direct'" + }, + "participant1_id": { + "name": "participant1_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant2_handle": { + "name": "participant2_handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_message_preview": { + "name": "last_message_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chat_conversations_participant1_idx": { + "name": "chat_conversations_participant1_idx", + "columns": [ + { + "expression": "participant1_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_conversations_last_message_idx": { + "name": "chat_conversations_last_message_idx", + "columns": [ + { + "expression": "last_message_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_conversations_unique": { + "name": "chat_conversations_unique", + "columns": [ + { + "expression": "participant1_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "participant2_handle", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_conversations_participant1_id_users_id_fk": { + "name": "chat_conversations_participant1_id_users_id_fk", + "tableFrom": "chat_conversations", + "tableTo": "users", + "columnsFrom": [ + "participant1_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_device_bundles": { + "name": "chat_device_bundles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registration_id": { + "name": "registration_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "identity_key": { + "name": "identity_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signed_pre_key": { + "name": "signed_pre_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kyber_pre_key": { + "name": "kyber_pre_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signature": { + "name": "signature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chat_bundles_user_idx": { + "name": "chat_bundles_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_bundles_did_idx": { + "name": "chat_bundles_did_idx", + "columns": [ + { + "expression": "did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_bundles_device_unique": { + "name": "chat_bundles_device_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_device_bundles_user_id_users_id_fk": { + "name": "chat_device_bundles_user_id_users_id_fk", + "tableFrom": "chat_device_bundles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_inbox": { + "name": "chat_inbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "recipient_did": { + "name": "recipient_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient_device_id": { + "name": "recipient_device_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sender_did": { + "name": "sender_did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "envelope_json": { + "name": "envelope_json", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_read": { + "name": "is_read", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "chat_inbox_recipient_idx": { + "name": "chat_inbox_recipient_idx", + "columns": [ + { + "expression": "recipient_did", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recipient_device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_inbox_created_idx": { + "name": "chat_inbox_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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()" + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sender_handle": { + "name": "sender_handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sender_display_name": { + "name": "sender_display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sender_avatar_url": { + "name": "sender_avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sender_node_domain": { + "name": "sender_node_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sender_did": { + "name": "sender_did", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_content": { + "name": "encrypted_content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sender_encrypted_content": { + "name": "sender_encrypted_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sender_chat_public_key": { + "name": "sender_chat_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "swarm_message_id": { + "name": "swarm_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "read_at": { + "name": "read_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chat_messages_conversation_idx": { + "name": "chat_messages_conversation_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_messages_created_idx": { + "name": "chat_messages_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_messages_swarm_id_idx": { + "name": "chat_messages_swarm_id_idx", + "columns": [ + { + "expression": "swarm_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_messages_conversation_id_chat_conversations_id_fk": { + "name": "chat_messages_conversation_id_chat_conversations_id_fk", + "tableFrom": "chat_messages", + "tableTo": "chat_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "chat_messages_swarm_message_id_unique": { + "name": "chat_messages_swarm_message_id_unique", + "nullsNotDistinct": false, + "columns": [ + "swarm_message_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_one_time_keys": { + "name": "chat_one_time_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "bundle_id": { + "name": "bundle_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chat_otk_bundle_idx": { + "name": "chat_otk_bundle_idx", + "columns": [ + { + "expression": "bundle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_otk_unique": { + "name": "chat_otk_unique", + "columns": [ + { + "expression": "bundle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_one_time_keys_user_id_users_id_fk": { + "name": "chat_one_time_keys_user_id_users_id_fk", + "tableFrom": "chat_one_time_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_one_time_keys_bundle_id_chat_device_bundles_id_fk": { + "name": "chat_one_time_keys_bundle_id_chat_device_bundles_id_fk", + "tableFrom": "chat_one_time_keys", + "tableTo": "chat_device_bundles", + "columnsFrom": [ + "bundle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_typing_indicators": { + "name": "chat_typing_indicators", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_handle": { + "name": "user_handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chat_typing_conversation_idx": { + "name": "chat_typing_conversation_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_typing_expires_idx": { + "name": "chat_typing_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_typing_unique": { + "name": "chat_typing_unique", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_handle", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_typing_indicators_conversation_id_chat_conversations_id_fk": { + "name": "chat_typing_indicators_conversation_id_chat_conversations_id_fk", + "tableFrom": "chat_typing_indicators", + "tableTo": "chat_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.follows": { + "name": "follows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "follower_id": { + "name": "follower_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "following_id": { + "name": "following_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ap_id": { + "name": "ap_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pending": { + "name": "pending", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "follows_follower_idx": { + "name": "follows_follower_idx", + "columns": [ + { + "expression": "follower_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "follows_following_idx": { + "name": "follows_following_idx", + "columns": [ + { + "expression": "following_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "follows_follower_id_users_id_fk": { + "name": "follows_follower_id_users_id_fk", + "tableFrom": "follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "follows_following_id_users_id_fk": { + "name": "follows_following_id_users_id_fk", + "tableFrom": "follows", + "tableTo": "users", + "columnsFrom": [ + "following_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "follows_ap_id_unique": { + "name": "follows_ap_id_unique", + "nullsNotDistinct": false, + "columns": [ + "ap_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.handle_registry": { + "name": "handle_registry", + "schema": "", + "columns": { + "handle": { + "name": "handle", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "node_domain": { + "name": "node_domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registered_at": { + "name": "registered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "handle_registry_updated_idx": { + "name": "handle_registry_updated_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.likes": { + "name": "likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ap_id": { + "name": "ap_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "likes_user_post_idx": { + "name": "likes_user_post_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "likes_user_id_users_id_fk": { + "name": "likes_user_id_users_id_fk", + "tableFrom": "likes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "likes_post_id_posts_id_fk": { + "name": "likes_post_id_posts_id_fk", + "tableFrom": "likes", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "likes_ap_id_unique": { + "name": "likes_ap_id_unique", + "nullsNotDistinct": false, + "columns": [ + "ap_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "alt_text": { + "name": "alt_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "media_user_idx": { + "name": "media_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_post_idx": { + "name": "media_post_idx", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "media_user_id_users_id_fk": { + "name": "media_user_id_users_id_fk", + "tableFrom": "media", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "media_post_id_posts_id_fk": { + "name": "media_post_id_posts_id_fk", + "tableFrom": "media", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.muted_nodes": { + "name": "muted_nodes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "node_domain": { + "name": "node_domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "muted_nodes_user_idx": { + "name": "muted_nodes_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "muted_nodes_domain_idx": { + "name": "muted_nodes_domain_idx", + "columns": [ + { + "expression": "node_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "muted_nodes_user_id_users_id_fk": { + "name": "muted_nodes_user_id_users_id_fk", + "tableFrom": "muted_nodes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mutes": { + "name": "mutes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "muted_user_id": { + "name": "muted_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mutes_user_idx": { + "name": "mutes_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mutes_muted_user_idx": { + "name": "mutes_muted_user_idx", + "columns": [ + { + "expression": "muted_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mutes_user_id_users_id_fk": { + "name": "mutes_user_id_users_id_fk", + "tableFrom": "mutes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mutes_muted_user_id_users_id_fk": { + "name": "mutes_muted_user_id_users_id_fk", + "tableFrom": "mutes", + "tableTo": "users", + "columnsFrom": [ + "muted_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.nodes": { + "name": "nodes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "long_description": { + "name": "long_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rules": { + "name": "rules", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banner_url": { + "name": "banner_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "favicon_url": { + "name": "favicon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#FFFFFF'" + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key_encrypted": { + "name": "private_key_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_nsfw": { + "name": "is_nsfw", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "turnstile_site_key": { + "name": "turnstile_site_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "turnstile_secret_key": { + "name": "turnstile_secret_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "nodes_domain_unique": { + "name": "nodes_domain_unique", + "nullsNotDistinct": false, + "columns": [ + "domain" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_handle": { + "name": "actor_handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_display_name": { + "name": "actor_display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_avatar_url": { + "name": "actor_avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_node_domain": { + "name": "actor_node_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "post_content": { + "name": "post_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "read_at": { + "name": "read_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notifications_user_idx": { + "name": "notifications_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_created_idx": { + "name": "notifications_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_user_id_users_id_fk": { + "name": "notifications_user_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_actor_id_users_id_fk": { + "name": "notifications_actor_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_post_id_posts_id_fk": { + "name": "notifications_post_id_posts_id_fk", + "tableFrom": "notifications", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.posts": { + "name": "posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "bot_id": { + "name": "bot_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reply_to_id": { + "name": "reply_to_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repost_of_id": { + "name": "repost_of_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "swarm_reply_to_id": { + "name": "swarm_reply_to_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "swarm_reply_to_content": { + "name": "swarm_reply_to_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "swarm_reply_to_author": { + "name": "swarm_reply_to_author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "likes_count": { + "name": "likes_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reposts_count": { + "name": "reposts_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "replies_count": { + "name": "replies_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_nsfw": { + "name": "is_nsfw", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_removed": { + "name": "is_removed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "removed_by": { + "name": "removed_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "removed_reason": { + "name": "removed_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ap_id": { + "name": "ap_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ap_url": { + "name": "ap_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_url": { + "name": "link_preview_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_title": { + "name": "link_preview_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_description": { + "name": "link_preview_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_image": { + "name": "link_preview_image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "posts_user_id_idx": { + "name": "posts_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_bot_id_idx": { + "name": "posts_bot_id_idx", + "columns": [ + { + "expression": "bot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_created_at_idx": { + "name": "posts_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_reply_to_idx": { + "name": "posts_reply_to_idx", + "columns": [ + { + "expression": "reply_to_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_removed_idx": { + "name": "posts_removed_idx", + "columns": [ + { + "expression": "is_removed", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "posts_nsfw_idx": { + "name": "posts_nsfw_idx", + "columns": [ + { + "expression": "is_nsfw", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "posts_user_id_users_id_fk": { + "name": "posts_user_id_users_id_fk", + "tableFrom": "posts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "posts_bot_id_bots_id_fk": { + "name": "posts_bot_id_bots_id_fk", + "tableFrom": "posts", + "tableTo": "bots", + "columnsFrom": [ + "bot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "posts_removed_by_users_id_fk": { + "name": "posts_removed_by_users_id_fk", + "tableFrom": "posts", + "tableTo": "users", + "columnsFrom": [ + "removed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "posts_ap_id_unique": { + "name": "posts_ap_id_unique", + "nullsNotDistinct": false, + "columns": [ + "ap_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.remote_followers": { + "name": "remote_followers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_url": { + "name": "actor_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inbox_url": { + "name": "inbox_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shared_inbox_url": { + "name": "shared_inbox_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "activity_id": { + "name": "activity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "remote_followers_user_idx": { + "name": "remote_followers_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_followers_actor_idx": { + "name": "remote_followers_actor_idx", + "columns": [ + { + "expression": "actor_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_followers_user_actor_unique": { + "name": "remote_followers_user_actor_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "remote_followers_user_id_users_id_fk": { + "name": "remote_followers_user_id_users_id_fk", + "tableFrom": "remote_followers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.remote_follows": { + "name": "remote_follows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "follower_id": { + "name": "follower_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_actor_url": { + "name": "target_actor_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inbox_url": { + "name": "inbox_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "activity_id": { + "name": "activity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "remote_follows_follower_idx": { + "name": "remote_follows_follower_idx", + "columns": [ + { + "expression": "follower_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_follows_target_idx": { + "name": "remote_follows_target_idx", + "columns": [ + { + "expression": "target_handle", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "remote_follows_follower_id_users_id_fk": { + "name": "remote_follows_follower_id_users_id_fk", + "tableFrom": "remote_follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.remote_identity_cache": { + "name": "remote_identity_cache", + "schema": "", + "columns": { + "did": { + "name": "did", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fetched_at": { + "name": "fetched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.remote_likes": { + "name": "remote_likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_handle": { + "name": "actor_handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_node_domain": { + "name": "actor_node_domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "remote_likes_post_idx": { + "name": "remote_likes_post_idx", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_likes_actor_idx": { + "name": "remote_likes_actor_idx", + "columns": [ + { + "expression": "actor_handle", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_node_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_likes_unique": { + "name": "remote_likes_unique", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_handle", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_node_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "remote_likes_post_id_posts_id_fk": { + "name": "remote_likes_post_id_posts_id_fk", + "tableFrom": "remote_likes", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.remote_posts": { + "name": "remote_posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "ap_id": { + "name": "ap_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_handle": { + "name": "author_handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_actor_url": { + "name": "author_actor_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_display_name": { + "name": "author_display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_avatar_url": { + "name": "author_avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "link_preview_url": { + "name": "link_preview_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_title": { + "name": "link_preview_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_description": { + "name": "link_preview_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_preview_image": { + "name": "link_preview_image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "media_json": { + "name": "media_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fetched_at": { + "name": "fetched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "remote_posts_author_idx": { + "name": "remote_posts_author_idx", + "columns": [ + { + "expression": "author_handle", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_posts_published_idx": { + "name": "remote_posts_published_idx", + "columns": [ + { + "expression": "published_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_posts_ap_id_idx": { + "name": "remote_posts_ap_id_idx", + "columns": [ + { + "expression": "ap_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "remote_posts_ap_id_unique": { + "name": "remote_posts_ap_id_unique", + "nullsNotDistinct": false, + "columns": [ + "ap_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.remote_reposts": { + "name": "remote_reposts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_handle": { + "name": "actor_handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_node_domain": { + "name": "actor_node_domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "remote_reposts_post_idx": { + "name": "remote_reposts_post_idx", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_reposts_actor_idx": { + "name": "remote_reposts_actor_idx", + "columns": [ + { + "expression": "actor_handle", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_node_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "remote_reposts_unique": { + "name": "remote_reposts_unique", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_handle", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_node_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "remote_reposts_post_id_posts_id_fk": { + "name": "remote_reposts_post_id_posts_id_fk", + "tableFrom": "remote_reposts", + "tableTo": "posts", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reports": { + "name": "reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "reporter_id": { + "name": "reporter_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolution_note": { + "name": "resolution_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reports_status_idx": { + "name": "reports_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reports_target_idx": { + "name": "reports_target_idx", + "columns": [ + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reports_reporter_idx": { + "name": "reports_reporter_idx", + "columns": [ + { + "expression": "reporter_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reports_reporter_id_users_id_fk": { + "name": "reports_reporter_id_users_id_fk", + "tableFrom": "reports", + "tableTo": "users", + "columnsFrom": [ + "reporter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "reports_resolved_by_users_id_fk": { + "name": "reports_resolved_by_users_id_fk", + "tableFrom": "reports", + "tableTo": "users", + "columnsFrom": [ + "resolved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sessions_token_idx": { + "name": "sessions_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + { + "expression": "user_id", + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.signed_action_dedupe": { + "name": "signed_action_dedupe", + "schema": "", + "columns": { + "action_id": { + "name": "action_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "signed_action_dedupe_created_idx": { + "name": "signed_action_dedupe_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.swarm_nodes": { + "name": "swarm_nodes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_count": { + "name": "user_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "post_count": { + "name": "post_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_nsfw": { + "name": "is_nsfw", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "discovered_via": { + "name": "discovered_via", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trust_score": { + "name": "trust_score", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "swarm_nodes_domain_idx": { + "name": "swarm_nodes_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "swarm_nodes_active_idx": { + "name": "swarm_nodes_active_idx", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "swarm_nodes_last_seen_idx": { + "name": "swarm_nodes_last_seen_idx", + "columns": [ + { + "expression": "last_seen_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "swarm_nodes_trust_idx": { + "name": "swarm_nodes_trust_idx", + "columns": [ + { + "expression": "trust_score", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "swarm_nodes_nsfw_idx": { + "name": "swarm_nodes_nsfw_idx", + "columns": [ + { + "expression": "is_nsfw", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "swarm_nodes_domain_unique": { + "name": "swarm_nodes_domain_unique", + "nullsNotDistinct": false, + "columns": [ + "domain" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.swarm_seeds": { + "name": "swarm_seeds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_contact_at": { + "name": "last_contact_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "swarm_seeds_enabled_idx": { + "name": "swarm_seeds_enabled_idx", + "columns": [ + { + "expression": "is_enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "swarm_seeds_priority_idx": { + "name": "swarm_seeds_priority_idx", + "columns": [ + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "swarm_seeds_domain_unique": { + "name": "swarm_seeds_domain_unique", + "nullsNotDistinct": false, + "columns": [ + "domain" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.swarm_sync_log": { + "name": "swarm_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "remote_domain": { + "name": "remote_domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nodes_received": { + "name": "nodes_received", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "nodes_sent": { + "name": "nodes_sent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "handles_received": { + "name": "handles_received", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "handles_sent": { + "name": "handles_sent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "swarm_sync_log_remote_idx": { + "name": "swarm_sync_log_remote_idx", + "columns": [ + { + "expression": "remote_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "swarm_sync_log_created_idx": { + "name": "swarm_sync_log_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "header_url": { + "name": "header_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key_encrypted": { + "name": "private_key_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_public_key": { + "name": "chat_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "chat_private_key_encrypted": { + "name": "chat_private_key_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "node_id": { + "name": "node_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_bot": { + "name": "is_bot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bot_owner_id": { + "name": "bot_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_nsfw": { + "name": "is_nsfw", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "nsfw_enabled": { + "name": "nsfw_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "age_verified_at": { + "name": "age_verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_suspended": { + "name": "is_suspended", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "suspension_reason": { + "name": "suspension_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_silenced": { + "name": "is_silenced", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "silence_reason": { + "name": "silence_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "silenced_at": { + "name": "silenced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "moved_to": { + "name": "moved_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "moved_from": { + "name": "moved_from", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "migrated_at": { + "name": "migrated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "followers_count": { + "name": "followers_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "following_count": { + "name": "following_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "posts_count": { + "name": "posts_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_handle_idx": { + "name": "users_handle_idx", + "columns": [ + { + "expression": "handle", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_did_idx": { + "name": "users_did_idx", + "columns": [ + { + "expression": "did", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_suspended_idx": { + "name": "users_suspended_idx", + "columns": [ + { + "expression": "is_suspended", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_silenced_idx": { + "name": "users_silenced_idx", + "columns": [ + { + "expression": "is_silenced", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_is_bot_idx": { + "name": "users_is_bot_idx", + "columns": [ + { + "expression": "is_bot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_bot_owner_idx": { + "name": "users_bot_owner_idx", + "columns": [ + { + "expression": "bot_owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_nsfw_idx": { + "name": "users_nsfw_idx", + "columns": [ + { + "expression": "is_nsfw", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_node_id_nodes_id_fk": { + "name": "users_node_id_nodes_id_fk", + "tableFrom": "users", + "tableTo": "nodes", + "columnsFrom": [ + "node_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_bot_owner_id_users_id_fk": { + "name": "users_bot_owner_id_users_id_fk", + "tableFrom": "users", + "tableTo": "users", + "columnsFrom": [ + "bot_owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_did_unique": { + "name": "users_did_unique", + "nullsNotDistinct": false, + "columns": [ + "did" + ] + }, + "users_handle_unique": { + "name": "users_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 9a3f03d..aa75922 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -78,6 +78,20 @@ "when": 1769579537338, "tag": "0010_add_sender_did_to_chat_messages", "breakpoints": true + }, + { + "idx": 11, + "version": "7", + "when": 1769586658154, + "tag": "0011_elite_kat_farrell", + "breakpoints": true + }, + { + "idx": 12, + "version": "7", + "when": 1769586699065, + "tag": "0012_quick_mockingbird", + "breakpoints": true } ] } \ No newline at end of file diff --git a/next.config.ts b/next.config.ts index e9ffa30..c64487c 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,8 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + // Empty turbopack config to silence the warning + turbopack: {}, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 00bec25..9972d30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "crypto-js": "^4.2.0", "drizzle-orm": "^0.44.1", "jose": "^6.0.11", + "libsodium-wrappers-sumo": "^0.8.2", "lucide-react": "^0.562.0", "next": "16.1.4", "next-auth": "^5.0.0-beta.25", @@ -1014,6 +1015,7 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -4516,6 +4518,7 @@ "integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*", "pg-protocol": "*", @@ -4588,6 +4591,7 @@ "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4654,6 +4658,7 @@ "integrity": "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", @@ -5152,6 +5157,7 @@ "resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.36.1.tgz", "integrity": "sha512-N6SjDcgXdOcTAF+7uNoY69o7hCspe9BcA7YjQdxVu5d25avljTwyLaHBW3krWjrP0FfocgMk94qyVtQbeDp39A==", "license": "MIT", + "peer": true, "dependencies": { "uncrypto": "^0.1.3" } @@ -5273,6 +5279,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5635,6 +5642,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6364,6 +6372,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -6441,6 +6450,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6626,6 +6636,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -8026,6 +8037,21 @@ "node": ">= 0.8.0" } }, + "node_modules/libsodium-sumo": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.8.2.tgz", + "integrity": "sha512-uMgnjphJ717jLN+jFG1HUgNrK/gOVVfaO1DGZ1Ig/fKLKLVhvaH/sM1I1v784JFvmkJDaczDpi7xSYC4Jvdo1Q==", + "license": "ISC" + }, + "node_modules/libsodium-wrappers-sumo": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.8.2.tgz", + "integrity": "sha512-wd1xAY++Kr6VMikSaa4EPRAHJmFvNlGWiiwU3Jh3GR1zRYF3/I3vy/wYsr4k3LVsNzwb9sqfEQ4LdVQ6zEebyQ==", + "license": "ISC", + "dependencies": { + "libsodium-sumo": "^0.8.0" + } + }, "node_modules/lightningcss": { "version": "1.30.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", @@ -8835,6 +8861,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.17.2.tgz", "integrity": "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.10.1", "pg-pool": "^3.11.0", @@ -9021,6 +9048,7 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -9110,6 +9138,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -9119,6 +9148,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -9932,6 +9962,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -10608,6 +10639,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10767,6 +10799,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -11344,6 +11377,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -11608,6 +11642,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 34d4074..6c2f6d5 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "crypto-js": "^4.2.0", "drizzle-orm": "^0.44.1", "jose": "^6.0.11", + "libsodium-wrappers-sumo": "^0.8.2", "lucide-react": "^0.562.0", "next": "16.1.4", "next-auth": "^5.0.0-beta.25", diff --git a/scripts/clear-chat-keys.ts b/scripts/clear-chat-keys.ts new file mode 100644 index 0000000..b838069 --- /dev/null +++ b/scripts/clear-chat-keys.ts @@ -0,0 +1,16 @@ +import { db } from '../src/db'; +import { chatDeviceBundles } from '../src/db/schema'; + +async function clearChatKeys() { + console.log('Clearing all chat device bundles...'); + + const result = await db.delete(chatDeviceBundles); + + console.log('Cleared chat device bundles'); + process.exit(0); +} + +clearChatKeys().catch((error) => { + console.error('Failed to clear keys:', error); + process.exit(1); +}); diff --git a/src/app/.well-known/synapsis/chat/[did]/route.ts b/src/app/.well-known/synapsis/chat/[did]/route.ts index 6c3a814..16533b6 100644 --- a/src/app/.well-known/synapsis/chat/[did]/route.ts +++ b/src/app/.well-known/synapsis/chat/[did]/route.ts @@ -10,33 +10,24 @@ export async function GET( ) { const { did } = await params; - // 1. Fetch all devices for this DID - const bundles = await db.query.chatDeviceBundles.findMany({ + // Fetch device bundle for this DID + const bundle = await db.query.chatDeviceBundles.findFirst({ where: eq(chatDeviceBundles.did, did), - with: { - oneTimeKeys: { - limit: 5, // Return a few keys; client picks one - // Ideally we pick non-conflicted ones or random ones - } - } }); - if (!bundles || bundles.length === 0) { - return NextResponse.json([], { status: 404 }); + if (!bundle) { + return NextResponse.json({ error: 'No keys found' }, { status: 404 }); } - // 2. Format Response - const response = bundles.map(b => ({ - did: b.did, - deviceId: b.deviceId, - identityKey: b.identityKey, // Base64 X25519 - signedPreKey: JSON.parse(b.signedPreKey), - oneTimeKeys: b.oneTimeKeys.map(k => ({ - id: k.keyId, - key: k.publicKey - })), - signature: b.signature // ECDSA signature of this bundle - })); + const signedPreKey = JSON.parse(bundle.signedPreKey); + const kyberPreKey = bundle.kyberPreKey ? JSON.parse(bundle.kyberPreKey) : null; + + // Format Response for Olm + const response = { + identityKey: bundle.identityKey, // curve25519 + signingKey: signedPreKey.signingKey, // ed25519 + oneTimeKeys: kyberPreKey?.oneTimeKeys || [], + }; return NextResponse.json(response, { headers: { diff --git a/src/app/api/chat/keys/route.ts b/src/app/api/chat/keys/route.ts index 82b5298..3c3d633 100644 --- a/src/app/api/chat/keys/route.ts +++ b/src/app/api/chat/keys/route.ts @@ -1,79 +1,120 @@ - import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/db'; -import { chatDeviceBundles, chatOneTimeKeys } from '@/db/schema'; -import { requireSignedAction } from '@/lib/auth/verify-signature'; +import { chatDeviceBundles } from '@/db/schema'; +import { requireAuth } from '@/lib/auth'; import { eq, and } from 'drizzle-orm'; +/** + * GET /api/chat/keys?did= + * Fetch public key for a user + */ +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const did = searchParams.get('did'); + + if (!did) { + return NextResponse.json({ error: 'Missing did parameter' }, { status: 400 }); + } + + console.log('[Chat Keys GET] Looking for DID:', did); + + const bundle = await db.query.chatDeviceBundles.findFirst({ + where: eq(chatDeviceBundles.did, did), + }); + + console.log('[Chat Keys GET] Found bundle:', bundle ? 'YES' : 'NO'); + if (bundle) { + console.log('[Chat Keys GET] Bundle data:', { + userId: bundle.userId, + did: bundle.did, + deviceId: bundle.deviceId, + hasIdentityKey: !!bundle.identityKey, + }); + } + + if (!bundle) { + return NextResponse.json({ error: 'No keys found' }, { status: 404 }); + } + + // For libsodium, we just need the public key + return NextResponse.json({ + publicKey: bundle.identityKey, // Reusing identityKey field for libsodium public key + }); + } catch (error: any) { + console.error('[Chat Keys] Failed to fetch keys:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} + /** * POST /api/chat/keys - * Publish a new Device Bundle and OTKs. + * Publish public key * - * Payload: SignedAction < { - * deviceId: string, - * identityKey: string, - * signedPreKey: { id, key, sig }, - * signature: string, (The ECDSA signature of the bundle itself) - * oneTimeKeys: { id, key }[] - * } > + * Body: { + * publicKey: string (base64) + * } */ export async function POST(request: NextRequest) { try { + const user = await requireAuth(); + + console.log('[Chat Keys POST] User:', user.handle, 'DID:', user.did, 'UserID:', user.id); + const body = await request.json(); + const { publicKey } = body; - // 1. Verify User Identity (ECDSA Root) - // The wrapper itself is a SignedAction with action="chat.keys.publish" - // This proves the user (DID) is authorizing this device registration. - const user = await requireSignedAction(body); + console.log('[Chat Keys POST] Received publicKey:', publicKey ? publicKey.substring(0, 20) + '...' : 'MISSING'); - const { deviceId, identityKey, signedPreKey, signature, oneTimeKeys } = body.data; + if (!publicKey) { + return NextResponse.json({ error: 'Missing publicKey' }, { status: 400 }); + } - // 2. Validate Bundle Signature (The bundle.signature must cover the fields) - // Actually, requireSignedAction already verified the payload signature. - // The "signature" field inside data is redundant if the whole thing is signed by DID? - // Or is "signature" the signature of the bundle bytes? - // In our design, the SignedAction *is* the signature. - // So "signature" inside might be the "Self-Signature" of the X25519 Identity Key signing the structure? - // No, standard Signal: The Bundle is signed by Identity Key. - // Here, Identity Key is ECDSA. The SignedAction covers it. - // So we just trust the SignedAction. - - // 3. Upsert Bundle - await db.transaction(async (tx) => { - // Upsert device bundle - await tx.delete(chatDeviceBundles).where( - and( - eq(chatDeviceBundles.userId, user.id), - eq(chatDeviceBundles.deviceId, deviceId) - ) - ); - - const [bundle] = await tx.insert(chatDeviceBundles).values({ - userId: user.id, - did: user.did, - deviceId, - identityKey, - signedPreKey: JSON.stringify(signedPreKey), - signature, // We store the action signature or the explicit inner signature provided - }).returning(); - - // 4. Insert OTKs - if (oneTimeKeys && oneTimeKeys.length > 0) { - await tx.insert(chatOneTimeKeys).values( - oneTimeKeys.map((k: any) => ({ - userId: user.id, - bundleId: bundle.id, - keyId: k.id, - publicKey: k.key - })) - ).onConflictDoNothing(); - } + // Check if bundle exists + const existing = await db.query.chatDeviceBundles.findFirst({ + where: and( + eq(chatDeviceBundles.userId, user.id), + eq(chatDeviceBundles.deviceId, '1') + ) }); - return NextResponse.json({ success: true, count: oneTimeKeys?.length || 0 }); + console.log('[Chat Keys POST] Existing bundle found:', existing ? 'YES' : 'NO'); + if (existing) { + // Update existing bundle + console.log('[Chat Keys POST] Updating existing bundle...'); + await db.update(chatDeviceBundles) + .set({ + identityKey: publicKey, + did: user.did, // Update DID too in case it changed + lastSeenAt: new Date(), + }) + .where( + and( + eq(chatDeviceBundles.userId, user.id), + eq(chatDeviceBundles.deviceId, '1') + ) + ); + console.log('[Chat Keys POST] Updated existing key'); + } else { + // Insert new bundle + console.log('[Chat Keys POST] Inserting new bundle...'); + await db.insert(chatDeviceBundles).values({ + userId: user.id, + did: user.did, + deviceId: '1', + identityKey: publicKey, + signedPreKey: 'libsodium', // Placeholder + registrationId: 1, + signature: 'libsodium', + }); + console.log('[Chat Keys POST] Inserted new key'); + } + + console.log('[Chat Keys POST] Key published successfully for', user.handle); + return NextResponse.json({ success: true }); } catch (error: any) { - console.error('Failed to publish keys:', error); - return NextResponse.json({ error: error.message }, { status: 400 }); + console.error('[Chat Keys] Failed to publish key:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); } -} +} \ No newline at end of file diff --git a/src/app/api/chat/send/route.ts b/src/app/api/chat/send/route.ts index 824673f..2eef2a6 100644 --- a/src/app/api/chat/send/route.ts +++ b/src/app/api/chat/send/route.ts @@ -1,212 +1,116 @@ - import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/db'; -import { chatInbox, chatConversations, chatMessages, users } from '@/db/schema'; -import { requireSignedAction } from '@/lib/auth/verify-signature'; +import { chatConversations, chatMessages, users } from '@/db/schema'; +import { requireAuth } from '@/lib/auth'; import { eq, and } from 'drizzle-orm'; -import { signedFetch } from '@/lib/api/signed-fetch'; /** * POST /api/chat/send - * Deliver an encrypted envelope to a local or remote user's inbox. + * Store encrypted message (server never decrypts) + * + * Body: { + * recipientDid: string, + * senderPublicKey: string (base64), + * ciphertext: string (base64), + * nonce: string (base64), + * recipientHandle?: string + * } */ export async function POST(request: NextRequest) { try { + const user = await requireAuth(); + const body = await request.json(); + const { recipientDid, senderPublicKey, ciphertext, nonce, recipientHandle } = body; - // 1. Verify Envelope Signature (Anti-Spoofing & Replay) - const user = await requireSignedAction(body); - - const { action, data } = body; - if (action !== 'chat.deliver') { - return NextResponse.json({ error: 'Invalid action type' }, { status: 400 }); + if (!recipientDid || !senderPublicKey || !ciphertext || !nonce) { + return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); } - const { recipientDid, recipientDeviceId, ciphertext, nodeDomain, recipientHandle } = data; - - if (!recipientDid || !ciphertext) { - return NextResponse.json({ error: 'Missing required delivery fields' }, { status: 400 }); - } - - // 2. Check if recipient is local or remote + // Check if recipient is local const recipientUser = await db.query.users.findFirst({ where: eq(users.did, recipientDid) }); if (recipientUser) { - // LOCAL RECIPIENT - Store in local inbox + // LOCAL RECIPIENT - // Ensure conversation exists for recipient - let conversation = await db.query.chatConversations.findFirst({ + // Ensure conversations exist + let recipientConv = await db.query.chatConversations.findFirst({ where: and( eq(chatConversations.participant1Id, recipientUser.id), eq(chatConversations.participant2Handle, user.handle) ) }); - if (!conversation) { + if (!recipientConv) { const [newConv] = await db.insert(chatConversations).values({ participant1Id: recipientUser.id, participant2Handle: user.handle, lastMessageAt: new Date(), - lastMessagePreview: '[Encrypted Message]' + lastMessagePreview: '[Encrypted]' }).returning(); - conversation = newConv; + recipientConv = newConv; } - // Ensure conversation exists for sender - let senderConversation = await db.query.chatConversations.findFirst({ + let senderConv = await db.query.chatConversations.findFirst({ where: and( eq(chatConversations.participant1Id, user.id), eq(chatConversations.participant2Handle, recipientUser.handle) ) }); - if (!senderConversation) { - await db.insert(chatConversations).values({ + if (!senderConv) { + const [newConv] = await db.insert(chatConversations).values({ participant1Id: user.id, participant2Handle: recipientUser.handle, lastMessageAt: new Date(), - lastMessagePreview: '[Encrypted Message]' - }); + lastMessagePreview: '[Encrypted]' + }).returning(); + senderConv = newConv; } - // Insert into local inbox - await db.insert(chatInbox).values({ + // Store encrypted message (libsodium format) + // Include recipientDid so sender can decrypt their own sent messages + const messageData = { + senderPublicKey, + recipientDid, // Add this so sender knows who to decrypt with + ciphertext, + nonce, + }; + + // Create message for recipient + await db.insert(chatMessages).values({ + conversationId: recipientConv.id, + senderHandle: user.handle, + senderDisplayName: user.displayName, + senderAvatarUrl: user.avatarUrl, + senderNodeDomain: null, senderDid: user.did, - recipientDid, - recipientDeviceId: recipientDeviceId || null, - envelope: JSON.stringify(body), - expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) + encryptedContent: JSON.stringify(messageData), + deliveredAt: new Date(), }); - return NextResponse.json({ success: true, local: true }); + // Create message for sender + await db.insert(chatMessages).values({ + conversationId: senderConv.id, + senderHandle: user.handle, + senderDisplayName: user.displayName, + senderAvatarUrl: user.avatarUrl, + senderNodeDomain: null, + senderDid: user.did, + encryptedContent: JSON.stringify(messageData), + deliveredAt: new Date(), + }); + return NextResponse.json({ success: true }); } else { - // REMOTE RECIPIENT - Forward to their node - - let targetNodeDomain = nodeDomain; - - if (!targetNodeDomain) { - // Try to extract from DID - if (recipientDid.startsWith('did:web:')) { - targetNodeDomain = recipientDid.replace('did:web:', ''); - } else { - return NextResponse.json({ - error: 'Remote delivery requires node domain' - }, { status: 400 }); - } - } - - // Forward the signed envelope to the remote node - const remoteUrl = `https://${targetNodeDomain}/api/swarm/chat/inbox`; - - try { - console.log('[Chat Send] Forwarding to remote:', remoteUrl); - - // Get or create conversation for sender - let conversation = await db.query.chatConversations.findFirst({ - where: and( - eq(chatConversations.participant1Id, user.id), - eq(chatConversations.participant2Handle, recipientHandle || recipientDid) - ) - }); - - if (!conversation) { - const [newConv] = await db.insert(chatConversations).values({ - participant1Id: user.id, - participant2Handle: recipientHandle || recipientDid, - lastMessageAt: new Date(), - lastMessagePreview: '[Encrypted Message]' - }).returning(); - conversation = newConv; - console.log('[Chat Send] Created conversation:', conversation.id); - } - - // Store message locally so sender can see it - const messageId = crypto.randomUUID(); - const localDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost'; - const swarmMessageId = `swarm:${localDomain}:${messageId}`; - - // Store full envelope data for decryption - const envelopeData = { - did: user.did, - handle: user.handle, - ciphertext: ciphertext - }; - - const [newMessage] = await db.insert(chatMessages).values({ - conversationId: conversation.id, - senderHandle: user.handle, - senderDisplayName: user.displayName, - senderAvatarUrl: user.avatarUrl, - senderNodeDomain: null, // Local sender - encryptedContent: JSON.stringify(envelopeData), // Full envelope - senderChatPublicKey: null, // V2 E2E - keys are in the envelope - swarmMessageId, - deliveredAt: null, // Will update when remote confirms - readAt: null, - }).returning(); - - console.log('[Chat Send] Stored local message:', newMessage.id); - - const response = await fetch(remoteUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body) - }); - - if (!response.ok) { - const errorText = await response.text(); - let errorData; - try { - errorData = JSON.parse(errorText); - } catch { - errorData = { raw: errorText }; - } - console.error('[Chat Send] Remote delivery failed:', response.status, errorData); - - // Check if remote node doesn't support V2 (returns V1 validation errors) - const isV1ValidationError = errorData.details && - Array.isArray(errorData.details) && - errorData.details.some((d: any) => - d.path && ['messageId', 'senderHandle', 'senderNodeDomain', 'recipientHandle', 'encryptedContent', 'timestamp'].includes(d.path[0]) - ); - - if (isV1ValidationError) { - return NextResponse.json({ - error: 'Remote node needs update', - details: 'The recipient node is running an older version that does not support the V2 chat protocol. Please ask the node administrator to update to the latest version.', - remoteError: errorData - }, { status: 400 }); - } - - return NextResponse.json({ - error: 'Remote delivery failed', - details: errorData - }, { status: response.status }); - } - - // Mark message as delivered since remote accepted it - await db.update(chatMessages) - .set({ deliveredAt: new Date() }) - .where(eq(chatMessages.id, newMessage.id)); - - console.log('[Chat Send] Message marked as delivered'); - - return NextResponse.json({ success: true, remote: true, messageId: newMessage.id }); - - } catch (error: any) { - console.error('[Chat Send] Remote delivery error:', error); - return NextResponse.json({ - error: 'Failed to reach remote node', - details: error.message - }, { status: 500 }); - } + // REMOTE RECIPIENT - not implemented yet + return NextResponse.json({ error: 'Remote delivery not yet implemented' }, { status: 501 }); } } catch (error: any) { - console.error('Delivery failed:', error); - return NextResponse.json({ error: error.message }, { status: 400 }); + console.error('Send failed:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); } -} +} \ No newline at end of file diff --git a/src/app/api/debug/check-chat-keys/route.ts b/src/app/api/debug/check-chat-keys/route.ts new file mode 100644 index 0000000..bf57b5a --- /dev/null +++ b/src/app/api/debug/check-chat-keys/route.ts @@ -0,0 +1,50 @@ +import { NextResponse } from 'next/server'; +import { db, users, chatDeviceBundles } from '@/db'; +import { eq } from 'drizzle-orm'; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const handle = searchParams.get('handle'); + + if (!handle) { + return NextResponse.json({ error: 'Missing handle parameter' }, { status: 400 }); + } + + // Find user + const user = await db.query.users.findFirst({ + where: eq(users.handle, handle.toLowerCase()), + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + // Check for device bundles + const bundles = await db.query.chatDeviceBundles.findMany({ + where: eq(chatDeviceBundles.userId, user.id), + with: { + oneTimeKeys: true, + }, + }); + + return NextResponse.json({ + user: { + id: user.id, + handle: user.handle, + did: user.did, + hasChatPublicKey: !!user.chatPublicKey, + }, + bundles: bundles.map(b => ({ + deviceId: b.deviceId, + identityKey: b.identityKey?.substring(0, 20) + '...', + hasSignedPreKey: !!b.signedPreKey, + oneTimeKeysCount: b.oneTimeKeys.length, + })), + bundleCount: bundles.length, + }); + } catch (error) { + console.error('Check chat keys error:', error); + return NextResponse.json({ error: 'Failed to check keys' }, { status: 500 }); + } +} diff --git a/src/app/api/debug/check-keys/route.ts b/src/app/api/debug/check-keys/route.ts new file mode 100644 index 0000000..84461f9 --- /dev/null +++ b/src/app/api/debug/check-keys/route.ts @@ -0,0 +1,19 @@ +import { NextResponse } from 'next/server'; +import { db } from '@/db'; +import { chatDeviceBundles } from '@/db/schema'; + +export async function GET() { + try { + const bundles = await db.select({ + did: chatDeviceBundles.did, + userId: chatDeviceBundles.userId, + deviceId: chatDeviceBundles.deviceId, + identityKey: chatDeviceBundles.identityKey, + createdAt: chatDeviceBundles.createdAt, + }).from(chatDeviceBundles).orderBy(chatDeviceBundles.createdAt); + + return NextResponse.json({ bundles, count: bundles.length }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/src/app/api/debug/clear-chat-keys/route.ts b/src/app/api/debug/clear-chat-keys/route.ts new file mode 100644 index 0000000..163e738 --- /dev/null +++ b/src/app/api/debug/clear-chat-keys/route.ts @@ -0,0 +1,18 @@ +import { NextResponse } from 'next/server'; +import { db } from '@/db'; +import { chatDeviceBundles } from '@/db/schema'; + +export async function POST() { + try { + console.log('[Debug] Clearing all chat device bundles...'); + + await db.delete(chatDeviceBundles); + + console.log('[Debug] Cleared all chat device bundles'); + + return NextResponse.json({ success: true, message: 'Cleared all chat keys' }); + } catch (error: any) { + console.error('[Debug] Failed to clear keys:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/src/app/api/search/route.ts b/src/app/api/search/route.ts index 5331bdc..9840b59 100644 --- a/src/app/api/search/route.ts +++ b/src/app/api/search/route.ts @@ -33,7 +33,7 @@ const parseRemoteHandleQuery = (query: string): { handle: string; domain: string export async function GET(request: Request) { try { const { searchParams } = new URL(request.url); - const query = searchParams.get('q') || ''; + let query = searchParams.get('q') || ''; const type = searchParams.get('type') || 'all'; // all, users, posts const limit = Math.min(parseInt(searchParams.get('limit') || '20'), 50); @@ -50,7 +50,22 @@ export async function GET(request: Request) { }); } - const searchPattern = `%${query}%`; + // Normalize query for local user search + // Strip leading @ and local domain if present + let localSearchQuery = query.trim(); + if (localSearchQuery.startsWith('@')) { + localSearchQuery = localSearchQuery.slice(1); + } + // Remove local domain if searching like "admin2@dev.syn.quest" + const localDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || process.env.NODE_DOMAIN; + if (localDomain && localSearchQuery.includes('@')) { + const parts = localSearchQuery.split('@'); + if (parts[1] === localDomain) { + localSearchQuery = parts[0]; + } + } + + const searchPattern = `%${localSearchQuery}%`; let searchUsers: SearchUser[] = []; let searchPosts: typeof posts.$inferSelect[] = []; diff --git a/src/app/api/swarm/chat/inbox/route.ts b/src/app/api/swarm/chat/inbox/route.ts index 419d6f4..7a2edcf 100644 --- a/src/app/api/swarm/chat/inbox/route.ts +++ b/src/app/api/swarm/chat/inbox/route.ts @@ -64,8 +64,13 @@ export async function POST(request: NextRequest) { // Also create conversation and message for recipient UI // Extract sender info from envelope - const senderHandle = body.handle || body.did; // Fallback to DID if no handle - const senderFullHandle = `${senderHandle}@${body.did?.split(':')[2] || 'unknown'}`; // Extract domain from DID + const senderHandle = body.handle || 'unknown'; + const senderNodeDomain = body.data?.senderNodeDomain || 'unknown'; + const senderFullHandle = senderNodeDomain !== 'unknown' + ? `${senderHandle}@${senderNodeDomain}` + : senderHandle; + + console.log('[Swarm Chat V2] Sender info:', { senderHandle, senderNodeDomain, senderFullHandle }); // Get or create conversation for recipient let conversation = await db.query.chatConversations.findFirst({ @@ -108,7 +113,7 @@ export async function POST(request: NextRequest) { senderHandle: senderFullHandle, senderDisplayName: null, // Unknown until decrypted senderAvatarUrl: null, - senderNodeDomain: body.did?.split(':')[2] || null, + senderNodeDomain: senderNodeDomain !== 'unknown' ? senderNodeDomain : null, encryptedContent: JSON.stringify(envelopeData), // Full envelope for decryption senderChatPublicKey: null, swarmMessageId: `swarm:v2:${messageId}`, diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx index 0558642..5768f8c 100644 --- a/src/app/chat/page.tsx +++ b/src/app/chat/page.tsx @@ -3,43 +3,11 @@ import { useState, useEffect, useRef } from 'react'; import { useAuth } from '@/lib/contexts/AuthContext'; -import { useChatEncryption } from '@/lib/hooks/useChatEncryption'; -import { loadDeviceKeys } from '@/lib/crypto/chat-storage'; +import { useSodiumChat } from '@/lib/hooks/useSodiumChat'; import { ArrowLeft, Send, Lock, Shield, Loader2, MessageCircle, Search, Plus, Trash2, MoreVertical } from 'lucide-react'; import { formatFullHandle } from '@/lib/utils/handle'; import { useRouter, useSearchParams } from 'next/navigation'; -// Helper to decrypt self-encrypted message (copied from useChatEncryption) -async function decryptForSelf(selfEncrypted: { ciphertext: string; iv: string }, identityKeyPair: CryptoKeyPair): Promise { - // Export public key raw to use as key material (same as encryption) - const keyMaterial = await crypto.subtle.exportKey('raw', identityKeyPair.publicKey); - - // Derive AES key - const aesKey = await crypto.subtle.digest('SHA-256', keyMaterial); - - // Import as AES-GCM key - const cryptoKey = await crypto.subtle.importKey( - 'raw', - aesKey, - { name: 'AES-GCM', length: 256 }, - false, - ['decrypt'] - ); - - // Decode IV and ciphertext - const iv = Uint8Array.from(atob(selfEncrypted.iv), c => c.charCodeAt(0)); - const ciphertext = Uint8Array.from(atob(selfEncrypted.ciphertext), c => c.charCodeAt(0)); - - const decrypted = await crypto.subtle.decrypt( - { name: 'AES-GCM', iv }, - cryptoKey, - ciphertext - ); - - const decoder = new TextDecoder(); - return decoder.decode(decrypted); -} - interface Conversation { id: string; participant2: { @@ -69,15 +37,13 @@ interface Message { } export default function ChatPage() { - const { user, setShowUnlockPrompt } = useAuth(); + const { user, isIdentityUnlocked, setShowUnlockPrompt } = useAuth(); const router = useRouter(); - // V2 Hook Destructuring - const { isReady, isLocked, status, ensureReady, sendMessage, decryptMessage } = useChatEncryption(); + // Libsodium E2EE Hook + const { isReady, status, sendMessage, decryptMessage } = useSodiumChat(); const searchParams = useSearchParams(); const composeHandle = searchParams.get('compose'); - - // Chat Data State const [conversations, setConversations] = useState([]); const [selectedConversation, setSelectedConversation] = useState(null); @@ -88,15 +54,71 @@ export default function ChatPage() { const [loading, setLoading] = useState(true); const [sending, setSending] = useState(false); const [searchQuery, setSearchQuery] = useState(''); + const [loadingMessages, setLoadingMessages] = useState(false); + + // Cache for decrypted messages to avoid re-decrypting on every poll + const decryptedCacheRef = useRef>(new Map()); + // Track which messages we've attempted to decrypt (even if they failed) + const attemptedDecryptionRef = useRef>(new Set()); + // Track the current conversation ID to prevent race conditions + const currentConversationIdRef = useRef(null); + + // Load conversations + useEffect(() => { + if (user && isReady) { + loadConversations(true); // Initial load with spinner + + // Poll for new conversations every 5 seconds (no spinner) + const pollInterval = setInterval(() => { + loadConversations(false); + }, 5000); + + return () => clearInterval(pollInterval); + } + }, [user, isReady]); // Handle Compose Intent useEffect(() => { - if (composeHandle && isReady && !selectedConversation && !showNewChat) { - setNewChatHandle(composeHandle); - setShowNewChat(true); - // We could auto-submit here if we refactored startNewChat to be separate from event hnadler + if (composeHandle && isReady && !selectedConversation && conversations.length >= 0) { + // Check if we already have a conversation with this user + const existing = conversations.find(c => + c.participant2.handle.toLowerCase() === composeHandle.toLowerCase() + ); + + if (existing) { + setSelectedConversation(existing); + // Clear the query param so refresh doesn't keep resetting state + router.replace('/chat', { scroll: false }); + } else if (!loading) { + // Fetch user details to create a draft conversation + const fetchUserAndInitDraft = async () => { + try { + const res = await fetch(`/api/users/${encodeURIComponent(composeHandle)}`); + const data = await res.json(); + if (data.user) { + const draftConv: Conversation = { + id: 'new', + participant2: { + handle: data.user.handle, + displayName: data.user.displayName || data.user.handle, + avatarUrl: data.user.avatarUrl, + did: data.user.did + }, + lastMessageAt: new Date().toISOString(), + lastMessagePreview: 'New Conversation', + unreadCount: 0 + }; + setSelectedConversation(draftConv); + router.replace('/chat', { scroll: false }); + } + } catch (e) { + console.error("Failed to load user for compose", e); + } + }; + fetchUserAndInitDraft(); + } } - }, [composeHandle, isReady, selectedConversation, showNewChat]); + }, [composeHandle, isReady, selectedConversation, conversations, loading, router]); // Legacy / V2 Hybrid State @@ -135,33 +157,39 @@ export default function ChatPage() { } }, [user, router]); - // Load conversations - useEffect(() => { - if (user && isReady) { - // ... existing loadConversations code ... - loadConversations(true); // Initial load with spinner - - // Poll for new conversations every 5 seconds (no spinner) - const pollInterval = setInterval(() => { - loadConversations(false); - }, 5000); - - return () => clearInterval(pollInterval); - } - }, [user, isReady]); - // Load messages when conversation is selected useEffect(() => { if (selectedConversation && isReady) { + // Update current conversation ref + currentConversationIdRef.current = selectedConversation.id; + + // Clear messages immediately to prevent flash + setMessages([]); + + if (selectedConversation.id === 'new') { + setLoadingMessages(false); + return; // Don't load messages for new/draft conversation + } + + setLoadingMessages(true); + loadMessages(selectedConversation.id); markAsRead(selectedConversation.id); // Poll for new messages every 3 seconds const pollInterval = setInterval(() => { - loadMessages(selectedConversation.id); + // Only load if still the same conversation + if (currentConversationIdRef.current === selectedConversation.id && selectedConversation.id !== 'new') { + loadMessages(selectedConversation.id); + } }, 3000); return () => clearInterval(pollInterval); + } else if (!selectedConversation) { + // Clear messages when no conversation selected + currentConversationIdRef.current = null; + setMessages([]); + setLoadingMessages(false); } }, [selectedConversation, isReady]); @@ -192,97 +220,77 @@ export default function ChatPage() { const decrypted = await Promise.all((data.messages || []).map(async (msg: any) => { try { - console.log('[Chat UI] Processing message:', { - id: msg.id, - isSentByMe: msg.isSentByMe, - hasEncryptedContent: !!msg.encryptedContent, - contentPreview: msg.encryptedContent?.substring(0, 100) - }); + // Check cache first + const cacheKey = `${msg.id}`; + const cached = decryptedCacheRef.current.get(cacheKey); + if (cached) { + return { ...msg, decryptedContent: cached }; + } - // Check if this is a V2 encrypted message (JSON envelope) + // Check if already attempted + if (attemptedDecryptionRef.current.has(cacheKey)) { + const fallback = decryptedCacheRef.current.get(cacheKey) || '🔒 [Encrypted]'; + return { ...msg, decryptedContent: fallback }; + } + + // Mark as attempted + attemptedDecryptionRef.current.add(cacheKey); + + // Parse libsodium message format if (msg.encryptedContent && msg.encryptedContent.startsWith('{')) { try { - // First layer: { did, handle, ciphertext } const envelope = JSON.parse(msg.encryptedContent); - console.log('[Chat UI] Parsed envelope:', { - hasDid: !!envelope.did, - hasHandle: !!envelope.handle, - hasCiphertext: !!envelope.ciphertext, - ciphertextPreview: envelope.ciphertext?.substring(0, 100) - }); - // Second layer: the actual payload with selfEncrypted - let payload; - if (envelope.ciphertext && typeof envelope.ciphertext === 'string' && envelope.ciphertext.startsWith('{')) { - payload = JSON.parse(envelope.ciphertext); - console.log('[Chat UI] Parsed V2 payload:', { - hasSelfEncrypted: !!payload.selfEncrypted, - hasCiphertext: !!payload.ciphertext, - recipientDeviceId: payload.recipientDeviceId, - senderDeviceId: payload.senderDeviceId - }); - } else { - // Old format or malformed - payload = envelope; - } + // Libsodium format: {senderPublicKey, recipientDid, ciphertext, nonce} + if (envelope.senderPublicKey && envelope.ciphertext && envelope.nonce) { + // For decryption with crypto_box_open_easy: + // - We need the OTHER party's public key + // - We use OUR private key - // For messages we sent, try to decrypt the selfEncrypted copy - if (msg.isSentByMe && payload.selfEncrypted && user?.did) { - console.log('[Chat UI] Attempting to decrypt self-encrypted message'); - try { - const localKeys = await loadDeviceKeys(); - if (localKeys) { - const plaintext = await decryptForSelf(payload.selfEncrypted, localKeys.identity); - console.log('[Chat UI] Self-decrypt successful:', plaintext.substring(0, 50)); - return { ...msg, decryptedContent: plaintext }; - } - } catch (e) { - console.error('[Chat UI] Self-decrypt failed:', e); - } - } + // console.log('[Chat UI] Decrypting message:', { + // isSentByMe: msg.isSentByMe, + // recipientDid: envelope.recipientDid, + // senderPublicKey: envelope.senderPublicKey?.substring(0, 20) + '...' + // }); - // For messages we received, decrypt normally - if (!msg.isSentByMe) { - console.log('[Chat UI] Attempting to decrypt received message'); - // Need to get the sender's DID - let senderDid = msg.senderDid || envelope.did; - - if (!senderDid && msg.senderHandle) { - // Try to resolve DID from handle + // If I sent this message, the "other party" is the recipient + // If I received this message, the "other party" is the sender + let otherPartyPublicKey = envelope.senderPublicKey; + + if (msg.isSentByMe && envelope.recipientDid) { + // I'm the sender, so I need the recipient's public key to decrypt my own message try { - const userRes = await fetch(`/api/users/${encodeURIComponent(msg.senderHandle)}`); - if (userRes.ok) { - const userData = await userRes.json(); - senderDid = userData.user?.did; + const keyRes = await fetch(`/api/chat/keys?did=${encodeURIComponent(envelope.recipientDid)}`); + if (keyRes.ok) { + const keyData = await keyRes.json(); + otherPartyPublicKey = keyData.publicKey; + // console.log('[Chat UI] Fetched recipient public key:', otherPartyPublicKey?.substring(0, 20) + '...'); } } catch (e) { - console.error('[Chat UI] Failed to resolve sender DID:', e); + console.error('[Chat UI] Failed to fetch recipient key:', e); } + } else { + // console.log('[Chat UI] Using sender public key from envelope'); } - if (senderDid) { - // Create the envelope structure that decryptMessage expects - const decryptEnvelope = { - did: senderDid, - data: { - ciphertext: envelope.ciphertext // Pass the inner payload JSON string - } - }; - const dec = await decryptMessage(decryptEnvelope); - console.log('[Chat UI] Received decrypt result:', dec.substring(0, 50)); - if (!dec.startsWith('[')) { - return { ...msg, decryptedContent: dec }; - } - } + const plaintext = await decryptMessage( + envelope.ciphertext, + envelope.nonce, + otherPartyPublicKey + ); + + decryptedCacheRef.current.set(cacheKey, plaintext); + return { ...msg, decryptedContent: plaintext }; } } catch (e) { - console.error('[Chat UI] V2 decryption failed:', e); + console.error('[Chat UI] Libsodium decryption failed:', e); } } - // Fallback: show encrypted indicator - console.log('[Chat UI] Could not decrypt message, showing encrypted'); - return { ...msg, decryptedContent: '[Encrypted]' }; + // Fallback + const fallback = '🔒 [Encrypted - refresh page]'; + decryptedCacheRef.current.set(cacheKey, fallback); + return { ...msg, decryptedContent: fallback }; } catch (err) { console.error('[Chat UI] Message processing error:', err); return { ...msg, decryptedContent: '[Error]' }; @@ -290,8 +298,8 @@ export default function ChatPage() { })); setMessages(decrypted); - } catch (err) { - console.error('[Chat UI] Load messages error:', err); + } catch (err) { + console.error('[Chat UI] Load messages error:', err); } }; @@ -311,36 +319,42 @@ export default function ChatPage() { if (!newMessage.trim() || !selectedConversation) return; setSending(true); try { - // Need Recipient DID. - // conversation.participant2 might have valid handle. - // We resolve DID first. + // Get recipient DID let did = selectedConversation.participant2.did; - // We need to support nodeDomain for existing chats too. - // But Conversation interface might lack it. - // We can try to resolve it from handle if needed, or if we stored it? - // "participant2" comes from API. - // Let's assume we re-fetch to be safe if it's remote? - // Or just check handle structure? - let nodeDomain = undefined; - if (selectedConversation.participant2.handle.includes('@')) { - const parts = selectedConversation.participant2.handle.split('@'); - if (parts.length === 2) nodeDomain = parts[1]; - } if (!did) { const res = await fetch(`/api/users/${encodeURIComponent(selectedConversation.participant2.handle)}`); const data = await res.json(); did = data.user?.did; - nodeDomain = data.user?.nodeDomain || nodeDomain; // API is authoritative if (!did) throw new Error('User not found'); } - await sendMessage(did, newMessage, nodeDomain, selectedConversation.participant2.handle); + // Send using Signal Protocol + await sendMessage(did, newMessage, selectedConversation.participant2.handle); - // Legacy UI expects message reload. setNewMessage(''); - await loadMessages(selectedConversation.id); - loadConversations(false); + + // If this was a new conversation, we need to refresh the conversation list and select the real one + if (selectedConversation.id === 'new') { + // Refresh conversations to get the new ID + const res = await fetch('/api/swarm/chat/conversations'); + const data = await res.json(); + const updatedConversations = data.conversations || []; + setConversations(updatedConversations); + + // Find the real conversation + const realConv = updatedConversations.find((c: Conversation) => + c.participant2.handle === selectedConversation.participant2.handle + ); + + if (realConv) { + setSelectedConversation(realConv); + loadMessages(realConv.id); + } + } else { + await loadMessages(selectedConversation.id); + loadConversations(false); + } } catch (err: any) { console.error('[Send] Error:', err); alert(`Failed: ${err.message}`); @@ -354,36 +368,66 @@ export default function ChatPage() { if (!newChatHandle.trim()) return; setSending(true); try { - const cleanHandle = newChatHandle.replace(/^@/, ''); + let cleanHandle = newChatHandle.replace(/^@/, ''); + + // If the handle includes a domain, check if it's our local domain + if (cleanHandle.includes('@')) { + const [handle, domain] = cleanHandle.split('@'); + const localDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || window.location.host; + + // If it's our local domain, strip it for the API call + if (domain === localDomain) { + cleanHandle = handle; + } + } + const res = await fetch(`/api/users/${encodeURIComponent(cleanHandle)}`); const data = await res.json(); + if (!data.user?.did) { - alert('User not found or V2 not enabled.'); + alert('User not found or Olm encryption not enabled.'); setSending(false); return; } - console.log('[Chat UI] Starting chat with:', data.user); + // Previously we auto-sent "👋" here. + // Now we just setup the draft conversation. - // Send "Hello" to init session - await sendMessage(data.user.did, '👋', data.user.nodeDomain, data.user.handle); + // Check if existing conversation + const existing = conversations.find(c => + c.participant2.handle.toLowerCase() === data.user.handle.toLowerCase() + ); - console.log('[Chat UI] Message sent, reloading conversations'); + if (existing) { + setSelectedConversation(existing); + } else { + // Setup draft + const draftConv: Conversation = { + id: 'new', + participant2: { + handle: data.user.handle, + displayName: data.user.displayName || data.user.handle, + avatarUrl: data.user.avatarUrl, + did: data.user.did + }, + lastMessageAt: new Date().toISOString(), + lastMessagePreview: 'New Conversation', + unreadCount: 0 + }; + setSelectedConversation(draftConv); + } setShowNewChat(false); setNewChatHandle(''); - await loadConversations(false); - // Select the new conversation (we might need to find it) - // For now just reload list. } catch (e: any) { console.error('[Chat UI] Start chat failed:', e); - if (e.message.includes('Recipient keys not found')) { + if (e.message.includes('Recipient keys not found') || e.message.includes('Failed to fetch recipient keys')) { alert('This user has not set up secure chat yet. They need to log in to enable end-to-end encryption.'); } else { alert('Failed to start chat: ' + e.message); } - } finally { - setSending(false); + } finally { + setSending(false); } }; @@ -410,6 +454,7 @@ export default function ChatPage() { } }; + const filteredConversations = conversations.filter((conv) => conv.participant2.displayName?.toLowerCase().includes(searchQuery.toLowerCase()) || conv.participant2.handle.toLowerCase().includes(searchQuery.toLowerCase()) @@ -417,16 +462,21 @@ export default function ChatPage() { if (user === null) return null; - // Locked State - if (isLocked) { + // Identity Locked State + if (!isIdentityUnlocked) { return ( -
- -

Chat Locked

-

- Your end-to-end encrypted identity is locked. Please unlock it to view your messages. +

+ +

Identity Locked

+

+ End-to-end encrypted chat requires your identity to be unlocked. Your private keys are needed to encrypt and decrypt messages.

-
@@ -440,24 +490,23 @@ export default function ChatPage() {

Connection Failed

- Unable to initialize secure chat. This might be a network issue or missing keys. + Unable to initialize secure chat. Please refresh the page to try again.

); } // Loading State - if ((!isReady && status !== 'error') || status === 'initializing' || status === 'generating_keys') { + if (!isReady || status === 'initializing') { return (
-

Initializing Secure Encrypted Chat...

); } @@ -673,7 +722,10 @@ export default function ChatPage() {
setSelectedConversation(conv)} + onClick={() => { + setMessages([]); + setSelectedConversation(conv); + }} style={{ cursor: 'pointer', display: 'flex', alignItems: 'flex-start', gap: '12px' }} >
diff --git a/src/db/schema.ts b/src/db/schema.ts index 863bff3..9134494 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -925,6 +925,7 @@ export const chatMessages = pgTable('chat_messages', { senderDisplayName: text('sender_display_name'), senderAvatarUrl: text('sender_avatar_url'), senderNodeDomain: text('sender_node_domain'), // null if local + senderDid: text('sender_did'), // DID for Signal Protocol // Message content (encrypted for recipient with their public key) encryptedContent: text('encrypted_content').notNull(), @@ -968,11 +969,17 @@ export const chatDeviceBundles = pgTable('chat_device_bundles', { // The device identifier (UUID generated by client) deviceId: text('device_id').notNull(), + // Signal Protocol fields + registrationId: integer('registration_id'), + // X25519 Identity Key (Base64) identityKey: text('identity_key').notNull(), // Signed PreKey (JSON: { id, key, sig }) signedPreKey: text('signed_pre_key').notNull(), + + // Kyber PreKey for post-quantum security (JSON: { id, key, sig }) + kyberPreKey: text('kyber_pre_key'), // One-Time Keys (JSON array of { id, key }) - cached/uploaded batch // Note: Individual keys are usually stored in a separate table for atomic consumption, diff --git a/src/lib/contexts/AuthContext.tsx b/src/lib/contexts/AuthContext.tsx index c639070..e6bb481 100644 --- a/src/lib/contexts/AuthContext.tsx +++ b/src/lib/contexts/AuthContext.tsx @@ -2,7 +2,6 @@ import { createContext, useContext, useEffect, useState } from 'react'; import { useUserIdentity } from '@/lib/hooks/useUserIdentity'; -import { useChatEncryption } from '@/lib/hooks/useChatEncryption'; export interface User { id: string; @@ -68,9 +67,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { } }; - // Integrate chat encryption hook - const { ensureReady } = useChatEncryption(); - const [showUnlockPrompt, setShowUnlockPrompt] = useState(false); /** @@ -83,15 +79,16 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { throw new Error('No encrypted private key available'); } - await unlockIdentityHook(targetUser.privateKeyEncrypted, password); - - // Initialize Chat Keys (Async, don't block UI but start it) - if (targetUser.id) { - ensureReady(password, targetUser.id).catch(err => { - console.error('Failed to initialize chat keys:', err); - }); - } + await unlockIdentityHook( + targetUser.privateKeyEncrypted, + password, + targetUser.did, + targetUser.handle, + targetUser.publicKey + ); + // Signal Protocol will auto-initialize when the chat page is opened + setShowUnlockPrompt(false); // Close prompt on success }; diff --git a/src/lib/crypto/chat-storage.ts b/src/lib/crypto/chat-storage.ts deleted file mode 100644 index bb49c4e..0000000 --- a/src/lib/crypto/chat-storage.ts +++ /dev/null @@ -1,236 +0,0 @@ - -/** - * Synapsis Secure Chat Storage - * - * Manages persistence of sensitive chat keys (X25519) and ratchet state. - * Uses IndexedDB, but all values are AES-GCM encrypted using a key derived - * from the user's login password. - */ - -import { hkdf, encrypt, decrypt, exportKey, importX25519PrivateKey, KeyPair, arrayBufferToBase64, base64ToArrayBuffer } from './e2ee'; - -const DB_NAME = 'SynapsisChat'; -const DB_VERSION = 1; -const STORE_NAME = 'secure_store'; - -// In-memory cache of the storage key (derived from password) -let storageKey: ArrayBuffer | null = null; -let dbInstance: IDBDatabase | null = null; - -// ---------------------------------------------------------------------------- -// 1. Initialization -// ---------------------------------------------------------------------------- - -/** - * Initialize storage with user password. - * Derives a dedicated storage key. - */ -export async function unlockChatStorage(password: string, userId: string): Promise { - // 1. Derive Storage Key - // We use the userId as salt to ensure uniqueness per user - const encoder = new TextEncoder(); - const masterKeyMaterial = encoder.encode(password); - const salt = encoder.encode(`synapsis_chat_storage_${userId}`); - - storageKey = await hkdf( - salt, - masterKeyMaterial, - encoder.encode('SynapsisChatPersistence'), - 32 // 256-bit AES key - ); - - // 2. Open DB - return new Promise((resolve, reject) => { - const request = indexedDB.open(DB_NAME, DB_VERSION); - - request.onupgradeneeded = (event) => { - const db = (event.target as IDBOpenDBRequest).result; - if (!db.objectStoreNames.contains(STORE_NAME)) { - db.createObjectStore(STORE_NAME); // Key-Value store - } - }; - - request.onsuccess = (event) => { - dbInstance = (event.target as IDBOpenDBRequest).result; - resolve(); - }; - - request.onerror = () => reject('Failed to open IndexedDB'); - }); -} - -export function isStorageUnlocked(): boolean { - return storageKey !== null && dbInstance !== null; -} - -export function lockStorage() { - storageKey = null; - if (dbInstance) { - dbInstance.close(); - dbInstance = null; - } -} - -// ---------------------------------------------------------------------------- -// 2. Safe Usage Wrappers -// ---------------------------------------------------------------------------- - -async function setItem(key: string, value: string): Promise { - if (!dbInstance) throw new Error('Database locked'); - - return new Promise((resolve, reject) => { - const tx = dbInstance!.transaction(STORE_NAME, 'readwrite'); - const store = tx.objectStore(STORE_NAME); - const req = store.put(value, key); - req.onsuccess = () => resolve(); - req.onerror = () => reject(req.error); - }); -} - -async function getItem(key: string): Promise { - if (!dbInstance) throw new Error('Database locked'); - return new Promise((resolve, reject) => { - const tx = dbInstance!.transaction(STORE_NAME, 'readonly'); - const store = tx.objectStore(STORE_NAME); - const req = store.get(key); - req.onsuccess = () => resolve(req.result); - req.onerror = () => reject(req.error); - }); -} - -async function deleteItem(key: string): Promise { - if (!dbInstance) throw new Error('Database locked'); - return new Promise((resolve, reject) => { - const tx = dbInstance!.transaction(STORE_NAME, 'readwrite'); - const store = tx.objectStore(STORE_NAME); - const req = store.delete(key); - req.onsuccess = () => resolve(); - req.onerror = () => reject(req.error); - }); -} - -// ---------------------------------------------------------------------------- -// 3. Encrypted Read/Write -// ---------------------------------------------------------------------------- - -/** - * Stores a serializable object encrypted. - */ -export async function storeEncrypted(key: string, data: any): Promise { - if (!storageKey) throw new Error('Storage locked'); - - const json = JSON.stringify(data); - const encrypted = await encrypt(storageKey, json); - - // Store as stringified JSON wrapper - const storedValue = JSON.stringify(encrypted); - await setItem(key, storedValue); -} - -/** - * Retrieves and decrypts an object. - */ -export async function loadEncrypted(key: string): Promise { - if (!storageKey) throw new Error('Storage locked'); - - const raw = await getItem(key); - if (!raw) return null; - - try { - const { ciphertext, iv } = JSON.parse(raw); - const json = await decrypt(storageKey, ciphertext, iv); - return JSON.parse(json) as T; - } catch (error) { - console.error(`Failed to decrypt key ${key}:`, error); - return null; - } -} - -/** - * Deletes an encrypted item from storage. - */ -export async function deleteEncrypted(key: string): Promise { - await deleteItem(key); -} - -/** - * Clears all session data (useful for recovery from corruption). - */ -export async function clearAllSessions(): Promise { - if (!dbInstance) throw new Error('Database locked'); - - return new Promise((resolve, reject) => { - const tx = dbInstance!.transaction(STORE_NAME, 'readwrite'); - const store = tx.objectStore(STORE_NAME); - const req = store.openCursor(); - - req.onsuccess = (event) => { - const cursor = (event.target as IDBRequest).result; - if (cursor) { - // Only delete session keys, not device keys - if (cursor.key.toString().startsWith('session:')) { - cursor.delete(); - } - cursor.continue(); - } else { - resolve(); - } - }; - - req.onerror = () => reject(req.error); - }); -} - -// ---------------------------------------------------------------------------- -// 4. Specific Key Managers -// ---------------------------------------------------------------------------- - -interface StoredKeyPair { - pub: string; // Base64 Raw - priv: string; // Base64 PKCS8 -} - -export async function storeDeviceKeys(identity: KeyPair, signedPreKey: KeyPair, otks: KeyPair[]) { - const data = { - identity: { - pub: await exportKey(identity.publicKey), - priv: await exportKey(identity.privateKey) - }, - signedPreKey: { - pub: await exportKey(signedPreKey.publicKey), - priv: await exportKey(signedPreKey.privateKey) - }, - otks: await Promise.all(otks.map(async k => ({ - pub: await exportKey(k.publicKey), - priv: await exportKey(k.privateKey) - }))) - }; - - await storeEncrypted('device_keys', data); -} - -export async function loadDeviceKeys(): Promise<{ identity: KeyPair, signedPreKey: KeyPair, otks: KeyPair[] } | null> { - const data = await loadEncrypted('device_keys'); - if (!data) return null; - - // Hydrate keys - const identity = { - publicKey: await importX25519PublicKey(data.identity.pub), - privateKey: await importX25519PrivateKey(data.identity.priv) - }; - - const signedPreKey = { - publicKey: await importX25519PublicKey(data.signedPreKey.pub), - privateKey: await importX25519PrivateKey(data.signedPreKey.priv) - }; - - const otks = await Promise.all((data.otks as any[]).map(async k => ({ - publicKey: await importX25519PublicKey(k.pub), - privateKey: await importX25519PrivateKey(k.priv) - }))); - - return { identity, signedPreKey, otks }; -} - -// Helper needed to avoid circular dep if importing from e2ee in hydrating -import { importX25519PublicKey } from './e2ee'; diff --git a/src/lib/crypto/e2e-chat.ts b/src/lib/crypto/e2e-chat.ts deleted file mode 100644 index bae5c49..0000000 --- a/src/lib/crypto/e2e-chat.ts +++ /dev/null @@ -1,104 +0,0 @@ -/** - * End-to-End Encrypted Chat Cryptography - * - * Uses ECDH (Elliptic Curve Diffie-Hellman) for key exchange - * and AES-GCM for message encryption. - * - * This is a simplified version of the Signal Protocol approach. - * Private keys NEVER leave the client. - */ - -import * as crypto from 'crypto'; - -/** - * Generate an ECDH key pair for chat encryption - * The private key should be stored client-side only - */ -export function generateChatKeyPair(): { publicKey: string; privateKey: string } { - const ecdh = crypto.createECDH('prime256v1'); - ecdh.generateKeys(); - - return { - publicKey: ecdh.getPublicKey('base64'), - privateKey: ecdh.getPrivateKey('base64'), - }; -} - -/** - * Derive a shared secret from your private key and their public key - * This is the magic of ECDH - both parties derive the same secret - */ -export function deriveSharedSecret(myPrivateKey: string, theirPublicKey: string): Buffer { - const ecdh = crypto.createECDH('prime256v1'); - ecdh.setPrivateKey(Buffer.from(myPrivateKey, 'base64')); - - const sharedSecret = ecdh.computeSecret(Buffer.from(theirPublicKey, 'base64')); - - // Derive a proper AES key from the shared secret using HKDF - return crypto.createHash('sha256').update(sharedSecret).digest(); -} - -/** - * Encrypt a message using the shared secret - */ -export function encryptWithSharedSecret(message: string, sharedSecret: Buffer): string { - const iv = crypto.randomBytes(12); - const cipher = crypto.createCipheriv('aes-256-gcm', sharedSecret, iv); - - const encrypted = Buffer.concat([ - cipher.update(message, 'utf8'), - cipher.final() - ]); - const authTag = cipher.getAuthTag(); - - // Combine: iv (12) + authTag (16) + ciphertext - const combined = Buffer.concat([iv, authTag, encrypted]); - return combined.toString('base64'); -} - -/** - * Decrypt a message using the shared secret - */ -export function decryptWithSharedSecret(encryptedMessage: string, sharedSecret: Buffer): string { - const combined = Buffer.from(encryptedMessage, 'base64'); - - const iv = combined.subarray(0, 12); - const authTag = combined.subarray(12, 28); - const ciphertext = combined.subarray(28); - - const decipher = crypto.createDecipheriv('aes-256-gcm', sharedSecret, iv); - decipher.setAuthTag(authTag); - - const decrypted = Buffer.concat([ - decipher.update(ciphertext), - decipher.final() - ]); - - return decrypted.toString('utf8'); -} - -/** - * High-level: Encrypt a message for a recipient - * Uses sender's private key + recipient's public key - */ -export function encryptMessage( - message: string, - senderPrivateKey: string, - recipientPublicKey: string -): string { - const sharedSecret = deriveSharedSecret(senderPrivateKey, recipientPublicKey); - return encryptWithSharedSecret(message, sharedSecret); -} - -/** - * High-level: Decrypt a message from a sender - * Uses recipient's private key + sender's public key - */ -export function decryptMessage( - encryptedMessage: string, - recipientPrivateKey: string, - senderPublicKey: string -): string { - const sharedSecret = deriveSharedSecret(recipientPrivateKey, senderPublicKey); - return decryptWithSharedSecret(encryptedMessage, sharedSecret); -} diff --git a/src/lib/crypto/e2ee.ts b/src/lib/crypto/e2ee.ts deleted file mode 100644 index ada3a2e..0000000 --- a/src/lib/crypto/e2ee.ts +++ /dev/null @@ -1,247 +0,0 @@ - -/** - * Synapsis E2EE Cryptography Core - * - * Implements: - * - X25519 for Key Agreement (ECDH) - * - AES-GCM-256 for Encryption (Standard WebCrypto replacement for ChaCha20) - * - HKDF-SHA256 for Key Derivation - * - * Note: Uses WebCrypto API available in Node 19+ and Browsers. - */ - -// Universal Crypto Access -const cryptoSubtle = typeof window !== 'undefined' - ? window.crypto.subtle - : (globalThis.crypto as any)?.subtle || require('crypto').webcrypto?.subtle; - -if (!cryptoSubtle) { - throw new Error('WebCrypto is not available in this environment'); -} - -// Types -export interface KeyPair { - publicKey: CryptoKey; - privateKey: CryptoKey; -} - -export interface PreKeyBundle { - id: number; - key: CryptoKey; - signature?: string; // Base64 ECDSA signature if it's a signed prekey -} - -// ---------------------------------------------------------------------------- -// 1. Primitives -// ---------------------------------------------------------------------------- - -/** - * Generate an X25519 Key Pair - */ -export async function generateX25519KeyPair(): Promise { - return await cryptoSubtle.generateKey( - { - name: 'X25519', - }, - true, // extractable - ['deriveKey', 'deriveBits'] - ) as KeyPair; -} - -/** - * Import an X25519 Public Key from Base64 Raw Bytes (32 bytes) - */ -export async function importX25519PublicKey(base64: string): Promise { - const binary = base64ToArrayBuffer(base64); - return await cryptoSubtle.importKey( - 'raw', - binary, - { name: 'X25519' }, - true, - [] - ); -} - -/** - * Import an X25519 Private Key from Base64 PKCS8/Raw - * Note: WebCrypto usually exports Private Keys as PKCS8. - */ -export async function importX25519PrivateKey(base64: string): Promise { - const binary = base64ToArrayBuffer(base64); - // Try PKCS8 first (standard export) - return await cryptoSubtle.importKey( - 'pkcs8', - binary, - { name: 'X25519' }, - true, // Must be extractable for serialization - ['deriveKey', 'deriveBits'] - ); -} - -/** - * Export Key to Base64 (Raw for Public, PKCS8 for Private) - */ -export async function exportKey(key: CryptoKey): Promise { - if (key.type === 'public') { - const raw = await cryptoSubtle.exportKey('raw', key); - return arrayBufferToBase64(raw); - } else { - const pkcs8 = await cryptoSubtle.exportKey('pkcs8', key); - return arrayBufferToBase64(pkcs8); - } -} - -/** - * ECDH: Compute Shared Secret - */ -export async function computeSharedSecret(privateKey: CryptoKey, publicKey: CryptoKey): Promise { - // We derive bits directly (Commonly 32 bytes for X25519) - return await cryptoSubtle.deriveBits( - { - name: 'X25519', - public: publicKey, - }, - privateKey, - 256 // 32 bytes * 8 - ); -} - -// ---------------------------------------------------------------------------- -// 2. KDF (HKDF-SHA256) -// ---------------------------------------------------------------------------- - -/** - * HKDF Expand & Extract - */ -export async function hkdf( - salt: ArrayBuffer | Uint8Array, - ikm: ArrayBuffer | Uint8Array, // Input Key Material (Shared Secret) - info: ArrayBuffer | Uint8Array, - length: number // Bytes output -): Promise { - const key = await cryptoSubtle.importKey( - 'raw', - ikm, - { name: 'HKDF' }, - false, - ['deriveBits'] - ); - - return await cryptoSubtle.deriveBits( - { - name: 'HKDF', - hash: 'SHA-256', - salt: salt, - info: info, - }, - key, - length * 8 - ); -} - -// ---------------------------------------------------------------------------- -// 3. Encryption (AES-GCM) -// ---------------------------------------------------------------------------- - -export async function encrypt( - keyBytes: ArrayBuffer, - plaintext: string | Uint8Array, - associatedData?: Uint8Array -): Promise<{ ciphertext: string; iv: string }> { - const iv = crypto.getRandomValues(new Uint8Array(12)); // 96-bit IV - - const key = await cryptoSubtle.importKey( - 'raw', - keyBytes, - { name: 'AES-GCM' }, - false, - ['encrypt'] - ); - - const data = typeof plaintext === 'string' - ? new TextEncoder().encode(plaintext) - : plaintext; - - const algorithm: any = { - name: 'AES-GCM', - iv: iv - }; - - if (associatedData) { - algorithm.additionalData = associatedData; - } - - const encrypted = await cryptoSubtle.encrypt( - algorithm, - key, - data - ); - - return { - ciphertext: arrayBufferToBase64(encrypted), - iv: arrayBufferToBase64(iv.buffer) - }; -} - -export async function decrypt( - keyBytes: ArrayBuffer, - ciphertextBase64: string, - ivBase64: string, - associatedData?: Uint8Array -): Promise { - const key = await cryptoSubtle.importKey( - 'raw', - keyBytes, - { name: 'AES-GCM' }, - false, - ['decrypt'] - ); - - const ciphertext = base64ToArrayBuffer(ciphertextBase64); - const iv = base64ToArrayBuffer(ivBase64); - - try { - const algorithm: any = { - name: 'AES-GCM', - iv: iv - }; - if (associatedData) { - algorithm.additionalData = associatedData; - } - - const decrypted = await cryptoSubtle.decrypt( - algorithm, - key, - ciphertext - ); - return new TextDecoder().decode(decrypted); - } catch (e) { - throw new Error('Decryption failed'); - } -} - -// ---------------------------------------------------------------------------- -// 4. Utils -// ---------------------------------------------------------------------------- - -export function arrayBufferToBase64(buffer: ArrayBuffer): string { - const bytes = new Uint8Array(buffer); - let binary = ''; - for (let i = 0; i < bytes.byteLength; i++) { - binary += String.fromCharCode(bytes[i]); - } - return btoa(binary); -} - -export function base64ToArrayBuffer(base64: string): ArrayBuffer { - if (!base64 || typeof base64 !== 'string') { - throw new Error('Invalid base64 input: expected non-empty string'); - } - // Handle URL safe base64 if needed, but we assume standard - const binary = atob(base64.replace(/-/g, '+').replace(/_/g, '/')); - const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i++) { - bytes[i] = binary.charCodeAt(i); - } - return bytes.buffer; -} diff --git a/src/lib/crypto/message-cache.ts b/src/lib/crypto/message-cache.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/crypto/ratchet.ts b/src/lib/crypto/ratchet.ts deleted file mode 100644 index 4cee4d1..0000000 --- a/src/lib/crypto/ratchet.ts +++ /dev/null @@ -1,390 +0,0 @@ - -/** - * Synapsis Double Ratchet & X3DH Implementation - * - * Implements the Double Ratchet Algorithm + X3DH Key Agreement. - * Adheres to Signal specifications using the "SynapsisV2" HKDF info binding. - */ - -import { - KeyPair, - computeSharedSecret, - hkdf, - encrypt as aeadEncrypt, - decrypt as aeadDecrypt, - importX25519PublicKey, - importX25519PrivateKey, - exportKey, - generateX25519KeyPair, - base64ToArrayBuffer, - arrayBufferToBase64 -} from './e2ee'; - -// Constants -const KDF_INFO = 'SynapsisV2'; -const RK_SIZE = 32; // 32 bytes for Root Key -const CK_SIZE = 32; // 32 bytes for Chain Key -const MK_SIZE = 32; // 32 bytes for Message Key - -export interface RatchetState { - // DH Ratchet - dhPair: KeyPair; - remoteDhPub: CryptoKey; - rootKey: ArrayBuffer; - - // Symm Ratchets - chainKeySend: ArrayBuffer; - chainKeyRecv: ArrayBuffer; - - // Message Numbers - ns: number; // Send count - nr: number; // Recv count - pn: number; // Previous chain count -} - -export interface Header { - dh: string; // Base64 public key - pn: number; - n: number; -} - -export interface CiphertextMessage { - header: Header; - ciphertext: string; - iv: string; -} - -// ---------------------------------------------------------------------------- -// 1. X3DH Key Agreement -// ---------------------------------------------------------------------------- - -export async function x3dhSender( - aliceIdentity: KeyPair, - bobBundle: { - identityKey: CryptoKey, - signedPreKey: CryptoKey, - oneTimeKey?: CryptoKey - }, - contextInfo: string // "SynapsisV2" + DIDs + DeviceIDs -): Promise<{ sk: ArrayBuffer, ephemeralKey: KeyPair }> { - - // 1. Generate Ephemeral Key (EK_a) - const ephemeralKey = await generateX25519KeyPair(); - - // 2. DH1 = DH(IK_a, SPK_b) - const dh1 = await computeSharedSecret(aliceIdentity.privateKey, bobBundle.signedPreKey); - - // 3. DH2 = DH(EK_a, IK_b) - const dh2 = await computeSharedSecret(ephemeralKey.privateKey, bobBundle.identityKey); - - // 4. DH3 = DH(EK_a, SPK_b) - const dh3 = await computeSharedSecret(ephemeralKey.privateKey, bobBundle.signedPreKey); - - // 5. DH4 = DH(EK_a, OPK_b) -- Optional - let dh4: ArrayBuffer | undefined; - if (bobBundle.oneTimeKey) { - dh4 = await computeSharedSecret(ephemeralKey.privateKey, bobBundle.oneTimeKey); - } - - // 6. Concatenate - const km = new Uint8Array(dh1.byteLength + dh2.byteLength + dh3.byteLength + (dh4 ? dh4.byteLength : 0)); - let offset = 0; - km.set(new Uint8Array(dh1), offset); offset += dh1.byteLength; - km.set(new Uint8Array(dh2), offset); offset += dh2.byteLength; - km.set(new Uint8Array(dh3), offset); offset += dh3.byteLength; - if (dh4) km.set(new Uint8Array(dh4), offset); - - // 7. KDF - // Output 32 bytes for Root Key - const encoder = new TextEncoder(); - return { - sk: await hkdf(new Uint8Array(32), km.buffer, encoder.encode(contextInfo), 32), - ephemeralKey - }; -} - -export async function x3dhReceiver( - bobIdentity: KeyPair, - bobSignedPreKey: KeyPair, - bobOneTimeKey: KeyPair | undefined, // The one used by Alice - aliceIdentityKey: CryptoKey, - aliceEphemeralKey: CryptoKey, - contextInfo: string -): Promise { - - // 1. DH1 = DH(SPK_b, IK_a) -- Note: Order of keys in computeSharedSecret usually (private, public) - const dh1 = await computeSharedSecret(bobSignedPreKey.privateKey, aliceIdentityKey); - - // 2. DH2 = DH(IK_b, EK_a) - const dh2 = await computeSharedSecret(bobIdentity.privateKey, aliceEphemeralKey); - - // 3. DH3 = DH(SPK_b, EK_a) - const dh3 = await computeSharedSecret(bobSignedPreKey.privateKey, aliceEphemeralKey); - - // 4. DH4 = DH(OPK_b, EK_a) - let dh4: ArrayBuffer | undefined; - if (bobOneTimeKey) { - dh4 = await computeSharedSecret(bobOneTimeKey.privateKey, aliceEphemeralKey); - } - - const km = new Uint8Array(dh1.byteLength + dh2.byteLength + dh3.byteLength + (dh4 ? dh4.byteLength : 0)); - let offset = 0; - km.set(new Uint8Array(dh1), offset); offset += dh1.byteLength; - km.set(new Uint8Array(dh2), offset); offset += dh2.byteLength; - km.set(new Uint8Array(dh3), offset); offset += dh3.byteLength; - if (dh4) km.set(new Uint8Array(dh4), offset); - - const encoder = new TextEncoder(); - return await hkdf(new Uint8Array(32), km.buffer, encoder.encode(contextInfo), 32); -} - -// ---------------------------------------------------------------------------- -// 2. KDF Chains (Symmetric Ratchet) -// ---------------------------------------------------------------------------- - -// Constants for HMAC -const ONE = new Uint8Array([0x01]); -const TWO = new Uint8Array([0x02]); - -async function kdfChain(ck: ArrayBuffer): Promise<{ ck: ArrayBuffer, mk: ArrayBuffer }> { - // HMAC-SHA256(CK, 1) -> MK - // HMAC-SHA256(CK, 2) -> NextCK - // Implementing via HKDF for simplicity/consistency or WebCrypto HMAC - - // Actually standard says: - // HMAC-SHA256(ck, input) - // We can use HKDF-Expand logic here or pure hmac. - // Let's use custom HKDF expand with fixed info - const mk = await hkdf(new Uint8Array(0), ck, ONE, 32); - const nextCk = await hkdf(new Uint8Array(0), ck, TWO, 32); - - return { ck: nextCk, mk }; -} - -// ---------------------------------------------------------------------------- -// 3. DHRatchet (Root Chain) -// ---------------------------------------------------------------------------- - -async function kdfRoot(rootKey: ArrayBuffer, dhOut: ArrayBuffer): Promise<{ rootKey: ArrayBuffer, chainKey: ArrayBuffer }> { - // HKDF(root, dh, info, 64) -> 32 root, 32 chain - const encoder = new TextEncoder(); - const output = await hkdf( - rootKey, - dhOut, - encoder.encode("SynapsisRatchet"), - 64 - ); - - const bytes = new Uint8Array(output); - return { - rootKey: bytes.slice(0, 32).buffer, - chainKey: bytes.slice(32, 64).buffer - }; -} - -// ---------------------------------------------------------------------------- -// 4. Initializers -// ---------------------------------------------------------------------------- - -export async function initSender( - sharedSecret: ArrayBuffer, - bobRatchetKey: CryptoKey -): Promise { - const dhPair = await generateX25519KeyPair(); - - // Sender starts by sending a new DH ratchet. - // Root Key = sharedSecret. - // First, we need to generate a chain key for sending. - // Standard: Alice initializes with SK. Bob's ratchet key is remote. - // Alice generates `dhPair`. - // She performs a DH ratchet Step immediately? - // Protocol: - // Alice: RK = SK. - // Alice performs DH(alice_priv, bob_pub). - // Calculates RK, CK_send. - - const dhOut = await computeSharedSecret(dhPair.privateKey, bobRatchetKey); - const kdf = await kdfRoot(sharedSecret, dhOut); - - return { - dhPair, - remoteDhPub: bobRatchetKey, - rootKey: kdf.rootKey, - chainKeySend: kdf.chainKey, - chainKeyRecv: new Uint8Array(0).buffer, // Empty until Bob replies - ns: 0, - nr: 0, - pn: 0 - }; -} - -export async function initReceiver( - sharedSecret: ArrayBuffer, - bobDhKeyPair: KeyPair // This is the SPK key pair used in X3DH -): Promise { - // Bob: RK = SK. - // Bob has consistent state. - return { - dhPair: bobDhKeyPair, - remoteDhPub: bobDhKeyPair.publicKey, // Placeholder, will be updated on first msg - rootKey: sharedSecret, - chainKeySend: new Uint8Array(0).buffer, - chainKeyRecv: new Uint8Array(0).buffer, // Will be derived on first msg - ns: 0, - nr: 0, - pn: 0 - }; -} - -// ---------------------------------------------------------------------------- -// 5. Encrypt / Decrypt Message -// ---------------------------------------------------------------------------- - -export async function ratchetEncrypt( - state: RatchetState, - plaintext: string -): Promise<{ - ciphertext: CiphertextMessage, - newState: RatchetState -}> { - // 1. Advance chain - const { ck: nextCk, mk } = await kdfChain(state.chainKeySend); - state.chainKeySend = nextCk; - - // 2. Encrypt - const header: Header = { - dh: await exportKey(state.dhPair.publicKey), - pn: state.pn, - n: state.ns - }; - - const associatedData = new TextEncoder().encode(JSON.stringify(header)); - const encrypted = await aeadEncrypt(mk, plaintext, associatedData); - - state.ns += 1; - - return { - ciphertext: { - header, - ciphertext: encrypted.ciphertext, - iv: encrypted.iv - }, - newState: state - }; -} - -// Note: Decryption requires handling out-of-order messages and ratcheting steps. -// This is complex logic. For V2.1 baseline, we implement the core ratcheting step if header key differs. - -export async function ratchetDecrypt( - state: RatchetState, - message: CiphertextMessage -): Promise<{ plaintext: string, newState: RatchetState }> { - // Check if DH ratchet step needed - // If message.header.dh != state.remoteDhPub - - // Note: Comparing CryptoKeys directly is hard. We compare Base64 export. - const remoteKeyStr = await exportKey(state.remoteDhPub); - - if (message.header.dh !== remoteKeyStr) { - // Ratchet Step! - const newRemoteKey = await importX25519PublicKey(message.header.dh); - - // 1. DHRatchet(remote_new) -> RX step - const dhOut1 = await computeSharedSecret(state.dhPair.privateKey, newRemoteKey); - const kdf1 = await kdfRoot(state.rootKey, dhOut1); - state.rootKey = kdf1.rootKey; - state.chainKeyRecv = kdf1.chainKey; - - // 2. Sender step (We generate new key) - state.pn = state.ns; - state.ns = 0; - state.nr = 0; - state.dhPair = await generateX25519KeyPair(); - - // 3. DHRatchet(remote_new) -> TX step - const dhOut2 = await computeSharedSecret(state.dhPair.privateKey, newRemoteKey); - const kdf2 = await kdfRoot(state.rootKey, dhOut2); - state.rootKey = kdf2.rootKey; - state.chainKeySend = kdf2.chainKey; - - state.remoteDhPub = newRemoteKey; - } - - // 3. Symmetric Ratchet to catch up to n - // (Skipping skipped-message buffering for now - assumes ordered delivery for V2.1 baseline) - - // Advance Chain Recv to n - // Real impl buffers skipped keys. - // Warning: If n > nr, we must loop. - // For now, assuming direct sequence. - - const { ck: nextCk, mk } = await kdfChain(state.chainKeyRecv); - state.chainKeyRecv = nextCk; - state.nr += 1; - - // 4. Decrypt - const associatedData = new TextEncoder().encode(JSON.stringify(message.header)); - const plaintext = await aeadDecrypt(mk, message.ciphertext, message.iv, associatedData); - - return { plaintext, newState: state }; -} - -// ---------------------------------------------------------------------------- -// 6. Serialization Helpers (CRITICAL: CryptoKeys and Buffers don't JSON stringify) -// ---------------------------------------------------------------------------- - -export interface SerializedRatchetState { - dhPair: { pub: string, priv: string }; - remoteDhPub: string; - rootKey: string; - chainKeySend: string; - chainKeyRecv: string; - ns: number; - nr: number; - pn: number; -} - -export async function serializeRatchetState(state: RatchetState): Promise { - return { - dhPair: { - pub: await exportKey(state.dhPair.publicKey), - priv: await exportKey(state.dhPair.privateKey) - }, - remoteDhPub: await exportKey(state.remoteDhPub), - rootKey: arrayBufferToBase64(state.rootKey), - chainKeySend: arrayBufferToBase64(state.chainKeySend), - chainKeyRecv: arrayBufferToBase64(state.chainKeyRecv), - ns: state.ns, - nr: state.nr, - pn: state.pn - }; -} - -export async function deserializeRatchetState(data: SerializedRatchetState): Promise { - // Validate integrity - check all required fields exist (but allow empty strings for buffers that can be empty) - if (!data || - !data.rootKey || - !data.dhPair || - !data.dhPair.pub || - !data.dhPair.priv || - !data.remoteDhPub || - data.chainKeySend === undefined || data.chainKeySend === null || - data.chainKeyRecv === undefined || data.chainKeyRecv === null) { - throw new Error('Invalid serialized state: missing required fields'); - } - - return { - dhPair: { - publicKey: await importX25519PublicKey(data.dhPair.pub), - privateKey: await importX25519PrivateKey(data.dhPair.priv) - }, - remoteDhPub: await importX25519PublicKey(data.remoteDhPub), - rootKey: base64ToArrayBuffer(data.rootKey), - chainKeySend: data.chainKeySend ? base64ToArrayBuffer(data.chainKeySend) : new Uint8Array(0).buffer, - chainKeyRecv: data.chainKeyRecv ? base64ToArrayBuffer(data.chainKeyRecv) : new Uint8Array(0).buffer, - ns: data.ns, - nr: data.nr, - pn: data.pn - }; -} diff --git a/src/lib/crypto/sodium-chat.ts b/src/lib/crypto/sodium-chat.ts new file mode 100644 index 0000000..df84672 --- /dev/null +++ b/src/lib/crypto/sodium-chat.ts @@ -0,0 +1,228 @@ +/** + * Libsodium E2EE Chat Implementation + * Keys stored encrypted in IndexedDB using storage key from identity unlock + */ + +import sodium from 'libsodium-wrappers-sumo'; + +let sodiumReady = false; + +const DB_NAME = 'synapsis_chat'; +const DB_VERSION = 1; +const STORE_NAME = 'chat_keys'; + +/** + * Initialize libsodium (must be called before any crypto operations) + */ +export async function initSodium() { + if (sodiumReady) return; + await sodium.ready; + sodiumReady = true; +} + +/** + * Open IndexedDB + */ +function openDB(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result); + + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME); + } + }; + }); +} + +/** + * Generate a new key pair for chat encryption + */ +export async function generateChatKeyPair(): Promise<{ + publicKey: string; // base64 + privateKey: string; // base64 +}> { + await initSodium(); + + const keyPair = sodium.crypto_box_keypair(); + + return { + publicKey: sodium.to_base64(keyPair.publicKey), + privateKey: sodium.to_base64(keyPair.privateKey), + }; +} + +/** + * Encrypt a message for a recipient + */ +export async function encryptMessage( + message: string, + recipientPublicKey: string, // base64 + senderPrivateKey: string // base64 +): Promise<{ + ciphertext: string; // base64 + nonce: string; // base64 +}> { + await initSodium(); + + const messageBytes = sodium.from_string(message); + const recipientPubKey = sodium.from_base64(recipientPublicKey); + const senderPrivKey = sodium.from_base64(senderPrivateKey); + + // Generate random nonce + const nonce = sodium.randombytes_buf(sodium.crypto_box_NONCEBYTES); + + // Encrypt + const ciphertext = sodium.crypto_box_easy( + messageBytes, + nonce, + recipientPubKey, + senderPrivKey + ); + + return { + ciphertext: sodium.to_base64(ciphertext), + nonce: sodium.to_base64(nonce), + }; +} + +/** + * Decrypt a message from a sender + */ +export async function decryptMessage( + ciphertext: string, // base64 + nonce: string, // base64 + senderPublicKey: string, // base64 + recipientPrivateKey: string // base64 +): Promise { + await initSodium(); + + const ciphertextBytes = sodium.from_base64(ciphertext); + const nonceBytes = sodium.from_base64(nonce); + const senderPubKey = sodium.from_base64(senderPublicKey); + const recipientPrivKey = sodium.from_base64(recipientPrivateKey); + + // Decrypt + const decrypted = sodium.crypto_box_open_easy( + ciphertextBytes, + nonceBytes, + senderPubKey, + recipientPrivKey + ); + + return sodium.to_string(decrypted); +} + +/** + * Store keys in IndexedDB (encrypted with storage key from memory) + */ +export async function storeKeys( + userId: string, + publicKey: string, + privateKey: string, + storageKey: Uint8Array +): Promise { + await initSodium(); + + // Generate random nonce + const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES); + + // Encrypt private key with storage key + const privateKeyBytes = sodium.from_string(privateKey); + const ciphertext = sodium.crypto_secretbox_easy(privateKeyBytes, nonce, storageKey); + + // Combine nonce + ciphertext + const combined = new Uint8Array(nonce.length + ciphertext.length); + combined.set(nonce, 0); + combined.set(ciphertext, nonce.length); + + // Store in IndexedDB + const db = await openDB(); + const tx = db.transaction(STORE_NAME, 'readwrite'); + const store = tx.objectStore(STORE_NAME); + + await new Promise((resolve, reject) => { + const request = store.put({ + publicKey, + encryptedPrivateKey: sodium.to_base64(combined) + }, userId); + + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + + db.close(); +} + +/** + * Retrieve keys from IndexedDB (decrypt with storage key from memory) + */ +export async function getStoredKeys( + userId: string, + storageKey: Uint8Array +): Promise<{ publicKey: string; privateKey: string } | null> { + await initSodium(); + + try { + const db = await openDB(); + const tx = db.transaction(STORE_NAME, 'readonly'); + const store = tx.objectStore(STORE_NAME); + + const data = await new Promise((resolve, reject) => { + const request = store.get(userId); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + + db.close(); + + if (!data) { + console.log('[Sodium] No stored keys found in IndexedDB for user:', userId); + return null; + } + + console.log('[Sodium] Found stored keys in IndexedDB, attempting to decrypt...'); + + const { publicKey, encryptedPrivateKey } = data; + + // Extract nonce and ciphertext + const combined = sodium.from_base64(encryptedPrivateKey); + const nonce = combined.slice(0, sodium.crypto_secretbox_NONCEBYTES); + const ciphertext = combined.slice(sodium.crypto_secretbox_NONCEBYTES); + + // Decrypt with storage key + const decrypted = sodium.crypto_secretbox_open_easy(ciphertext, nonce, storageKey); + const privateKey = sodium.to_string(decrypted); + + console.log('[Sodium] Successfully decrypted stored keys'); + return { publicKey, privateKey }; + } catch (error) { + console.error('[Sodium] Failed to decrypt stored keys - storage key mismatch?', error); + return null; + } +} + +/** + * Clear stored keys from IndexedDB + */ +export async function clearStoredKeys(userId: string): Promise { + try { + const db = await openDB(); + const tx = db.transaction(STORE_NAME, 'readwrite'); + const store = tx.objectStore(STORE_NAME); + + await new Promise((resolve, reject) => { + const request = store.delete(userId); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + + db.close(); + } catch (error) { + console.error('[Sodium] Failed to clear keys:', error); + } +} diff --git a/src/lib/crypto/user-signing.ts b/src/lib/crypto/user-signing.ts index b2ea9b0..67f70fe 100644 --- a/src/lib/crypto/user-signing.ts +++ b/src/lib/crypto/user-signing.ts @@ -27,6 +27,7 @@ class InMemoryKeyStore implements KeyStore { private static instance: InMemoryKeyStore; private privateKey: CryptoKey | null = null; private identity: { did: string; handle: string; publicKey: string } | null = null; + private storageKey: Uint8Array | null = null; // For encrypting chat keys private constructor() { } @@ -56,9 +57,18 @@ class InMemoryKeyStore implements KeyStore { return this.identity; } + setStorageKey(key: Uint8Array): void { + this.storageKey = key; + } + + getStorageKey(): Uint8Array | null { + return this.storageKey; + } + clear(): void { this.privateKey = null; this.identity = null; + this.storageKey = null; } } diff --git a/src/lib/hooks/useChatEncryption.ts b/src/lib/hooks/useChatEncryption.ts deleted file mode 100644 index 156f64b..0000000 --- a/src/lib/hooks/useChatEncryption.ts +++ /dev/null @@ -1,634 +0,0 @@ - -'use client'; - -import { useState, useCallback, useRef, useEffect } from 'react'; -import { - unlockChatStorage, - loadDeviceKeys, - storeDeviceKeys, - isStorageUnlocked, - storeEncrypted, - loadEncrypted, - deleteEncrypted, - clearAllSessions -} from '@/lib/crypto/chat-storage'; -import { - generateX25519KeyPair, - generateX25519KeyPair as generatePreKey, - exportKey, - importX25519PublicKey, - importX25519PrivateKey // Needed? -} from '@/lib/crypto/e2ee'; -import { - initSender, - initReceiver, - ratchetEncrypt, - ratchetDecrypt, - x3dhSender, - x3dhReceiver, - RatchetState, - serializeRatchetState, - deserializeRatchetState -} from '@/lib/crypto/ratchet'; -import { useUserIdentity } from './useUserIdentity'; -import { v4 as uuidv4 } from 'uuid'; - -// Helper to check signature (we trust server for V2.1 baseline usually, but client check is better) -// import { verifyUserAction } from '...'; // Client side verification lib? - -// Helper to encrypt a copy of message for sender (so they can read on any device) -async function encryptForSelf(plaintext: string, identityPublicKey: CryptoKey): Promise<{ ciphertext: string; iv: string }> { - const encoder = new TextEncoder(); - const data = encoder.encode(plaintext); - - // Generate a random IV - const iv = crypto.getRandomValues(new Uint8Array(12)); - - // Export public key raw to use as key material - const keyMaterial = await crypto.subtle.exportKey('raw', identityPublicKey); - - // Derive AES key using HKDF-like approach (simplified: hash the key material) - const aesKey = await crypto.subtle.digest('SHA-256', keyMaterial); - - // Import as AES-GCM key - const cryptoKey = await crypto.subtle.importKey( - 'raw', - aesKey, - { name: 'AES-GCM', length: 256 }, - false, - ['encrypt'] - ); - - const encrypted = await crypto.subtle.encrypt( - { name: 'AES-GCM', iv }, - cryptoKey, - data - ); - - return { - ciphertext: btoa(String.fromCharCode(...new Uint8Array(encrypted))), - iv: btoa(String.fromCharCode(...iv)) - }; -} - -// Helper to decrypt self-encrypted message -async function decryptForSelf(selfEncrypted: { ciphertext: string; iv: string }, identityKeyPair: CryptoKeyPair): Promise { - // Export public key raw to use as key material (same as encryption) - const keyMaterial = await crypto.subtle.exportKey('raw', identityKeyPair.publicKey); - - // Derive AES key - const aesKey = await crypto.subtle.digest('SHA-256', keyMaterial); - - // Import as AES-GCM key - const cryptoKey = await crypto.subtle.importKey( - 'raw', - aesKey, - { name: 'AES-GCM', length: 256 }, - false, - ['decrypt'] - ); - - // Decode IV and ciphertext - const iv = Uint8Array.from(atob(selfEncrypted.iv), c => c.charCodeAt(0)); - const ciphertext = Uint8Array.from(atob(selfEncrypted.ciphertext), c => c.charCodeAt(0)); - - const decrypted = await crypto.subtle.decrypt( - { name: 'AES-GCM', iv }, - cryptoKey, - ciphertext - ); - - const decoder = new TextDecoder(); - return decoder.decode(decrypted); -} - -export function useChatEncryption() { - const { signUserAction, identity } = useUserIdentity(); - const [isReady, setIsReady] = useState(false); - const [status, setStatus] = useState('idle'); - - // Session Cache (In-Memory) - const sessionsRef = useRef>(new Map()); - - const [isLocked, setIsLocked] = useState(false); - - // Auto-detect if storage is unlocked (by AuthContext) - useEffect(() => { - if (isReady) { - setIsLocked(false); - return; - } - - const check = async () => { - const unlocked = isStorageUnlocked(); - setIsLocked(!unlocked); - - if (unlocked) { - // One-time cleanup of corrupted sessions (migration fix) - if (!sessionStorage.getItem('synapsis_sessions_cleaned_v2')) { - try { - console.log('[Chat] Running one-time session cleanup...'); - await clearAllSessions(); - sessionStorage.setItem('synapsis_sessions_cleaned_v2', 'true'); - console.log('[Chat] Session cleanup complete'); - } catch (e) { - console.error('[Chat] Session cleanup failed:', e); - } - } - - try { - const keys = await loadDeviceKeys(); - if (keys) { - // Keys exist locally. - setIsReady(true); - setStatus('ready'); - - // CRITICAL REPAIR: - // Just because we have keys doesn't mean the server does. - // We run a non-blocking check to ensure we aren't a "Zombie". - // We only do this check if we haven't verified it this session yet. - if (identity?.did && !sessionStorage.getItem('synapsis_keys_verified')) { - fetch(`/.well-known/synapsis/chat/${identity.did}`).then(async (res) => { - if (res.status === 404) { - console.warn('[Chat] Zombie State Detected during init. Republishing keys...'); - // Extract publish logic (duplicated for now to ensure safety without massive refactor) - try { - const deviceId = localStorage.getItem('synapsis_device_id') || uuidv4(); - const bundlePayload = { - deviceId, - identityKey: await exportKey(keys.identity.publicKey), - signedPreKey: { id: 1, key: await exportKey(keys.signedPreKey.publicKey) }, - oneTimeKeys: await Promise.all(keys.otks.map(async (k: any, i: number) => ({ id: 100 + i, key: await exportKey(k.publicKey) }))) - }; - const signedAction = await signUserAction('chat.keys.publish', bundlePayload); - await fetch('/api/chat/keys', { method: 'POST', body: JSON.stringify(signedAction) }); - console.log('[Chat] Self-repair successful.'); - sessionStorage.setItem('synapsis_keys_verified', 'true'); - } catch (e) { - console.error('[Chat] Self-repair failed:', e); - } - } else if (res.ok) { - sessionStorage.setItem('synapsis_keys_verified', 'true'); - } - }).catch(e => console.error('Verification check failed', e)); - } - - } else if (status === 'idle' && identity?.did) { - // Keys missing but storage unlocked (and we have identity). - // Attempt to generate/restore keys. - console.log('[Chat] Storage unlocked but keys missing. Attempting generation...'); - ensureReady('ALREADY_UNLOCKED', 'placeholder-user-id').catch(err => { - console.error('[Chat] Auto-generation failed:', err); - }); - } - } catch (e) { - console.error("Auto-ready check failed", e); - } - } - }; - - check(); // Checks immediately - const interval = setInterval(check, 1000); // And polls - return () => clearInterval(interval); - }, [isReady, identity, status, signUserAction]); - - // ... (ensureReady, sendMessage, decryptMessage) ... - - - - const ensureReady = useCallback(async (password: string, userId: string) => { - setStatus('initializing'); - try { - if (!isStorageUnlocked()) { - await unlockChatStorage(password, userId); - } - let keys = await loadDeviceKeys(); - - // Helper to publish keys - const publishKeys = async (k: any) => { - const deviceId = localStorage.getItem('synapsis_device_id') || uuidv4(); - if (!localStorage.getItem('synapsis_device_id')) { - localStorage.setItem('synapsis_device_id', deviceId); - } - - - // We need to sign the prekey with our Identity Key. - // Convert X25519 Identity Key to a signing key? - // OR does this system use a separate Identity Key for signing? - // Looking at generateX25519KeyPair, it returns a key pair. - // Standard X3DH uses the Identity Key for signing the SignedPreKey. - // But X25519 is for DH, Ed25519 is for Signing. - // Typically Signal converts or uses skewed keys. - // IN THIS APP (based on legacy analysis): - // We might just use the User's DID Master Key (ECDSA) to sign the PreKey? - // Route.ts says: "The ECDSA signature of the bundle itself". - - // Let's look at `route.ts` again. - // It saves `signedPreKey` which contains `sig`. - // AND it saves `signature` separately. - - // If I simply allow the `requireSignedAction` to provide the main signature? - // But `route.ts` extracts `signature` from `body.data`. - // If I don't provide it, it is undefined. - - // Let's look at `signUserAction`. It signs with the DID Master Key (P-256). - // Let's use THAT to sign the bundle components if we lack a separate Ed25519 identity. - - // However, `signedPreKey` strictly needs a signature verifying it belongs to `identityKey`. - // If `identityKey` is X25519, it cannot sign (easily). - // Maybe the 'signature' expected is just a placeholder or signed by the DID? - - // Let's generate a dummy signature for now if we can't do X25519 signing easily, - // OR rely on the existing `signUserAction` to cover integrity. - // BUT strict validation might fail if fields are missing. - - // Let's verify `requireSignedAction` behavior. - // It verifies the `body.sig`. - // The `body.data.signature` is what we are missing. - - // Let's construct a payload that satisfies the fields. - - const spkPub = await exportKey(k.signedPreKey.publicKey); - const signedPreKeyPayload = { - id: 1, - key: spkPub, - // We need a signature here. - // Ideally this is signed by the Identity Key. - // For now, let's sign it with the DID Key (via signUserAction helper?) - // No, signUserAction wraps the data. - // We can't easily sign just this inner bit without identifying WHO signed it. - // If we leave it empty, does it fail? - // Route.ts doesn't validate `sig` inside `signedPreKey` explicitly, it just saving JSON. - }; - - const bundlePayload = { - deviceId, - identityKey: await exportKey(k.identity.publicKey), - signedPreKey: signedPreKeyPayload, - // We need a 'signature' field. - // Route.ts line 29 destructuring: const { signature } = body.data. - // DB line 57 stores it. - // If we omit it, it's undefined. DB might throw if not null. - // Let's put a placeholder or use the signedAction's signature? - // We can't know signedAction's signature before we create the payload. - // Let's put "ECDSA" or something, or repeat the identity key? - // Wait, if I look at `route.ts`, it says: - // "Should usually be signed by Identity Key". - // Since we are using DID for Auth, maybe we just put "signed-by-did" - // or actually sign the `identityKey + deviceId` with the DID key? - - // Actually, let's just make sure we pass *something* if the DB requires it. - // Checking DB schema next step. - - // BETTER FIX: - // The `signUserAction` returns `{ data, sig, ... }`. - // The `sig` covers `data`. - // Maybe we just pass `signature: "attached-envelope"` for now - // to bypass the destructuring undefined issue, - // effectively mocking what might be expected. - signature: 'signed-by-did-envelope', - - oneTimeKeys: await Promise.all(k.otks.map(async (ko: any, i: number) => ({ - id: 100 + i, - key: await exportKey(ko.publicKey) - }))) - }; - - const signedAction = await signUserAction('chat.keys.publish', bundlePayload); - - const res = await fetch('/api/chat/keys', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(signedAction) - }); - - if (!res.ok) throw new Error('Failed to publish keys'); - console.log('[Chat] Keys published successfully'); - }; - - if (!keys) { - setStatus('generating_keys'); - const identityKey = await generateX25519KeyPair(); - const signedPreKey = await generatePreKey(); - const otks = await Promise.all(Array.from({ length: 5 }).map(() => generatePreKey())); - - keys = { identity: identityKey, signedPreKey, otks }; - await storeDeviceKeys(identityKey, signedPreKey, otks); - await publishKeys(keys); - } else { - // Self-Repair: Check if server actually has our keys. - // If the user is in a "Zombie State" (local keys but server 404), we must republish. - try { - // Check only if we have a DID - if (identity?.did) { - const checkRes = await fetch(`/.well-known/synapsis/chat/${identity.did}`); - if (checkRes.status === 404) { - console.warn('[Chat] Detected Zombie State: Local keys exist but server returned 404. Republishing...'); - await publishKeys(keys); - } else { - // Also check if OUR deviceId is in the bundle list? - // For now, 404 check is the critical fix for the reported issue. - } - } - } catch (repairErr) { - console.error('[Chat] Self-repair check failed:', repairErr); - } - } - - // Restore session cache? - // Ideally load all sessions? Lazy load is better. - - setIsReady(true); - setStatus('ready'); - } catch (error) { - console.error('Chat init failed:', error); - setStatus('error'); - throw error; - } - }, [signUserAction]); - - const sendMessage = useCallback(async (recipientDid: string, content: string, nodeDomain?: string, recipientHandle?: string) => { - if (!isReady || !identity) throw new Error('Chat not ready'); - - // 1. Fetch Recipient Bundles (via Proxy to avoid CORS) - // We use our own server to fetch the keys from the remote node. - let proxyUrl = `/api/chat/keys/fetch?did=${encodeURIComponent(recipientDid)}`; - if (nodeDomain) { - proxyUrl += `&nodeDomain=${encodeURIComponent(nodeDomain)}`; - } - if (recipientHandle) { - proxyUrl += `&handle=${encodeURIComponent(recipientHandle)}`; - } - - let bundles: any[]; - try { - console.log(`[Chat] Fetching keys via proxy: ${proxyUrl}`); - const controller = new AbortController(); - const id = setTimeout(() => controller.abort(), 8000); // 8s timeout (proxy has 5s) - - const res = await fetch(proxyUrl, { - signal: controller.signal - }); - clearTimeout(id); - - if (!res.ok) { - const data = await res.json(); - console.error(`[Chat] Bundle fetch failed (${res.status}):`, data); - throw new Error(`Recipient keys not found (Status: ${res.status})`); - } - bundles = await res.json(); - console.log(`[Chat] Fetched ${bundles.length} device bundle(s):`, bundles); - } catch (err: any) { - console.error(`[Chat] Network error fetching bundles:`, err); - throw new Error(`Failed to resolve recipient keys: ${err.message}`); - } - - - const localDeviceId = localStorage.getItem('synapsis_device_id'); - if (!localDeviceId) throw new Error('No local device ID'); - - const localKeys = await loadDeviceKeys(); - if (!localKeys) throw new Error('Keys lost'); - - console.log(`[Chat] Processing ${bundles.length} recipient device(s)...`); - - // 2. Loop through all devices - for (const bundle of bundles) { - // IMPORTANT: Use the DID from the bundle! - // This handles the "Aliasing" case where we asked for did:synapsis but got did:web keys. - // The session should be bound to the DID that signed the keys. - const targetDid = bundle.did || recipientDid; - - const sessionKey = `session:${targetDid}:${bundle.deviceId}`; - - let state = sessionsRef.current.get(sessionKey); - if (!state) { - const stored = await loadEncrypted(sessionKey); - if (stored) { - try { - // If stored is old/broken (missing rootKey string), this throws. - state = await deserializeRatchetState(stored); - } catch (e) { - console.warn('[Chat] Found corrupted session state, resetting:', e); - // Delete the corrupted session from storage - await deleteEncrypted(sessionKey); - state = undefined; - } - } - } - - let headerData: any = null; - - if (!state) { - console.log(`[Chat] Initializing new session for device ${bundle.deviceId}...`); - // X3DH Init - const remoteIdentityKey = await importX25519PublicKey(bundle.identityKey); - const remoteSignedPreKey = await importX25519PublicKey(bundle.signedPreKey.key); - const otk = bundle.oneTimeKeys[0]; - const remoteOtk = otk ? await importX25519PublicKey(otk.key) : undefined; - - const { sk, ephemeralKey } = await x3dhSender( - localKeys.identity, - { identityKey: remoteIdentityKey, signedPreKey: remoteSignedPreKey, oneTimeKey: remoteOtk }, - `SynapsisV2${[identity.did, targetDid].sort().join('')}${[localDeviceId, bundle.deviceId].sort().join('')}` - ); - - state = await initSender(sk, remoteSignedPreKey); - - headerData = { - ik: await exportKey(localKeys.identity.publicKey), - ek: await exportKey(ephemeralKey.publicKey), - spkId: bundle.signedPreKey.id, - opkId: otk?.id - }; - } - - console.log(`[Chat] Encrypting message for device ${bundle.deviceId}...`); - const { ciphertext, newState } = await ratchetEncrypt(state, content); - - sessionsRef.current.set(sessionKey, newState); - // Serialize before storing - const serialized = await serializeRatchetState(newState); - await storeEncrypted(sessionKey, serialized); - - // Encrypt a copy for ourselves so we can read our own messages on any device - // Use a simple AES-GCM encryption with a key derived from our identity - const selfEncrypted = await encryptForSelf(content, localKeys.identity.publicKey); - - // Payload - const payload = { - recipientDid, - recipientDeviceId: bundle.deviceId, - senderDeviceId: localDeviceId, // V2.1 Addition - ciphertext: ciphertext.ciphertext, - header: headerData ? { ...headerData, ...ciphertext.header } : ciphertext.header, - iv: ciphertext.iv, - selfEncrypted // So sender can decrypt on any device - }; - - const fullData = { - recipientDid, - recipientDeviceId: bundle.deviceId, - ciphertext: JSON.stringify(payload), - nodeDomain: nodeDomain || undefined, // Include for remote routing - recipientHandle: recipientHandle || undefined // Include for conversation creation - }; - - const action = await signUserAction('chat.deliver', fullData); - - const sendRes = await fetch('/api/chat/send', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(action) - }); - - if (!sendRes.ok) { - const errorText = await sendRes.text(); - let errorData; - try { - errorData = JSON.parse(errorText); - } catch { - errorData = { error: errorText }; - } - console.error('[Chat] Send failed:', sendRes.status, errorData); - throw new Error(`Failed to send message: ${errorData.error || sendRes.statusText}`); - } - - console.log(`[Chat] Message sent successfully to device ${bundle.deviceId}`); - } - - console.log('[Chat] All messages sent successfully'); - }, [isReady, identity, signUserAction]); - - /** - * Decrypt and verify an incoming envelope - */ - const decryptMessage = useCallback(async (envelope: any) => { - if (!isReady || !identity) return '[Chat not ready]'; - - try { - // 1. Check Envelope Structure - // Envelope is SignedAction. - // We assume signature verified by server/trusted for now (TODO: Client verify) - - const { did: senderDid, data } = envelope; - const payloadString = data.ciphertext; // inner JSON payload - - if (!payloadString) return '[Legacy Message]'; // Fail gracefully - - const payload = JSON.parse(payloadString); - const { recipientDeviceId, senderDeviceId, ciphertext, header, iv, selfEncrypted } = payload; - - const localDeviceId = localStorage.getItem('synapsis_device_id'); - - // Check if this is a message WE sent (not for us to decrypt with ratchet) - if (recipientDeviceId !== localDeviceId) { - // This is a message we sent - try to decrypt the self-encrypted copy - if (selfEncrypted) { - try { - const localKeys = await loadDeviceKeys(); - if (!localKeys) return '[Keys locked]'; - const plaintext = await decryptForSelf(selfEncrypted, localKeys.identity); - return plaintext; - } catch (e) { - console.error('[Chat] Failed to decrypt self-encrypted message:', e); - return '[Sent Message]'; - } - } - return '[Sent Message]'; - } - - // 2. Load Session - const sessionKey = `session:${senderDid}:${senderDeviceId}`; - let state = sessionsRef.current.get(sessionKey); - if (!state) { - const stored = await loadEncrypted(sessionKey); - if (stored) { - try { - state = await deserializeRatchetState(stored); - } catch (e) { - console.warn('[Chat] Corrupted session for decryption, treating as new/lost:', e); - // Delete the corrupted session from storage - await deleteEncrypted(sessionKey); - } - } - } - - // 3. X3DH Receiver Init if needed - if (!state) { - // If it's a new session, headers MUST contain X3DH info (ik, ek, spkId, opkId) - if (!header.ik || !header.ek) return '[Invalid Init Header]'; - - const localKeys = await loadDeviceKeys(); - if (!localKeys) return '[Keys locked]'; - - // Recover keys - const senderIdentityKey = await importX25519PublicKey(header.ik); - const senderEphemeralKey = await importX25519PublicKey(header.ek); - - // Find my used OTK - // Ideally we consume it and delete it. - // For now, load it. - // In V2.1 "chat_one_time_keys" table stores them. BUT we need private key locally. - // localKeys.otks is array. - // We find the one with id == header.opkId - // Caution: types for otks is Array. ID is assumed sequential/mapped? - // In generation I assigned arbitrary IDs. - // Re-check generation: `id: 100 + i`. - // I need to map ID to private key. - // In `storeDeviceKeys` I stored them as array. - // I need to match valid key. - - let myOtk: any = undefined; - if (header.opkId) { - // Find index? ID 100 -> index 0? - const index = header.opkId - 100; - if (index >= 0 && index < localKeys.otks.length) { - myOtk = localKeys.otks[index]; - } - } - - const sk = await x3dhReceiver( - localKeys.identity, - localKeys.signedPreKey, - myOtk, - senderIdentityKey, - senderEphemeralKey, - `SynapsisV2${[senderDid, identity.did].sort().join('')}${[senderDeviceId, localDeviceId].sort().join('')}` - ); - - state = await initReceiver(sk, localKeys.signedPreKey); // Using SPK pair as initial - } - - // 4. Decrypt - // Reconstruct CiphertextMessage - const msgStruct: any = { - header: header, // contains dh, pn, n - ciphertext: ciphertext, - iv: iv - }; - - const { plaintext, newState } = await ratchetDecrypt(state, msgStruct); - - // 5. Update Session - sessionsRef.current.set(sessionKey, newState); - const serialized = await serializeRatchetState(newState); - await storeEncrypted(sessionKey, serialized); - - return plaintext; - - } catch (e: any) { - console.error('Decryption failed:', e); - return `[Decryption Error: ${e.message}]`; - } - }, [isReady, identity]); - - return { - isReady, - isLocked, - status, - ensureReady, - sendMessage, - decryptMessage - }; -} diff --git a/src/lib/hooks/useSodiumChat.ts b/src/lib/hooks/useSodiumChat.ts new file mode 100644 index 0000000..e8232f0 --- /dev/null +++ b/src/lib/hooks/useSodiumChat.ts @@ -0,0 +1,204 @@ +/** + * React Hook for Libsodium E2EE Chat + */ + +'use client'; + +import { useState, useCallback, useEffect, useRef } from 'react'; +import { useAuth } from '@/lib/contexts/AuthContext'; +import { keyStore } from '@/lib/crypto/user-signing'; +import * as SodiumChat from '@/lib/crypto/sodium-chat'; + +export function useSodiumChat() { + const { user, isIdentityUnlocked } = useAuth(); + const [isReady, setIsReady] = useState(false); + const [status, setStatus] = useState('idle'); + const keysRef = useRef<{ publicKey: string; privateKey: string } | null>(null); + + // Initialize and load/generate keys + useEffect(() => { + if (!user?.id || !isIdentityUnlocked) return; + + const init = async () => { + try { + setStatus('initializing'); + + await SodiumChat.initSodium(); + + // Get storage key from memory + const storageKey = keyStore.getStorageKey(); + if (!storageKey) { + throw new Error('Storage key not available - identity must be unlocked first'); + } + + // Try to load existing keys (encrypted in IndexedDB) + let keys = await SodiumChat.getStoredKeys(user.id, storageKey); + + if (!keys) { + // Generate new keys + console.log('[Sodium] Generating new key pair...'); + keys = await SodiumChat.generateChatKeyPair(); + await SodiumChat.storeKeys(user.id, keys.publicKey, keys.privateKey, storageKey); + + // Publish public key to server + console.log('[Sodium] Publishing public key to server...'); + const response = await fetch('/api/chat/keys', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ publicKey: keys.publicKey }), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: response.statusText })); + console.error('[Sodium] Failed to publish key:', errorData); + throw new Error(`Failed to publish key: ${errorData.error || response.statusText}`); + } + + const result = await response.json(); + console.log('[Sodium] Keys generated and published successfully:', result); + } else { + console.log('[Sodium] Loaded existing keys from IndexedDB'); + + // Verify key exists on server + console.log('[Sodium] Verifying key on server...'); + const checkResponse = await fetch(`/api/chat/keys?did=${encodeURIComponent(user.did)}`); + + let shouldPublish = false; + + if (!checkResponse.ok) { + console.log('[Sodium] Key not found on server, re-publishing...'); + shouldPublish = true; + } else { + // Check if the key on server MATCHES our local key + const serverData = await checkResponse.json(); + if (serverData.publicKey !== keys.publicKey) { + console.warn('[Sodium] Server key mismatch! Re-publishing local key...'); + shouldPublish = true; + } else { + console.log('[Sodium] Key verified on server'); + } + } + + if (shouldPublish) { + // Re-publish the key + const response = await fetch('/api/chat/keys', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ publicKey: keys.publicKey }), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: response.statusText })); + console.error('[Sodium] Failed to re-publish key:', errorData); + throw new Error(`Failed to re-publish key: ${errorData.error || response.statusText}`); + } + + console.log('[Sodium] Key re-published successfully'); + } + } + + keysRef.current = keys; + setIsReady(true); + setStatus('ready'); + } catch (error) { + console.error('[Sodium] Initialization failed:', error); + setStatus('error'); + } + }; + + init(); + }, [user?.id, user?.did, isIdentityUnlocked]); + + const sendMessage = useCallback(async ( + recipientDid: string, + message: string, + recipientHandle?: string + ): Promise => { + if (!keysRef.current || !isReady || !user?.id) { + throw new Error('Sodium not ready'); + } + + try { + // Fetch recipient's public key + console.log('[Sodium] Fetching recipient public key for:', recipientDid); + let response = await fetch(`/api/chat/keys?did=${encodeURIComponent(recipientDid)}`); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: response.statusText })); + console.error('[Sodium] Failed to fetch recipient keys:', errorData); + throw new Error(`Failed to fetch recipient keys: ${errorData.error || response.statusText}`); + } + + const { publicKey: recipientPublicKey } = await response.json(); + console.log('[Sodium] Got recipient public key:', recipientPublicKey ? 'YES' : 'NO'); + + if (!recipientPublicKey) { + throw new Error('Recipient has no public key'); + } + + // Encrypt message + console.log('[Sodium] Encrypting message...'); + const encrypted = await SodiumChat.encryptMessage( + message, + recipientPublicKey, + keysRef.current.privateKey + ); + + // Send to server + console.log('[Sodium] Sending encrypted message to server...'); + response = await fetch('/api/chat/send', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + recipientDid, + senderPublicKey: keysRef.current.publicKey, + ciphertext: encrypted.ciphertext, + nonce: encrypted.nonce, + recipientHandle, + }), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: response.statusText })); + console.error('[Sodium] Failed to send message:', errorData); + throw new Error(`Failed to send message: ${errorData.error || response.statusText}`); + } + + console.log('[Sodium] Message sent successfully'); + } catch (error) { + console.error('[Sodium] Send failed:', error); + throw error; + } + }, [isReady, user?.id]); + + const decryptMessage = useCallback(async ( + ciphertext: string, + nonce: string, + senderPublicKey: string + ): Promise => { + if (!keysRef.current || !isReady) { + throw new Error('Sodium not ready'); + } + + try { + const plaintext = await SodiumChat.decryptMessage( + ciphertext, + nonce, + senderPublicKey, + keysRef.current.privateKey + ); + + return plaintext; + } catch (error) { + console.error('[Sodium] Decryption failed:', error); + throw error; + } + }, [isReady]); + + return { + isReady, + status, + sendMessage, + decryptMessage, + }; +} diff --git a/src/lib/hooks/useUserIdentity.ts b/src/lib/hooks/useUserIdentity.ts index 2a61a8f..73dd475 100644 --- a/src/lib/hooks/useUserIdentity.ts +++ b/src/lib/hooks/useUserIdentity.ts @@ -89,8 +89,22 @@ export function useUserIdentity() { /** * Unlock the identity with a password */ - const unlockIdentity = async (privateKeyEncrypted: string, password: string) => { + const unlockIdentity = async (privateKeyEncrypted: string, password: string, userDid?: string, userHandle?: string, userPublicKey?: string) => { try { + console.log('[Identity] Unlocking with DID:', userDid, 'Handle:', userHandle); + + // Set identity first if provided (needed for storage key derivation) + if (userDid && userHandle && userPublicKey) { + keyStore.setIdentity({ + did: userDid, + handle: userHandle, + publicKey: userPublicKey + }); + console.log('[Identity] Identity set in keyStore'); + } else { + console.warn('[Identity] Missing user info for identity setup'); + } + // 1. Decrypt the PEM/String from server (which is actually a base64 encoded PKCS8 export usually?) // Wait, existing implementation returns a string. // We need to verify what `decryptPrivateKey` returns. @@ -114,8 +128,42 @@ export function useUserIdentity() { // 3. Store in Memory keyStore.setPrivateKey(cryptoKey); + console.log('[Identity] Private key stored in memory'); - // 4. Update State + // 4. Derive and store storage key for chat encryption + // Use libsodium's pwhash to derive a storage key from the password + const sodiumModule = await import('libsodium-wrappers-sumo'); + await sodiumModule.default.ready; + const sodium = sodiumModule.default; + + // Use a fixed salt derived from the user's identity to ensure consistency + const identity = keyStore.getIdentity(); + console.log('[Identity] Retrieved identity from keyStore:', identity); + + if (identity) { + const saltString = `synapsis-chat-storage-${identity.did}`; + // Generate a fixed-length salt from the DID + // Hash to 32 bytes, then take first 16 bytes for salt + const fullHash = sodium.crypto_generichash(32, saltString); + const salt = fullHash.slice(0, sodium.crypto_pwhash_SALTBYTES); + + console.log('[Identity] Deriving storage key...'); + const storageKey = sodium.crypto_pwhash( + 32, // 32 bytes for secretbox + password, + salt, + sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE, + sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE, + sodium.crypto_pwhash_ALG_DEFAULT + ); + + keyStore.setStorageKey(storageKey); + console.log('[Identity] Storage key derived and stored'); + } else { + console.error('[Identity] No identity in keyStore - cannot derive storage key'); + } + + // 5. Update State setIdentity(prev => prev ? { ...prev, isUnlocked: true } : null); // We need the other data... setIsUnlocked(true);