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);