diff --git a/.husky/pre-commit b/.husky/pre-commit index 2312dc587..a4cc01d56 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,3 @@ -npx lint-staged +# The .cmd shims on Windows add their own paths to each command, so keep +# lint-staged's chunks well under cmd.exe's 8191 character limit. +npx lint-staged --max-arg-length 4000 diff --git a/docker/nginx-https.conf b/docker/nginx-https.conf index 21a831f8c..04c0a0ca3 100644 --- a/docker/nginx-https.conf +++ b/docker/nginx-https.conf @@ -161,7 +161,7 @@ http { root /app/html; index index.html index.htm; expires off; - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-teIJ2CKVs77AjLM0kquz3mSdDahr9QtMZGG0tQjbbkU='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; media-src 'self' data: blob: http: https:; worker-src 'self' blob:; frame-src http: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-gjgrKvv0zkJ6QUgUJBcK4CPIIa2JbLMmHecileCoA9A='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; media-src 'self' data: blob: http: https:; worker-src 'self' blob:; frame-src http: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; diff --git a/docker/nginx.conf b/docker/nginx.conf index 5d3354b07..56d395307 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -142,7 +142,7 @@ http { root /app/html; index index.html index.htm; expires off; - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-teIJ2CKVs77AjLM0kquz3mSdDahr9QtMZGG0tQjbbkU='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; media-src 'self' data: blob: http: https:; worker-src 'self' blob:; frame-src http: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-gjgrKvv0zkJ6QUgUJBcK4CPIIa2JbLMmHecileCoA9A='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; media-src 'self' data: blob: http: https:; worker-src 'self' blob:; frame-src http: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; diff --git a/drizzle/mysql/0045_flowery_the_hood.sql b/drizzle/mysql/0045_flowery_the_hood.sql new file mode 100644 index 000000000..e8606bebc --- /dev/null +++ b/drizzle/mysql/0045_flowery_the_hood.sql @@ -0,0 +1,5 @@ +-- ssh_data.enable_docker and docker_config moved to the docker plugin's host +-- settings. The copy runs at boot, after this migration, so a real DROP +-- COLUMN here would delete the data first. The columns stay in place, +-- unused, until 3.0.0. +SELECT 1; diff --git a/drizzle/mysql/meta/0045_snapshot.json b/drizzle/mysql/meta/0045_snapshot.json new file mode 100644 index 000000000..0b1b21e92 --- /dev/null +++ b/drizzle/mysql/meta/0045_snapshot.json @@ -0,0 +1,7199 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "ca1e226b-6111-4345-9702-2f82da4c3090", + "prevId": "bf0c51de-8d0f-4d1d-8270-a6661aa24d8a", + "tables": { + "ai_conversations": { + "name": "ai_conversations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_conversations_user": { + "name": "idx_ai_conversations_user", + "columns": [ + "user_id", + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_conversations_user_id_users_id_fk": { + "name": "ai_conversations_user_id_users_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_conversations_id": { + "name": "ai_conversations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ai_messages": { + "name": "ai_messages", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('')" + }, + "tool_calls": { + "name": "tool_calls", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_messages_conversation": { + "name": "idx_ai_messages_conversation", + "columns": [ + "conversation_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_messages_conversation_id_ai_conversations_id_fk": { + "name": "ai_messages_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_messages", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_messages_id": { + "name": "ai_messages_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ai_proposals": { + "name": "ai_proposals", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_proposals_user": { + "name": "idx_ai_proposals_user", + "columns": [ + "user_id", + "status" + ], + "isUnique": false + }, + "idx_ai_proposals_conversation": { + "name": "idx_ai_proposals_conversation", + "columns": [ + "conversation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_proposals_conversation_id_ai_conversations_id_fk": { + "name": "ai_proposals_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_proposals_user_id_users_id_fk": { + "name": "ai_proposals_user_id_users_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_proposals_id": { + "name": "ai_proposals_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ai_providers": { + "name": "ai_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key_prefix": { + "name": "api_key_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_providers_user_label": { + "name": "idx_ai_providers_user_label", + "columns": [ + "user_id", + "label" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ai_providers_user_id_users_id_fk": { + "name": "ai_providers_user_id_users_id_fk", + "tableFrom": "ai_providers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_providers_id": { + "name": "ai_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_keys_id": { + "name": "api_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_channels": { + "name": "automation_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_channels_pair": { + "name": "idx_automation_channels_pair", + "columns": [ + "automation_id", + "channel_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "automation_channels_automation_id_automations_id_fk": { + "name": "automation_channels_automation_id_automations_id_fk", + "tableFrom": "automation_channels", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_channels_channel_id_notification_channels_id_fk": { + "name": "automation_channels_channel_id_notification_channels_id_fk", + "tableFrom": "automation_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_channels_id": { + "name": "automation_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_run_steps": { + "name": "automation_run_steps", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "run_id": { + "name": "run_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_index": { + "name": "step_index", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_id": { + "name": "step_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_type": { + "name": "step_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truncated": { + "name": "truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_automation_run_steps_run": { + "name": "idx_automation_run_steps_run", + "columns": [ + "run_id", + "step_index" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_run_steps_run_id_automation_runs_id_fk": { + "name": "automation_run_steps_run_id_automation_runs_id_fk", + "tableFrom": "automation_run_steps", + "tableTo": "automation_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_run_steps_id": { + "name": "automation_run_steps_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_runs": { + "name": "automation_runs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_context": { + "name": "trigger_context", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_runs_automation": { + "name": "idx_automation_runs_automation", + "columns": [ + "automation_id", + "started_at" + ], + "isUnique": false + }, + "idx_automation_runs_user": { + "name": "idx_automation_runs_user", + "columns": [ + "user_id", + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_runs_automation_id_automations_id_fk": { + "name": "automation_runs_automation_id_automations_id_fk", + "tableFrom": "automation_runs", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_runs_user_id_users_id_fk": { + "name": "automation_runs_user_id_users_id_fk", + "tableFrom": "automation_runs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_runs_id": { + "name": "automation_runs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_schedules": { + "name": "automation_schedules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_due_at": { + "name": "next_due_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_tick_at": { + "name": "last_tick_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_schedules_automation": { + "name": "idx_automation_schedules_automation", + "columns": [ + "automation_id" + ], + "isUnique": true + }, + "idx_automation_schedules_due": { + "name": "idx_automation_schedules_due", + "columns": [ + "next_due_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_schedules_automation_id_automations_id_fk": { + "name": "automation_schedules_automation_id_automations_id_fk", + "tableFrom": "automation_schedules", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_schedules_id": { + "name": "automation_schedules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automation_trigger_state": { + "name": "automation_trigger_state", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "state_key": { + "name": "state_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "breach_started_at": { + "name": "breach_started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_value": { + "name": "last_value", + "type": "double", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_observed_state": { + "name": "last_observed_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_automation_trigger_state_key": { + "name": "idx_automation_trigger_state_key", + "columns": [ + "automation_id", + "state_key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "automation_trigger_state_automation_id_automations_id_fk": { + "name": "automation_trigger_state_automation_id_automations_id_fk", + "tableFrom": "automation_trigger_state", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_trigger_state_id": { + "name": "automation_trigger_state_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automations": { + "name": "automations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "definition": { + "name": "definition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "definition_version": { + "name": "definition_version", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('skip')" + }, + "max_run_seconds": { + "name": "max_run_seconds", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_automations_user": { + "name": "idx_automations_user", + "columns": [ + "user_id", + "enabled" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automations_user_id_users_id_fk": { + "name": "automations_user_id_users_id_fk", + "tableFrom": "automations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automations_id": { + "name": "automations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_access": { + "name": "credential_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('use')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_access_id": { + "name": "credential_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_sidebar_preferences_user_id": { + "name": "credential_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dashboard_service_links_id": { + "name": "dashboard_service_links_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dismissed_alerts_id": { + "name": "dismissed_alerts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "folder_access": { + "name": "folder_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + "owner_user_id", + "folder" + ], + "isUnique": false + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "folder_access_id": { + "name": "folder_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "folder_id": { + "name": "folder_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_homepage_items_user_id": { + "name": "idx_homepage_items_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_items_id": { + "name": "homepage_items_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_layouts_id": { + "name": "homepage_layouts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ] + } + }, + "checkConstraint": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_access_id": { + "name": "host_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_sidebar_preferences_user_id": { + "name": "host_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('ssh')" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_web_ui": { + "name": "enable_web_ui", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_ai_assistant": { + "name": "enable_ai_assistant", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "web_ui_config": { + "name": "web_ui_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('sha256')" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_data_id": { + "name": "ssh_data_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "notification_channels_id": { + "name": "notification_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "opkssh_tokens_id": { + "name": "opkssh_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_install_counts": { + "name": "plugin_install_counts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('aggregate-telemetry')" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + "plugin_id", + "registry_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_install_counts_id": { + "name": "plugin_install_counts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_migrations": { + "name": "plugin_migrations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "migration_id": { + "name": "migration_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + "plugin_id", + "migration_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_migrations_id": { + "name": "plugin_migrations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_permission_grants": { + "name": "plugin_permission_grants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capability": { + "name": "capability", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('admin')" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + "plugin_id", + "capability" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_permission_grants_id": { + "name": "plugin_permission_grants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_registries": { + "name": "plugin_registries", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('community')" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_registries_id": { + "name": "plugin_registries_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_settings": { + "name": "plugin_settings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + "plugin_id", + "scope", + "scope_id", + "key" + ], + "isUnique": true + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + "plugin_id", + "scope" + ], + "isUnique": false + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_settings_id": { + "name": "plugin_settings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_storage": { + "name": "plugin_storage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + "plugin_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_storage_id": { + "name": "plugin_storage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugins": { + "name": "plugins", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('available')" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('community')" + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('disabled')" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + "registry_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugins_id": { + "name": "plugins_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "role_name": { + "name": "role_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + "role_name", + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rbac_applied_defaults_id": { + "name": "rbac_applied_defaults_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rbac_known_permissions": { + "name": "rbac_known_permissions", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rbac_known_permissions_id": { + "name": "rbac_known_permissions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recent_activity_id": { + "name": "recent_activity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "secret_sources": { + "name": "secret_sources", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('onepassword-connect')" + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_secret_sources_user": { + "name": "idx_secret_sources_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "secret_sources_user_id_users_id_fk": { + "name": "secret_sources_user_id_users_id_fk", + "tableFrom": "secret_sources", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "secret_sources_id": { + "name": "secret_sources_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "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": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "settings_key": { + "name": "settings_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_credential_secrets": { + "name": "shared_credential_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('password')" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + "credential_access_id", + "target_user_id" + ], + "isUnique": true + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + "target_user_id", + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_credential_secrets_id": { + "name": "shared_credential_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_auth_overrides_id": { + "name": "shared_host_auth_overrides_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('credential')" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_secrets_id": { + "name": "shared_host_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credential_usage_id": { + "name": "ssh_credential_usage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credentials_id": { + "name": "ssh_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_folders_id": { + "name": "ssh_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sso_providers_id": { + "name": "sso_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_tombstones_id": { + "name": "sync_tombstones_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identities_id": { + "name": "termix_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_ca_id": { + "name": "termix_identity_ca_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_keys_id": { + "name": "termix_identity_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "trusted_devices_id": { + "name": "trusted_devices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ui_preferences_user_id": { + "name": "ui_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_external_identities": { + "name": "user_external_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + "provider_id", + "subject" + ], + "isUnique": true + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_external_identities_id": { + "name": "user_external_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_open_tabs_id": { + "name": "user_open_tabs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_assistant_enabled": { + "name": "ai_assistant_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_read_only_commands": { + "name": "ai_read_only_commands", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_defaults": { + "name": "terminal_defaults", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_macros": { + "name": "terminal_macros", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id": { + "name": "user_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_id": { + "name": "user_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_second_factors": { + "name": "user_second_factors", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "factor_id": { + "name": "factor_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + "user_id", + "plugin_id", + "factor_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_second_factors_id": { + "name": "user_second_factors_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('openid email profile')" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_profiles_id": { + "name": "vault_profiles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_tokens_id": { + "name": "vault_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('preferred')" + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "webauthn_credentials_id": { + "name": "webauthn_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/_journal.json b/drizzle/mysql/meta/_journal.json index aa0ea3ff3..686dcce64 100644 --- a/drizzle/mysql/meta/_journal.json +++ b/drizzle/mysql/meta/_journal.json @@ -316,6 +316,13 @@ "when": 1790266402855, "tag": "0044_jittery_thunderball", "breakpoints": true + }, + { + "idx": 45, + "version": "5", + "when": 1790268466430, + "tag": "0045_flowery_the_hood", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/postgres/0046_small_white_tiger.sql b/drizzle/postgres/0046_small_white_tiger.sql new file mode 100644 index 000000000..e8606bebc --- /dev/null +++ b/drizzle/postgres/0046_small_white_tiger.sql @@ -0,0 +1,5 @@ +-- ssh_data.enable_docker and docker_config moved to the docker plugin's host +-- settings. The copy runs at boot, after this migration, so a real DROP +-- COLUMN here would delete the data first. The columns stay in place, +-- unused, until 3.0.0. +SELECT 1; diff --git a/drizzle/postgres/meta/0046_snapshot.json b/drizzle/postgres/meta/0046_snapshot.json new file mode 100644 index 000000000..2f76d3b1b --- /dev/null +++ b/drizzle/postgres/meta/0046_snapshot.json @@ -0,0 +1,7035 @@ +{ + "id": "6d15efa0-bf3b-4ada-9af2-56eb1a359b72", + "prevId": "261179bf-2c06-49d0-b6a8-062fd981c149", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ai_conversations": { + "name": "ai_conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_conversations_user": { + "name": "idx_ai_conversations_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_conversations_user_id_users_id_fk": { + "name": "ai_conversations_user_id_users_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_messages": { + "name": "ai_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "tool_calls": { + "name": "tool_calls", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_messages_conversation": { + "name": "idx_ai_messages_conversation", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_messages_conversation_id_ai_conversations_id_fk": { + "name": "ai_messages_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_messages", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_proposals": { + "name": "ai_proposals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_proposals_user": { + "name": "idx_ai_proposals_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_proposals_conversation": { + "name": "idx_ai_proposals_conversation", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_proposals_conversation_id_ai_conversations_id_fk": { + "name": "ai_proposals_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_proposals_user_id_users_id_fk": { + "name": "ai_proposals_user_id_users_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_providers": { + "name": "ai_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_prefix": { + "name": "api_key_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_providers_user_label": { + "name": "idx_ai_providers_user_label", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "label", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_providers_user_id_users_id_fk": { + "name": "ai_providers_user_id_users_id_fk", + "tableFrom": "ai_providers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_channels": { + "name": "automation_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_automation_channels_pair": { + "name": "idx_automation_channels_pair", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_channels_automation_id_automations_id_fk": { + "name": "automation_channels_automation_id_automations_id_fk", + "tableFrom": "automation_channels", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_channels_channel_id_notification_channels_id_fk": { + "name": "automation_channels_channel_id_notification_channels_id_fk", + "tableFrom": "automation_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_run_steps": { + "name": "automation_run_steps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "step_index": { + "name": "step_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "step_id": { + "name": "step_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_type": { + "name": "step_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "truncated": { + "name": "truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "idx_automation_run_steps_run": { + "name": "idx_automation_run_steps_run", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "step_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_run_steps_run_id_automation_runs_id_fk": { + "name": "automation_run_steps_run_id_automation_runs_id_fk", + "tableFrom": "automation_run_steps", + "tableTo": "automation_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_runs": { + "name": "automation_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_context": { + "name": "trigger_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_automation_runs_automation": { + "name": "idx_automation_runs_automation", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_automation_runs_user": { + "name": "idx_automation_runs_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_runs_automation_id_automations_id_fk": { + "name": "automation_runs_automation_id_automations_id_fk", + "tableFrom": "automation_runs", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_runs_user_id_users_id_fk": { + "name": "automation_runs_user_id_users_id_fk", + "tableFrom": "automation_runs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_schedules": { + "name": "automation_schedules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_due_at": { + "name": "next_due_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_tick_at": { + "name": "last_tick_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_automation_schedules_automation": { + "name": "idx_automation_schedules_automation", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_automation_schedules_due": { + "name": "idx_automation_schedules_due", + "columns": [ + { + "expression": "next_due_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_schedules_automation_id_automations_id_fk": { + "name": "automation_schedules_automation_id_automations_id_fk", + "tableFrom": "automation_schedules", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_trigger_state": { + "name": "automation_trigger_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "state_key": { + "name": "state_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "breach_started_at": { + "name": "breach_started_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_value": { + "name": "last_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "last_observed_state": { + "name": "last_observed_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_automation_trigger_state_key": { + "name": "idx_automation_trigger_state_key", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_trigger_state_automation_id_automations_id_fk": { + "name": "automation_trigger_state_automation_id_automations_id_fk", + "tableFrom": "automation_trigger_state", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automations": { + "name": "automations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "definition": { + "name": "definition", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "definition_version": { + "name": "definition_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'skip'" + }, + "max_run_seconds": { + "name": "max_run_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_automations_user": { + "name": "idx_automations_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automations_user_id_users_id_fk": { + "name": "automations_user_id_users_id_fk", + "tableFrom": "automations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_access": { + "name": "credential_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'use'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_service_links": { + "name": "dashboard_service_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dismissed_alerts": { + "name": "dismissed_alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.folder_access": { + "name": "folder_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "folder", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_items": { + "name": "homepage_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_homepage_items_user_id": { + "name": "idx_homepage_items_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_layouts": { + "name": "homepage_layouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_access": { + "name": "host_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_data": { + "name": "ssh_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_web_ui": { + "name": "enable_web_ui", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_ai_assistant": { + "name": "enable_ai_assistant", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "web_ui_config": { + "name": "web_ui_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + { + "expression": "parent_host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channels": { + "name": "notification_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.opkssh_tokens": { + "name": "opkssh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_install_counts": { + "name": "plugin_install_counts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'aggregate-telemetry'" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_migrations": { + "name": "plugin_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "migration_id": { + "name": "migration_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "migration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_permission_grants": { + "name": "plugin_permission_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "capability": { + "name": "capability", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "capability", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_registries": { + "name": "plugin_registries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'community'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_settings": { + "name": "plugin_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_storage": { + "name": "plugin_storage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugins": { + "name": "plugins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'community'" + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'disabled'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + { + "expression": "registry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "role_name": { + "name": "role_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + { + "expression": "role_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rbac_known_permissions": { + "name": "rbac_known_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recent_activity": { + "name": "recent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.secret_sources": { + "name": "secret_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'onepassword-connect'" + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_secret_sources_user": { + "name": "idx_secret_sources_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "secret_sources_user_id_users_id_fk": { + "name": "secret_sources_user_id_users_id_fk", + "tableFrom": "secret_sources", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + { + "expression": "expires_at", + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_credential_secrets": { + "name": "shared_credential_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'password'" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + { + "expression": "credential_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_secrets": { + "name": "shared_host_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + { + "expression": "host_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credential_usage": { + "name": "ssh_credential_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credentials": { + "name": "ssh_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_folders": { + "name": "ssh_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_providers": { + "name": "sso_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_tombstones": { + "name": "sync_tombstones", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identities": { + "name": "termix_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_ca": { + "name": "termix_identity_ca", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "nullsNotDistinct": false, + "columns": [ + "identity_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_keys": { + "name": "termix_identity_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_devices": { + "name": "trusted_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ui_preferences": { + "name": "ui_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_external_identities": { + "name": "user_external_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_open_tabs": { + "name": "user_open_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_assistant_enabled": { + "name": "ai_assistant_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ai_read_only_commands": { + "name": "ai_read_only_commands", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_defaults": { + "name": "terminal_defaults", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_macros": { + "name": "terminal_macros", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_second_factors": { + "name": "user_second_factors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "factor_id": { + "name": "factor_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "factor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_profiles": { + "name": "vault_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_tokens": { + "name": "vault_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webauthn_credentials": { + "name": "webauthn_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/postgres/meta/_journal.json b/drizzle/postgres/meta/_journal.json index aa33abc9a..21cb3d70b 100644 --- a/drizzle/postgres/meta/_journal.json +++ b/drizzle/postgres/meta/_journal.json @@ -323,6 +323,13 @@ "when": 1790266402403, "tag": "0045_thankful_shinobi_shaw", "breakpoints": true + }, + { + "idx": 46, + "version": "7", + "when": 1790268465907, + "tag": "0046_small_white_tiger", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/sqlite/0042_many_old_lace.sql b/drizzle/sqlite/0042_many_old_lace.sql new file mode 100644 index 000000000..e8606bebc --- /dev/null +++ b/drizzle/sqlite/0042_many_old_lace.sql @@ -0,0 +1,5 @@ +-- ssh_data.enable_docker and docker_config moved to the docker plugin's host +-- settings. The copy runs at boot, after this migration, so a real DROP +-- COLUMN here would delete the data first. The columns stay in place, +-- unused, until 3.0.0. +SELECT 1; diff --git a/drizzle/sqlite/meta/0042_snapshot.json b/drizzle/sqlite/meta/0042_snapshot.json new file mode 100644 index 000000000..a39c21aeb --- /dev/null +++ b/drizzle/sqlite/meta/0042_snapshot.json @@ -0,0 +1,6786 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "32849df3-69f5-42b0-88cd-e55aff3a00d9", + "prevId": "18dc3df0-32ee-41c0-829d-be744fa90225", + "tables": { + "ai_conversations": { + "name": "ai_conversations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_conversations_user": { + "name": "idx_ai_conversations_user", + "columns": [ + "user_id", + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_conversations_user_id_users_id_fk": { + "name": "ai_conversations_user_id_users_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_messages": { + "name": "ai_messages", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "tool_calls": { + "name": "tool_calls", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_messages_conversation": { + "name": "idx_ai_messages_conversation", + "columns": [ + "conversation_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_messages_conversation_id_ai_conversations_id_fk": { + "name": "ai_messages_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_messages", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_proposals": { + "name": "ai_proposals", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_proposals_user": { + "name": "idx_ai_proposals_user", + "columns": [ + "user_id", + "status" + ], + "isUnique": false + }, + "idx_ai_proposals_conversation": { + "name": "idx_ai_proposals_conversation", + "columns": [ + "conversation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_proposals_conversation_id_ai_conversations_id_fk": { + "name": "ai_proposals_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_proposals_user_id_users_id_fk": { + "name": "ai_proposals_user_id_users_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_providers": { + "name": "ai_providers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key_prefix": { + "name": "api_key_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_providers_user_label": { + "name": "idx_ai_providers_user_label", + "columns": [ + "user_id", + "label" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ai_providers_user_id_users_id_fk": { + "name": "ai_providers_user_id_users_id_fk", + "tableFrom": "ai_providers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_channels": { + "name": "automation_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_channels_pair": { + "name": "idx_automation_channels_pair", + "columns": [ + "automation_id", + "channel_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "automation_channels_automation_id_automations_id_fk": { + "name": "automation_channels_automation_id_automations_id_fk", + "tableFrom": "automation_channels", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_channels_channel_id_notification_channels_id_fk": { + "name": "automation_channels_channel_id_notification_channels_id_fk", + "tableFrom": "automation_channels", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_run_steps": { + "name": "automation_run_steps", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "run_id": { + "name": "run_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_index": { + "name": "step_index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_id": { + "name": "step_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step_type": { + "name": "step_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truncated": { + "name": "truncated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "idx_automation_run_steps_run": { + "name": "idx_automation_run_steps_run", + "columns": [ + "run_id", + "step_index" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_run_steps_run_id_automation_runs_id_fk": { + "name": "automation_run_steps_run_id_automation_runs_id_fk", + "tableFrom": "automation_run_steps", + "tableTo": "automation_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_runs": { + "name": "automation_runs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_context": { + "name": "trigger_context", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "finished_at": { + "name": "finished_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dry_run": { + "name": "dry_run", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_runs_automation": { + "name": "idx_automation_runs_automation", + "columns": [ + "automation_id", + "started_at" + ], + "isUnique": false + }, + "idx_automation_runs_user": { + "name": "idx_automation_runs_user", + "columns": [ + "user_id", + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_runs_automation_id_automations_id_fk": { + "name": "automation_runs_automation_id_automations_id_fk", + "tableFrom": "automation_runs", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_runs_user_id_users_id_fk": { + "name": "automation_runs_user_id_users_id_fk", + "tableFrom": "automation_runs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_schedules": { + "name": "automation_schedules", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_due_at": { + "name": "next_due_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_tick_at": { + "name": "last_tick_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_automation_schedules_automation": { + "name": "idx_automation_schedules_automation", + "columns": [ + "automation_id" + ], + "isUnique": true + }, + "idx_automation_schedules_due": { + "name": "idx_automation_schedules_due", + "columns": [ + "next_due_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automation_schedules_automation_id_automations_id_fk": { + "name": "automation_schedules_automation_id_automations_id_fk", + "tableFrom": "automation_schedules", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automation_trigger_state": { + "name": "automation_trigger_state", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "automation_id": { + "name": "automation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "state_key": { + "name": "state_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "breach_started_at": { + "name": "breach_started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_value": { + "name": "last_value", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_observed_state": { + "name": "last_observed_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_automation_trigger_state_key": { + "name": "idx_automation_trigger_state_key", + "columns": [ + "automation_id", + "state_key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "automation_trigger_state_automation_id_automations_id_fk": { + "name": "automation_trigger_state_automation_id_automations_id_fk", + "tableFrom": "automation_trigger_state", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "automations": { + "name": "automations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "definition": { + "name": "definition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "definition_version": { + "name": "definition_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'skip'" + }, + "max_run_seconds": { + "name": "max_run_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "dry_run": { + "name": "dry_run", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_automations_user": { + "name": "idx_automations_user", + "columns": [ + "user_id", + "enabled" + ], + "isUnique": false + } + }, + "foreignKeys": { + "automations_user_id_users_id_fk": { + "name": "automations_user_id_users_id_fk", + "tableFrom": "automations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_access": { + "name": "credential_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'use'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "folder_access": { + "name": "folder_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + "owner_user_id", + "folder" + ], + "isUnique": false + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_homepage_items_user_id": { + "name": "idx_homepage_items_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_web_ui": { + "name": "enable_web_ui", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_ai_assistant": { + "name": "enable_ai_assistant", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "web_ui_config": { + "name": "web_ui_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_install_counts": { + "name": "plugin_install_counts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registry_id": { + "name": "registry_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'aggregate-telemetry'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + "plugin_id", + "registry_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_migrations": { + "name": "plugin_migrations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "migration_id": { + "name": "migration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + "plugin_id", + "migration_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_permission_grants": { + "name": "plugin_permission_grants", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capability": { + "name": "capability", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'admin'" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + "plugin_id", + "capability" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_registries": { + "name": "plugin_registries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'community'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_settings": { + "name": "plugin_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted": { + "name": "encrypted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + "plugin_id", + "scope", + "scope_id", + "key" + ], + "isUnique": true + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + "plugin_id", + "scope" + ], + "isUnique": false + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_storage": { + "name": "plugin_storage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + "plugin_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugins": { + "name": "plugins", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'available'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'community'" + }, + "registry_id": { + "name": "registry_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'disabled'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + "registry_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "role_name": { + "name": "role_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + "role_name", + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rbac_known_permissions": { + "name": "rbac_known_permissions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "secret_sources": { + "name": "secret_sources", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'onepassword-connect'" + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_secret_sources_user": { + "name": "idx_secret_sources_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "secret_sources_user_id_users_id_fk": { + "name": "secret_sources_user_id_users_id_fk", + "tableFrom": "secret_sources", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "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": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_credential_secrets": { + "name": "shared_credential_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'password'" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + "credential_access_id", + "target_user_id" + ], + "isUnique": true + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + "target_user_id", + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_external_identities": { + "name": "user_external_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + "provider_id", + "subject" + ], + "isUnique": true + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_assistant_enabled": { + "name": "ai_assistant_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_read_only_commands": { + "name": "ai_read_only_commands", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_defaults": { + "name": "terminal_defaults", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_macros": { + "name": "terminal_macros", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_second_factors": { + "name": "user_second_factors", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "factor_id": { + "name": "factor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + "user_id", + "plugin_id", + "factor_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/_journal.json b/drizzle/sqlite/meta/_journal.json index 20745865d..292ba1482 100644 --- a/drizzle/sqlite/meta/_journal.json +++ b/drizzle/sqlite/meta/_journal.json @@ -295,6 +295,13 @@ "when": 1790266401908, "tag": "0041_nosy_mystique", "breakpoints": true + }, + { + "idx": 42, + "version": "6", + "when": 1790268465325, + "tag": "0042_many_old_lace", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/plugin-sdk/ARCHITECTURE.md b/packages/plugin-sdk/ARCHITECTURE.md index b67017eb0..b2ef44539 100644 --- a/packages/plugin-sdk/ARCHITECTURE.md +++ b/packages/plugin-sdk/ARCHITECTURE.md @@ -248,10 +248,18 @@ not merely unrouted: a terminal session must not outlive the plugin that owns it authenticated it, for a library that insists on owning its own WebSocketServer. guacamole-lite is why it exists. Three routes are public because they authenticate themselves in a way core cannot: the terminal (a share-link guest -arrives with a share token), the Docker console (per-message auth), and the +arrives with a share token), the Docker console (it refuses a socket without a signed-in user holding +`docker.use`, and re-checks the data key per message), and the Guacamole display (a single-use encrypted connection token in the query, minted by an authenticated route). +**B15** moved the Docker console off `ctx.ws.upgrade` and its own +`WebSocketServer` onto `ctx.ws.route` with `public` and `optionalAuth`: core +resolves the caller's token, the handler closes a socket without a user or +without `docker.use`, and it checks `isDataUnlocked()` on every connect +message. Nothing is left at module scope, so disable, enable while a console +is open closes it and the next enable serves new ones. + **B9** added `optionalAuth` for a public route: core still verifies a token when the client sent one and hands the handler that user, else an empty id. The terminal uses it, because one socket serves both signed-in users and @@ -348,7 +356,9 @@ plugin id in `src/ui` outside tests, apart from the entries in `scripts/shell-plugin-id-allowlist.json`, each with a reason (host data like the `docker` runtime name, which Phase B moves). **B14** removed the `rdp`, `vnc` and `telnet` entries: the shell learns those protocols from -`app.registerHostProtocol`. +`app.registerHostProtocol`. **B15** removed both `docker` entries: the Hosts +panel's Docker filter and the bulk Docker toggles went with the column, and +the host editor no longer carries a Proxmox default that spelled `docker`. #### The loader @@ -581,7 +591,10 @@ stopgap in the right place: D1 turns them into typed bridge members. **B16** added `LineChart`, `useAdaptivePolling`, `useAreaPreferences` (the Appearance density and chart options), and the homepage widget pieces `WidgetTitle` and `runVisibleInterval`, for host-metrics's cards and its -metrics chart widget. The frontend SDK gained `useHostStatus(hostId)`: core's +metrics chart widget. **B15** added `useUiPreferencesContext`, whose `setOverride` the +Docker manager uses to remember the card or table layout the user picked. +The `docker` and `hostMetrics` areas are still declared in core's +`types/ui-preferences.ts`; a plugin cannot contribute its own area yet. The frontend SDK gained `useHostStatus(hostId)`: core's status for a host, from the shell's own polling, or null outside the shell. **B14** added `buildOriginWsUrl`, `getBasePath`, `resolveRemoteHostId` and the `ConnectionStage` type, for remote desktop's display socket and its remote @@ -672,6 +685,17 @@ user. Recording files live under `ctx.files.dataDir()/session_logs//.cast`. Retention (`retentionDays`, an admin setting) runs a sweep at boot and every 24 hours. +**B15**'s docker plugin provides `docker.containers` (`listContainers`, +`action`) and `docker.events` (`subscribe(hostId, listener)`), both gated on +`docker.use`. The service name needs a dot, so the plain `docker` the step +was sketched with became `docker.containers`. `docker.events` polls each +subscribed host about once a minute as the subscribing user and diffs the +snapshots; a subscription is idempotent, which is how automations picks it +back up after docker is disabled and enabled again (it re-subscribes every +watched host each minute). Automations uses both as optional services: its +Docker step goes through `action`, and its `docker_event` trigger through +`subscribe`. + A service call runs a permission check for its actor, so a share-link guest, who has no user, cannot make one. Guest link resolution is therefore published on `ctx.registry` as `sessions.sharing.guests` instead: the token is @@ -802,6 +826,11 @@ missing service with an empty handle, like the runtime, instead of throwing. **B before), so `provide`/`consume`/`revoke` round-trip the way the real registry does. `plugins/workspaces/tests/backend/helpers.ts` is the worked example. +The doubles' `wsRoutes` (**B15**) record each route's `handler` (or the +`upgrade` callback) and `options`, so a test can hand a route a fake socket; +docker's `tests/backend/console.test.ts` opens a console, disposes the plugin +and activates it again that way. + `renderWithApp` also takes `api` (a stub for `app.api` and `usePluginApi()`), `layout` (what `app.tabs.getLayout()` returns) and `ready` (fire `app.tabs.onReady`). `app.tabs.applyLayout` is recorded in `shellCalls` and runs @@ -1366,8 +1395,14 @@ connect pipeline instead of importing ssh2 helpers from core: a chain has no single resolved host of its own. - `prepare(host, { client, purpose })`, `openTransport`, `classifyKeyboardInteractive` and `autoResponses` are the lower level for a - transport with its own prompt flow (docker's console, host metrics, **B6**'s - interactive file-manager connect with its TOTP/Warpgate parking flow). + transport with its own prompt flow (**B6**'s interactive file-manager + connect with its TOTP/Warpgate parking flow). +- **B15** showed the higher level covers a multi-request HTTP handshake too: + the Docker panel passes `connect` a `prompt` channel whose `ask` answers the + waiting request with what to ask (a code, a Warpgate sign-in, credentials) + and resolves when the next request (`connect-totp`, `connect-warpgate`) + brings the answer. A retry TOTP round re-asks instead of failing. docker + has no auth code of its own left. - `requiresSecret(authType)` and `supportsBackground(authType)` ask the provider. - **B6** added `"file-manager"` and `"file-transfer"` to `PluginSshPurpose` @@ -2050,8 +2085,8 @@ Then, with the app running: ## Legacy core imports: the debt D1 removes The bundled plugins predate the SDK, apart from workspaces (A9), snippets -(B2), remote-desktop (B14) and host-metrics (B16), which import nothing from -core. The others still reach core by relative +(B2), remote-desktop (B14), docker (B15) and host-metrics (B16), which import +nothing from core. The others still reach core by relative path (`../../../../src/backend/...`), which an esbuild plugin, `packages/plugin-sdk/cli/lib/legacy-core-imports.mjs`, keeps out of the bundle and rewrites to the compiled output path (`../../../backend/backend/...`, or @@ -2078,8 +2113,8 @@ What the lint fence enforces today, in `eslint.config.mjs`: | Core importing a plugin backend | **Error** | 0 | - | | A plugin backend importing frontend code or `@/` | **Error** | 0 | - | | The shell importing plugin code | **Error** | 0 | - | -| A plugin frontend importing core through `@/` | Warning | 84 files | D1 | -| A plugin importing core by relative path | Warning | 29 files | D1 | +| A plugin frontend importing core through `@/` | Warning | 70 files | D1 | +| A plugin importing core by relative path | Warning | 24 files | D1 | | A plugin importing another plugin's source | Warning | 1 file | B18 | A warning does not fail a build, so the counts are held by @@ -2119,8 +2154,10 @@ Known specifics: command history panel into ssh-terminal, which imports nothing from core. The file manager's terminal window renders the `terminal.view` component instead of importing the core terminal. -- Host data columns (`enableDocker` and so on) stay in core types until - Phase B moves them. Only UI branches on them left the shell. **B14** moved +- Host data columns (`enableFileManager` and so on) stay in core types until + Phase B moves them. Only UI branches on them left the shell. **B15** moved + docker's `enableDocker` and `docker_config` (now `containerRuntime`) into + its host settings. **B14** moved remote desktop's (`enableRdp`, `enableVnc`, `enableTelnet`, the three ports, `rdpSecurity`, `rdpIgnoreCert`, `guacamoleConfig`) into its host settings; the protocol logins stay core host fields. diff --git a/packages/plugin-sdk/FINISH-LIST.md b/packages/plugin-sdk/FINISH-LIST.md index 9fa377f31..e6c422921 100644 --- a/packages/plugin-sdk/FINISH-LIST.md +++ b/packages/plugin-sdk/FINISH-LIST.md @@ -80,7 +80,8 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite columns, so none of them can see a plugin's host settings. For tunnels the "Tunnel" filter and the bulk enable/disable entries were removed, and the export's "Tunnels" group became "Proxy" (SOCKS5 fields only) since - `tunnelConnections` there is now stale. Bringing these back needs a way for + `tunnelConnections` there is now stale. **B15** removed the "Docker" filter + and the bulk Docker toggles the same way. Bringing these back needs a way for core to read and write host-scope plugin settings generically (keyed off `contributes.settings.host` or `hostCapability`) rather than per column. Owner: D1. @@ -88,9 +89,9 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite `logActivity` is a core `@/main-axios` call and the plugin imports nothing from `@/`. Old "tunnel" entries still reopen the tab through the plugin's `activityTypes`. An `app.logActivity` (or equivalent bridge member) would - restore it for this plugin and let docker and file-manager drop their own - `@/main-axios` import for it too (host-metrics uses the `logActivity` the - ui entry exports). Owner: D1. + restore it for this plugin and let file-manager drop its own + `@/main-axios` import for it too (host-metrics and docker use the + `logActivity` the ui entry exports). Owner: D1. - **B7 (tunnels):** on the desktop app, tunnel statuses from a connected remote server are no longer merged into the tab: the old code polled the remote tunnel service through `getRemoteTunnelApi()`, and the SDK has no @@ -289,7 +290,8 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite Their drizzle migrations are `SELECT 1;` on purpose (the copy runs after drizzle on Postgres and MySQL). Drop them physically in 3.0.0, once every install has booted 2.9.0. The legacy `guac_enabled` and `guac_url` rows in - `settings` are left too. Owner: 3.0.0. + `settings` are left too. **B15** did the same with `ssh_data.enable_docker` + and `docker_config` (copied by `docker-settings-migration.ts`). Owner: 3.0.0. - **B14, found in B5 (proxmox):** `drizzle/postgres/0036_redundant_ender_wiggin.sql` and `drizzle/mysql/0035_clumsy_leech.sql` really `DROP COLUMN` the four proxmox host columns. On Postgres and MySQL that runs at boot before @@ -371,6 +373,31 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite gets "tunnels.start is not a function". Same cause as the tunnels line above: since B12 `ctx.services.get` hands back an empty handle instead of throwing, so the step has to check `typeof start`. Owner: B17 or D0. +- **B15 (docker), for D2:** the console socket `/plugin-ws/docker/console` + is a public route with `optionalAuth`. The handler refuses a socket with no + signed-in user or without `docker.use` and checks the data key per connect + message, but review it with the rest of the public routes. +- **B15 (docker):** `ssh_data.show_docker_in_sidebar` (and its siblings for + terminal, files, tunnels and server stats) are dead columns nothing reads; + only the raw database export still copies them. Drop them the B14 way. + Owner: D0. +- **B15 (docker):** the Docker manager's card or table layout is still an + override on core's Appearance presets (`types/ui-preferences.ts` declares a + `docker` area, as it does `hostMetrics`), reached through the ui entry's + `useAreaPreferences` and `useUiPreferencesContext`. A plugin cannot declare + its own area yet. Owner: D1. +- **B15 (docker):** the raw SQLite database import (`database.ts`) no longer + brings `enable_docker` across, the same gap every earlier column move left + there: it writes host rows only and never plugin settings. Owner: D1, with + the generic host-settings export and import. +- **B15 (docker), for B18:** the AI assistant's `list_hosts`/`get_host` + tools still report `enableDocker` from the host row, which no longer has + the column, so it always reads null. Read `pluginSettings.docker` or ask the + `docker.containers` service instead. Owner: B18. +- **B15 (docker), for B17:** automations' `docker-watcher.ts` still reads its + own rules through core's automation repository and types by relative + import; only the Docker side is on the SDK (`docker.containers` for the + step, `docker.events` for the trigger). Owner: B17. ## Manual checks after 2.9.0 @@ -411,3 +438,9 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite that only had `connection_type = rdp` comes up as RDP with SSH off; RDP user defaults apply to a saved host; Quick Connect over RDP and VNC; the desktop app with a host on the remote server and one on This device. +- Docker: a 2.8 host with Docker on (and one on Podman) keeps its switch and + runtime; list, start, stop, logs, stats and the console on a Linux host; + TOTP (including a wrong code first) and Warpgate on the connect; a host + with no stored password asks for credentials; the homepage Docker widget; + an automation Docker step and a `docker_event` trigger; disable Docker with + a console open, then enable it and open another. diff --git a/packages/plugin-sdk/src/backend.ts b/packages/plugin-sdk/src/backend.ts index a6a5f3d8e..87c539a78 100644 --- a/packages/plugin-sdk/src/backend.ts +++ b/packages/plugin-sdk/src/backend.ts @@ -487,7 +487,6 @@ export interface PluginHostRecord { enableTerminal?: boolean | null; enableFileManager?: boolean | null; enableTunnel?: boolean | null; - enableDocker?: boolean | null; createdAt?: string | null; updatedAt?: string | null; [key: string]: unknown; diff --git a/packages/plugin-sdk/src/testing.ts b/packages/plugin-sdk/src/testing.ts index a898a0f13..130485af0 100644 --- a/packages/plugin-sdk/src/testing.ts +++ b/packages/plugin-sdk/src/testing.ts @@ -17,7 +17,24 @@ import type { import type { TermixApp } from "./frontend.js"; import type { PluginManifest } from "./manifest.js"; import type { PluginTableDefinition } from "./db.js"; -import type { PluginMiddleware, PluginRouterOptions } from "./backend.js"; +import type { + PluginMiddleware, + PluginRouterOptions, + PluginWebSocketHandler, + PluginWebSockets, + PluginWebSocketOptions, +} from "./backend.js"; + +/** A ctx.ws registration as the test doubles record it. */ +export interface FakeWsRoute { + path: string; + raw: boolean; + /** For ctx.ws.route. */ + handler?: PluginWebSocketHandler; + /** For ctx.ws.upgrade. */ + upgrade?: Parameters[1]; + options?: PluginWebSocketOptions; +} import type { PluginLoginMethod, PluginSecondFactor, @@ -115,8 +132,11 @@ export interface FakePluginContext { syncEntities: SyncEntityRegistration[]; /** Tombstones recorded through ctx.sync.recordTombstone, in order. */ tombstones: Array<{ userId: string; entityType: string; syncId: string }>; - /** WebSocket routes registered through ctx.ws, in order. */ - wsRoutes: Array<{ path: string; raw: boolean }>; + /** + * WebSocket routes registered through ctx.ws, in order, with the handler + * and options, so a test can hand a route a fake socket. + */ + wsRoutes: FakeWsRoute[]; /** Router options passed to ctx.http.router, in order. */ httpRouters: Array; /** Backing store behind ctx.settings, keyed "::". */ @@ -237,7 +257,7 @@ export function createFakeContext( entityType: string; syncId: string; }> = []; - const wsRoutes: Array<{ path: string; raw: boolean }> = []; + const wsRoutes: FakeWsRoute[] = []; const httpRouters: Array = []; const listeners = new Map void>>(); const settings = new Map(); @@ -466,11 +486,16 @@ export function createFakeContext( }, ws: { - route: (path) => { - wsRoutes.push({ path, raw: false }); + route: (path, handler, wsOptions) => { + wsRoutes.push({ path, raw: false, handler, options: wsOptions }); }, - upgrade: (path) => { - wsRoutes.push({ path, raw: true }); + upgrade: (path, handler, wsOptions) => { + wsRoutes.push({ + path, + raw: true, + upgrade: handler, + options: wsOptions, + }); }, }, diff --git a/plugins/automations/manifest.json b/plugins/automations/manifest.json index 1ac744966..c891e4767 100644 --- a/plugins/automations/manifest.json +++ b/plugins/automations/manifest.json @@ -29,7 +29,8 @@ "optionalDependencies": { "snippets": "^1.0.0", "fleets": "^1.0.0", - "tunnels": "^1.0.0" + "tunnels": "^1.0.0", + "docker": "^1.0.0" }, "requires": [ { @@ -46,6 +47,16 @@ "service": "tunnels.access", "versionRange": "^1.0.0", "optional": true + }, + { + "service": "docker.containers", + "versionRange": "^1.0.0", + "optional": true + }, + { + "service": "docker.events", + "versionRange": "^1.0.0", + "optional": true } ], "contributes": { diff --git a/plugins/automations/src/backend/actions/index.ts b/plugins/automations/src/backend/actions/index.ts index 0dd14faa1..32d011a29 100644 --- a/plugins/automations/src/backend/actions/index.ts +++ b/plugins/automations/src/backend/actions/index.ts @@ -2,14 +2,11 @@ import { DEFAULT_STEP_TIMEOUT_MS, type Step, } from "../../../../../src/types/automations.js"; -import { - execCommand, - execElevated, - shellSingleQuote, -} from "@termix/plugin-sdk/host-commands"; +import { execCommand, execElevated } from "@termix/plugin-sdk/host-commands"; import { withSshConnection } from "../ssh.js"; import { createCurrentNotificationChannelRepository } from "../../../../../src/backend/database/repositories/factory.js"; import { getSnippet, resolveSnippetCommandFor } from "../snippets.js"; +import { runDockerAction } from "../docker.js"; import { runTunnelAction } from "../tunnels.js"; import { sendAutomationNotification } from "../notify.js"; import { automationFetch } from "../http.js"; @@ -227,11 +224,15 @@ async function runDocker( }; } - // The Docker HTTP routes are tied to an interactive session, so run the - // equivalent command over the same pooled SSH connection everything else - // uses. The container name is quoted because it comes from a template. - const command = `docker ${step.action} ${shellSingleQuote(container)}`; - return execOnHost(target.host, command, false, context, step.timeoutMs); + const result = await runDockerAction( + context.userId, + target.id, + container, + step.action, + ); + return result.ok + ? { output: `Container ${container}: ${step.action} done` } + : { output: "", error: result.error }; }); } diff --git a/plugins/automations/src/backend/docker-watcher.ts b/plugins/automations/src/backend/docker-watcher.ts index 92a26d2fa..f85366966 100644 --- a/plugins/automations/src/backend/docker-watcher.ts +++ b/plugins/automations/src/backend/docker-watcher.ts @@ -1,41 +1,29 @@ import type { AutomationDefinition } from "../../../../src/types/automations.js"; import { createCurrentAutomationRepository } from "../../../../src/backend/database/repositories/factory.js"; -import { resolveHostById } from "../../../../src/backend/hosts/host-resolver.js"; -import { DataCrypto } from "../../../../src/backend/utils/data-crypto.js"; -import { execCommand } from "@termix/plugin-sdk/host-commands"; -import { withSshConnection } from "./ssh.js"; -import { statsLogger } from "../../../../src/backend/utils/logger.js"; +import { subscribeDockerEvents, type DockerEvent } from "./docker.js"; import { onDockerEvent } from "./triggers.js"; /** - * Container state polling for docker_event triggers. + * Keeps a docker.events subscription for every host a docker_event trigger + * names, as the automation's owner. The docker plugin does the polling; this + * only follows which hosts are watched. * - * Everything else Docker-related in the backend hangs off an interactive - * session that only exists while somebody has the UI open, so a trigger built - * on it would only ever fire while being watched. This polls over the same - * pooled SSH connection the other automation steps use, and only for hosts a - * docker_event trigger actually names, so an install with no such automation - * does no extra work at all. - * - * Events are derived by diffing successive snapshots: the poll interval is the - * resolution, so a container that stops and starts between two polls is not - * reported. That is the tradeoff for not holding a `docker events` stream open - * against every host. + * Subscribing is idempotent on the docker side, so every watched host is + * subscribed again once a minute. That is what picks the subscriptions back + * up after the docker plugin is disabled and enabled again. */ -const POLL_INTERVAL_MS = 60_000; -const EXEC_TIMEOUT_MS = 15_000; +const RESUBSCRIBE_MS = 60_000; -interface ContainerState { - /** Docker's own state word: running, exited, restarting, ... */ - state: string; - /** Health from the status text, when the image declares a healthcheck. */ - unhealthy: boolean; +interface Watch { + listener: (event: DockerEvent) => void; + unsubscribe: (() => void) | null; + subscribedAt: number; } -/** Last snapshot per host, so transitions can be spotted. */ -const snapshots = new Map>(); -const lastPolledAt = new Map(); +const watches = new Map(); + +const keyOf = (hostId: number, userId: string) => `${userId}:${hostId}`; /** Hosts named by an enabled docker_event trigger, with the owning user. */ export async function listDockerWatchedHosts(): Promise> { @@ -60,8 +48,8 @@ export async function listDockerWatchedHosts(): Promise> { } else if (selector?.kind === "hosts") { for (const hostId of selector.hostIds) watched.set(hostId, row.userId); } - // Fleet and "all" selectors are deliberately not expanded: polling every - // host a user owns for container state is far too costly to do blindly. + // Fleet and "all" selectors are deliberately not expanded: watching + // every host a user owns for container state is far too costly. } } catch { return watched; @@ -70,165 +58,58 @@ export async function listDockerWatchedHosts(): Promise> { return watched; } -/** - * Parses `docker ps -a` output. One JSON object per line, matching the format - * string the container routes use. - */ -export function parseContainerStates( - output: string, -): Map { - const states = new Map(); - - for (const line of output.split("\n")) { - const trimmed = line.trim(); - if (!trimmed) continue; - - try { - const parsed = JSON.parse(trimmed) as { - name?: string; - state?: string; - status?: string; - }; - if (!parsed.name) continue; - - states.set(parsed.name, { - state: (parsed.state ?? "").toLowerCase(), - unhealthy: /\(unhealthy\)/i.test(parsed.status ?? ""), - }); - } catch { - // A partial line is not worth failing the whole poll over. - } - } - - return states; -} - -/** - * Works out which events a pair of snapshots implies. - * - * A container missing from the previous snapshot is treated as newly seen - * rather than started, so the first poll after a restart does not replay every - * running container as a fresh start event. - */ -export function diffContainerStates( - previous: Map, - current: Map, -): Array<{ container: string; event: DockerEventName }> { - const events: Array<{ container: string; event: DockerEventName }> = []; - - for (const [name, now] of current) { - const before = previous.get(name); - if (!before) continue; - - if (before.state !== now.state) { - if (now.state === "exited") - events.push({ container: name, event: "exited" }); - else if (now.state === "running") - events.push({ container: name, event: "started" }); - else if (now.state === "restarting") - events.push({ container: name, event: "restarting" }); - } - - // Health is independent of state: a container can go unhealthy while it - // stays up, which is exactly the case worth alerting on. - if (!before.unhealthy && now.unhealthy) { - events.push({ container: name, event: "unhealthy" }); - } - } - - return events; -} - -export type DockerEventName = "exited" | "started" | "unhealthy" | "restarting"; - -const PS_FORMAT = `'{"name":"{{.Names}}","state":"{{.State}}","status":"{{.Status}}"}'`; - -async function pollHost(hostId: number, userId: string): Promise { - const host = await resolveHostById(hostId, userId); - if (!host) { - snapshots.delete(hostId); - return; - } - - const result = await withSshConnection( - host as never as never, - { pool: "fleet", purpose: "fleet" }, - (client) => - execCommand( - client, - `docker ps -a --format ${PS_FORMAT}`, - EXEC_TIMEOUT_MS, - ), +/** Brings the subscriptions in line with the watched hosts. */ +export async function reconcileDockerWatch( + now: number = Date.now(), + watched?: Map, +): Promise { + const wanted = watched ?? (await listDockerWatchedHosts()); + const wantedKeys = new Set( + [...wanted].map(([hostId, userId]) => keyOf(hostId, userId)), ); - if (result.code !== 0 && result.code !== null) { - // Docker missing or not permitted on this host. Drop the snapshot so a - // later success is treated as a first observation rather than a diff. - snapshots.delete(hostId); - return; + for (const [key, watch] of watches) { + if (wantedKeys.has(key)) continue; + watch.unsubscribe?.(); + watches.delete(key); } - const current = parseContainerStates(result.stdout); - const previous = snapshots.get(hostId); - snapshots.set(hostId, current); - - if (!previous) return; - - for (const { container, event } of diffContainerStates(previous, current)) { - await onDockerEvent({ - hostId, - ownerUserId: userId, - container, - event, - }).catch(() => undefined); - } -} - -/** - * Polls every watched host whose interval has elapsed. Called from the - * automation scheduler tick rather than owning a timer of its own. - */ -export async function pollDockerEvents( - now: number = Date.now(), -): Promise { - const watched = await listDockerWatchedHosts(); - - for (const hostId of [...snapshots.keys()]) { - if (!watched.has(hostId)) { - snapshots.delete(hostId); - lastPolledAt.delete(hostId); + for (const [hostId, userId] of wanted) { + const key = keyOf(hostId, userId); + let watch = watches.get(key); + if (!watch) { + watch = { + listener: (event) => { + void onDockerEvent({ + hostId: event.hostId, + ownerUserId: userId, + container: event.container, + event: event.event, + }).catch(() => undefined); + }, + unsubscribe: null, + subscribedAt: 0, + }; + watches.set(key, watch); + } + if (watch.subscribedAt && now - watch.subscribedAt < RESUBSCRIBE_MS) { + continue; } - } - - for (const [hostId, userId] of watched) { - const last = lastPolledAt.get(hostId) ?? 0; - if (now - last < POLL_INTERVAL_MS) continue; - // Host credentials cannot be decrypted while the owner's key is locked. - if (!canAccess(userId)) continue; - lastPolledAt.set(hostId, now); - try { - await pollHost(hostId, userId); - } catch (error) { - statsLogger.warn("Docker event poll failed", { - operation: "automation_docker_poll_error", + watch.unsubscribe = await subscribeDockerEvents( + userId, hostId, - error: error instanceof Error ? error.message : String(error), - }); + watch.listener, + ); + watch.subscribedAt = watch.unsubscribe ? now : 0; + } catch { + watch.subscribedAt = 0; } } } -function canAccess(userId: string): boolean { - try { - return DataCrypto.canUserAccessData(userId); - } catch { - return false; - } -} - -/** Clears cached state, for shutdown and tests. */ +/** Drops every subscription, for shutdown and tests. */ export function resetDockerWatcher(): void { - snapshots.clear(); - lastPolledAt.clear(); + for (const watch of watches.values()) watch.unsubscribe?.(); + watches.clear(); } diff --git a/plugins/automations/src/backend/docker.ts b/plugins/automations/src/backend/docker.ts new file mode 100644 index 000000000..41d168c32 --- /dev/null +++ b/plugins/automations/src/backend/docker.ts @@ -0,0 +1,85 @@ +import type { PluginServices } from "@termix/plugin-sdk/backend"; + +let current: PluginServices | null = null; + +/** Set in activate, cleared on deactivate. */ +export function setDockerServices(services: PluginServices | null): void { + current = services; +} + +export type DockerEventName = "exited" | "started" | "unhealthy" | "restarting"; + +export interface DockerEvent { + hostId: number; + container: string; + event: DockerEventName; +} + +/** The docker plugin's "docker.containers" service, version 1. */ +interface DockerService { + action: ( + hostId: number, + container: string, + action: "start" | "stop" | "restart", + ) => Promise; +} + +/** The docker plugin's "docker.events" service, version 1. */ +interface DockerEventsService { + subscribe: ( + hostId: number, + listener: (event: DockerEvent) => void, + ) => Promise<() => void>; +} + +const UNAVAILABLE = "The docker plugin is not available"; + +function service( + name: string, + userId: string, +): Partial | null { + try { + return current?.get(name, { userId }) ?? null; + } catch { + return null; + } +} + +/** + * Starts, stops or restarts a container as userId. Optional: without the + * docker plugin the step fails with a clear message. + */ +export async function runDockerAction( + userId: string, + hostId: number, + container: string, + action: "start" | "stop" | "restart", +): Promise<{ ok: boolean; unavailable?: boolean; error?: string }> { + const docker = service("docker.containers", userId); + if (!docker || typeof docker.action !== "function") { + return { ok: false, unavailable: true, error: UNAVAILABLE }; + } + try { + await docker.action(hostId, container, action); + return { ok: true }; + } catch (error) { + return { + ok: false, + error: error instanceof Error ? error.message : String(error), + }; + } +} + +/** + * Subscribes `listener` to container changes on a host as userId, or + * resolves null while the docker plugin is off. + */ +export async function subscribeDockerEvents( + userId: string, + hostId: number, + listener: (event: DockerEvent) => void, +): Promise<(() => void) | null> { + const events = service("docker.events", userId); + if (!events || typeof events.subscribe !== "function") return null; + return events.subscribe(hostId, listener); +} diff --git a/plugins/automations/src/backend/index.ts b/plugins/automations/src/backend/index.ts index cd827e220..f14725a5e 100644 --- a/plugins/automations/src/backend/index.ts +++ b/plugins/automations/src/backend/index.ts @@ -2,6 +2,7 @@ import type { PluginContext } from "@termix/plugin-sdk/backend"; import { setPluginSsh } from "./ssh.js"; import { setPluginServices } from "./snippets.js"; import { setTunnelServices } from "./tunnels.js"; +import { setDockerServices } from "./docker.js"; import { startAutomationsService, stopAutomationsService } from "./routes.js"; import { startAutomationScheduler, @@ -23,6 +24,8 @@ export async function activate(ctx: PluginContext) { ctx.disposables.add(() => setPluginServices(null)); setTunnelServices(ctx.services); ctx.disposables.add(() => setTunnelServices(null)); + setDockerServices(ctx.services); + ctx.disposables.add(() => setDockerServices(null)); // Nothing arrives here while the tunnels plugin is off, which is the whole // of the optional dependency. diff --git a/plugins/automations/src/backend/scheduler.ts b/plugins/automations/src/backend/scheduler.ts index a5ffa821e..5cb3a618c 100644 --- a/plugins/automations/src/backend/scheduler.ts +++ b/plugins/automations/src/backend/scheduler.ts @@ -4,7 +4,7 @@ import { DataCrypto } from "../../../../src/backend/utils/data-crypto.js"; import { statsLogger } from "../../../../src/backend/utils/logger.js"; import { computeNextDueAt } from "./cron.js"; import { hasDwelled, isCoolingDown } from "./conditions.js"; -import { pollDockerEvents } from "./docker-watcher.js"; +import { reconcileDockerWatch, resetDockerWatcher } from "./docker-watcher.js"; import { AutomationEngine } from "./engine.js"; import { reconcileHeadlessViewers } from "./headless-viewer.js"; import { @@ -57,6 +57,7 @@ export function stopAutomationScheduler(): void { tickTimer = null; startupTimer = null; unsubscribeAutomationTriggers(); + resetDockerWatcher(); } /** Exposed for tests; the interval calls this. */ @@ -69,7 +70,7 @@ export async function tick(now: Date = new Date()): Promise { await reconcileHeadlessViewers().catch(() => undefined); await runDueSchedules(now); await recheckOpenBreaches(now); - await pollDockerEvents(now.getTime()).catch(() => undefined); + await reconcileDockerWatch(now.getTime()).catch(() => undefined); await pruneIfDue(now); } catch (error) { statsLogger.warn("Automation scheduler tick failed", { diff --git a/plugins/automations/tests/backend/docker-watcher.test.ts b/plugins/automations/tests/backend/docker-watcher.test.ts index 75ca1357a..401471c81 100644 --- a/plugins/automations/tests/backend/docker-watcher.test.ts +++ b/plugins/automations/tests/backend/docker-watcher.test.ts @@ -1,108 +1,131 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { PluginServices } from "@termix/plugin-sdk/backend"; + +const fired = vi.hoisted(() => [] as unknown[]); + +vi.mock("../../src/backend/triggers.js", () => ({ + onDockerEvent: async (event: unknown) => { + fired.push(event); + }, +})); + +vi.mock("../../../../src/backend/database/repositories/factory.js", () => ({ + createCurrentAutomationRepository: () => ({ listAllEnabled: async () => [] }), +})); + import { - diffContainerStates, - parseContainerStates, + reconcileDockerWatch, + resetDockerWatcher, } from "../../src/backend/docker-watcher.js"; +import { + setDockerServices, + runDockerAction, +} from "../../src/backend/docker.js"; -/** - * The polling side needs SSH, so what is tested here is the pure part: turning - * `docker ps` output into states, and turning two snapshots into events. - */ +type Listener = (event: { + hostId: number; + container: string; + event: string; +}) => void; -describe("parseContainerStates", () => { - it("reads name, state and health out of the ps output", () => { - const output = [ - '{"name":"web","state":"running","status":"Up 2 hours"}', - '{"name":"db","state":"exited","status":"Exited (0) 5 minutes ago"}', - '{"name":"api","state":"running","status":"Up 1 hour (unhealthy)"}', - ].join("\n"); +/** A docker.events provider that records who subscribed to what. */ +function fakeEvents() { + const subscriptions = new Map>(); + const calls: Array<{ userId?: string; hostId: number }> = []; + const services = { + provide: () => {}, + providers: () => [], + get: (name: string, options?: { userId?: string }) => { + if (name !== "docker.events") return {}; + return { + subscribe: async (hostId: number, listener: Listener) => { + calls.push({ userId: options?.userId, hostId }); + const key = `${options?.userId}:${hostId}`; + const set = subscriptions.get(key) ?? new Set(); + set.add(listener); + subscriptions.set(key, set); + return () => set.delete(listener); + }, + }; + }, + } as unknown as PluginServices; + const emit = (key: string, event: Parameters[0]) => { + for (const listener of subscriptions.get(key) ?? []) listener(event); + }; + return { services, subscriptions, calls, emit }; +} - const states = parseContainerStates(output); - - expect(states.get("web")).toEqual({ state: "running", unhealthy: false }); - expect(states.get("db")).toEqual({ state: "exited", unhealthy: false }); - expect(states.get("api")).toEqual({ state: "running", unhealthy: true }); - }); - - it("skips blank and malformed lines rather than failing the poll", () => { - const output = [ - '{"name":"web","state":"running","status":"Up"}', - "", - "not json at all", - '{"state":"running"}', - ].join("\n"); - - const states = parseContainerStates(output); - expect([...states.keys()]).toEqual(["web"]); - }); - - it("returns nothing for empty output", () => { - expect(parseContainerStates("").size).toBe(0); - }); +beforeEach(() => { + fired.length = 0; }); -describe("diffContainerStates", () => { - const running = { state: "running", unhealthy: false }; - const exited = { state: "exited", unhealthy: false }; +afterEach(() => { + resetDockerWatcher(); + setDockerServices(null); +}); - it("reports a container that stopped", () => { - const events = diffContainerStates( - new Map([["web", running]]), - new Map([["web", exited]]), - ); - expect(events).toEqual([{ container: "web", event: "exited" }]); +describe("docker watcher", () => { + it("subscribes each watched host as its owner and fires the trigger", async () => { + const events = fakeEvents(); + setDockerServices(events.services); + + await reconcileDockerWatch(0, new Map([[7, "user-1"]])); + expect(events.calls).toEqual([{ userId: "user-1", hostId: 7 }]); + + events.emit("user-1:7", { hostId: 7, container: "web", event: "exited" }); + await Promise.resolve(); + expect(fired).toEqual([ + { hostId: 7, ownerUserId: "user-1", container: "web", event: "exited" }, + ]); }); - it("reports a container that started", () => { - const events = diffContainerStates( - new Map([["web", exited]]), - new Map([["web", running]]), - ); - expect(events).toEqual([{ container: "web", event: "started" }]); + it("re-subscribes once a minute and drops hosts no longer watched", async () => { + const events = fakeEvents(); + setDockerServices(events.services); + const watched = new Map([[7, "user-1"]]); + + await reconcileDockerWatch(1_000, watched); + await reconcileDockerWatch(30_000, watched); + expect(events.calls).toHaveLength(1); + await reconcileDockerWatch(62_000, watched); + expect(events.calls).toHaveLength(2); + expect(events.subscriptions.get("user-1:7")?.size).toBe(1); + + await reconcileDockerWatch(63_000, new Map()); + expect(events.subscriptions.get("user-1:7")?.size).toBe(0); }); - it("reports a container that began restarting", () => { - const events = diffContainerStates( - new Map([["web", running]]), - new Map([["web", { state: "restarting", unhealthy: false }]]), - ); - expect(events).toEqual([{ container: "web", event: "restarting" }]); + it("does nothing while the docker plugin is off, then picks it up", async () => { + setDockerServices({ + get: () => ({}), + } as unknown as PluginServices); + await expect( + reconcileDockerWatch(0, new Map([[7, "user-1"]])), + ).resolves.toBeUndefined(); + + const events = fakeEvents(); + setDockerServices(events.services); + await reconcileDockerWatch(1, new Map([[7, "user-1"]])); + expect(events.calls).toHaveLength(1); }); - it("reports a container that went unhealthy while still running", () => { - const events = diffContainerStates( - new Map([["web", running]]), - new Map([["web", { state: "running", unhealthy: true }]]), - ); - expect(events).toEqual([{ container: "web", event: "unhealthy" }]); + it("fails a docker step cleanly while the plugin is off", async () => { + setDockerServices({ get: () => ({}) } as unknown as PluginServices); + expect(await runDockerAction("user-1", 7, "web", "restart")).toEqual({ + ok: false, + unavailable: true, + error: "The docker plugin is not available", + }); }); - it("stays quiet when nothing changed", () => { - const events = diffContainerStates( - new Map([["web", running]]), - new Map([["web", running]]), - ); - expect(events).toEqual([]); - }); - - it("does not re-announce a container that is still unhealthy", () => { - const unhealthy = { state: "running", unhealthy: true }; - const events = diffContainerStates( - new Map([["web", unhealthy]]), - new Map([["web", unhealthy]]), - ); - expect(events).toEqual([]); - }); - - // A first sighting is a baseline, not an event: otherwise every container - // running at boot would report itself as freshly started. - it("treats a newly seen container as a baseline", () => { - const events = diffContainerStates(new Map(), new Map([["web", running]])); - expect(events).toEqual([]); - }); - - it("ignores a container that disappeared", () => { - const events = diffContainerStates(new Map([["web", running]]), new Map()); - expect(events).toEqual([]); + it("runs a docker step through the docker service", async () => { + const action = vi.fn(async () => {}); + setDockerServices({ + get: (name: string) => (name === "docker.containers" ? { action } : {}), + } as unknown as PluginServices); + expect(await runDockerAction("user-1", 7, "web", "restart")).toEqual({ + ok: true, + }); + expect(action).toHaveBeenCalledWith(7, "web", "restart"); }); }); diff --git a/plugins/docker/CHANGELOG.md b/plugins/docker/CHANGELOG.md index 6a1bc3a37..bdb38094e 100644 --- a/plugins/docker/CHANGELOG.md +++ b/plugins/docker/CHANGELOG.md @@ -3,3 +3,20 @@ ## 1.0.0 Bundled with Termix 2.9.0. + +- Runs entirely through the plugin SDK: no imports from Termix core. +- Connects through Termix's one SSH pipeline, so TOTP, Warpgate, jump hosts + and proxies behave the same as in the terminal. A wrong TOTP code asks + again instead of ending the connect. +- Each host's Docker switch and container runtime are this plugin's host + settings now, moved over on upgrade. The host editor shows them in the + Plugins tab. +- The API lives at `/plugin-api/docker/` and the console at + `/plugin-ws/docker/console`. Disabling the plugin closes open consoles and + enabling it again works without a restart. +- New `docker.use` permission, given to the admin and user roles. +- Provides `docker.containers` (list containers, start, stop and the rest) + and `docker.events` (container state changes) for other plugins, which + Automations uses for its Docker step and trigger. +- The Docker homepage widget comes from this plugin. +- Downloaded logs are the plain log text. diff --git a/plugins/docker/README.md b/plugins/docker/README.md index 02ce4bc7d..aef9c0d36 100644 --- a/plugins/docker/README.md +++ b/plugins/docker/README.md @@ -4,6 +4,18 @@ Container management over SSH: list, inspect, start, stop and view logs for Dock Bundled with Termix and enabled by default. +## Services + +- `docker.containers` 1.0.0: `listContainers(hostId, { all? })` and + `action(hostId, container, "start" | "stop" | "restart" | "pause" | +"unpause" | "remove")`, run over a pooled SSH connection as the caller. +- `docker.events` 1.0.0: `subscribe(hostId, listener)` calls the listener + with `{ hostId, container, event }` when a container exits, starts, + restarts or turns unhealthy, polled about once a minute as the caller. + Subscribing the same listener again is a no-op. + +Both need `docker.use`. + ## Layout manifest.json id, capabilities and what this plugin contributes diff --git a/plugins/docker/locales/en.json b/plugins/docker/locales/en.json index 20d720bea..279051a95 100644 --- a/plugins/docker/locales/en.json +++ b/plugins/docker/locales/en.json @@ -100,26 +100,37 @@ "stats": "Stats", "consoleTab": "Console", "startContainerToAccess": "Start the container to access the console", - "noHostSelected": "No host selected" + "noHostSelected": "No host selected", + "failedToFetchLogs": "Failed to fetch logs: {{error}}", + "failedToDownloadLogs": "Failed to download logs: {{error}}" }, "nav": { "docker": "Docker" }, - "hosts": { - "tabDocker": "Docker", - "dockerIntegration": "Docker Integration", - "enableDockerMonitor": "Enable Docker", - "enableDockerMonitorDesc": "Monitor and manage containers on this host via Docker", - "containerRuntime": "Container Runtime", - "containerRuntimeDesc": "Choose the CLI used for container management on this host", - "containerRuntimeDocker": "Docker", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "Docker", "feature_docker_desc": "See containers, read logs and watch stats on any host running Docker." }, "terminal": { "retry": "Retry" + }, + "permissions": { + "use": { + "title": "Use Docker", + "description": "Open the Docker manager and container consoles on hosts with Docker enabled." + } + }, + "settings": { + "host": { + "enable": { + "label": "Enable Docker" + }, + "containerRuntime": { + "label": "Container Runtime", + "description": "Choose the CLI used for container management on this host", + "docker": "Docker", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/af_ZA.json b/plugins/docker/locales/translated/af_ZA.json index 7e8ee775a..0fc94b13b 100644 --- a/plugins/docker/locales/translated/af_ZA.json +++ b/plugins/docker/locales/translated/af_ZA.json @@ -99,18 +99,21 @@ "nav": { "docker": "Docker" }, - "hosts": { - "tabDocker": "Docker", - "dockerIntegration": "Docker-integrasie", - "enableDockerMonitor": "Aktiveer Docker", - "enableDockerMonitorDesc": "Monitor en bestuur houers op hierdie gasheer via Docker", - "containerRuntime": "Houerlooptyd", - "containerRuntimeDesc": "Kies die CLI wat vir houerbestuur op hierdie gasheer gebruik word", - "containerRuntimeDocker": "Docker", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "Docker", "feature_docker_desc": "Sien houers, lees logs en kyk na statistieke op enige gasheer wat Docker gebruik." + }, + "settings": { + "host": { + "enable": { + "label": "Aktiveer Docker" + }, + "containerRuntime": { + "label": "Houerlooptyd", + "description": "Kies die CLI wat vir houerbestuur op hierdie gasheer gebruik word", + "docker": "Docker", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/ar_SA.json b/plugins/docker/locales/translated/ar_SA.json index a6d11fee5..987421790 100644 --- a/plugins/docker/locales/translated/ar_SA.json +++ b/plugins/docker/locales/translated/ar_SA.json @@ -99,18 +99,21 @@ "nav": { "docker": "Docker" }, - "hosts": { - "tabDocker": "Docker", - "dockerIntegration": "تكامل Docker", - "enableDockerMonitor": "تمكين Docker", - "enableDockerMonitorDesc": "مراقبة وإدارة الحاويات على هذا المضيف عبر Docker", - "containerRuntime": "بيئة تشغيل الحاويات", - "containerRuntimeDesc": "اختر واجهة سطر الأوامر المستخدمة لإدارة الحاويات على هذا المضيف", - "containerRuntimeDocker": "Docker", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "عامل ميناء", "feature_docker_desc": "اطلع على الحاويات، واقرأ السجلات، وراقب الإحصائيات على أي مضيف يعمل بنظام Docker." + }, + "settings": { + "host": { + "enable": { + "label": "تمكين Docker" + }, + "containerRuntime": { + "label": "بيئة تشغيل الحاويات", + "description": "اختر واجهة سطر الأوامر المستخدمة لإدارة الحاويات على هذا المضيف", + "docker": "Docker", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/bg_BG.json b/plugins/docker/locales/translated/bg_BG.json index a06fdfbf7..e5d7a69d8 100644 --- a/plugins/docker/locales/translated/bg_BG.json +++ b/plugins/docker/locales/translated/bg_BG.json @@ -99,18 +99,21 @@ "nav": { "docker": "Докер" }, - "hosts": { - "tabDocker": "Докер", - "dockerIntegration": "Интеграция на Docker", - "enableDockerMonitor": "Активиране на Докер", - "enableDockerMonitorDesc": "Наблюдавайте и управлявайте контейнери на този хост чрез Docker", - "containerRuntime": "Изпълнение на контейнера", - "containerRuntimeDesc": "Изберете CLI, използван за управление на контейнери на този хост", - "containerRuntimeDocker": "Докер", - "containerRuntimePodman": "Подман" - }, "onboarding": { "feature_docker": "Докер", "feature_docker_desc": "Вижте контейнери, прочетете лог файлове и наблюдавайте статистики на всеки хост, на който работи Docker." + }, + "settings": { + "host": { + "enable": { + "label": "Активиране на Докер" + }, + "containerRuntime": { + "label": "Изпълнение на контейнера", + "description": "Изберете CLI, използван за управление на контейнери на този хост", + "docker": "Докер", + "podman": "Подман" + } + } } } diff --git a/plugins/docker/locales/translated/bn_BD.json b/plugins/docker/locales/translated/bn_BD.json index 9901c6a32..ca0541922 100644 --- a/plugins/docker/locales/translated/bn_BD.json +++ b/plugins/docker/locales/translated/bn_BD.json @@ -99,18 +99,21 @@ "nav": { "docker": "ডকার" }, - "hosts": { - "tabDocker": "ডকার", - "dockerIntegration": "ডকার ইন্টিগ্রেশন", - "enableDockerMonitor": "ডকার সক্রিয় করুন", - "enableDockerMonitorDesc": "ডকারের মাধ্যমে এই হোস্টে কন্টেইনারগুলি নিরীক্ষণ ও পরিচালনা করুন।", - "containerRuntime": "কন্টেইনার রানটাইম", - "containerRuntimeDesc": "এই হোস্টে কন্টেইনার ব্যবস্থাপনার জন্য ব্যবহৃত CLI নির্বাচন করুন", - "containerRuntimeDocker": "ডকার", - "containerRuntimePodman": "পডম্যান" - }, "onboarding": { "feature_docker": "ডকার", "feature_docker_desc": "ডকার চালিত যেকোনো হোস্টে কন্টেইনার দেখুন, লগ পড়ুন এবং পরিসংখ্যান পর্যবেক্ষণ করুন।" + }, + "settings": { + "host": { + "enable": { + "label": "ডকার সক্রিয় করুন" + }, + "containerRuntime": { + "label": "কন্টেইনার রানটাইম", + "description": "এই হোস্টে কন্টেইনার ব্যবস্থাপনার জন্য ব্যবহৃত CLI নির্বাচন করুন", + "docker": "ডকার", + "podman": "পডম্যান" + } + } } } diff --git a/plugins/docker/locales/translated/ca_ES.json b/plugins/docker/locales/translated/ca_ES.json index 9d2471510..2e880a1f5 100644 --- a/plugins/docker/locales/translated/ca_ES.json +++ b/plugins/docker/locales/translated/ca_ES.json @@ -99,18 +99,21 @@ "nav": { "docker": "Docker" }, - "hosts": { - "tabDocker": "Docker", - "dockerIntegration": "Integració de Docker", - "enableDockerMonitor": "Activa Docker", - "enableDockerMonitorDesc": "Supervisar i gestionar els contenidors d'aquest amfitrió mitjançant Docker", - "containerRuntime": "Temps d'execució del contenidor", - "containerRuntimeDesc": "Trieu la CLI utilitzada per a la gestió de contenidors en aquest amfitrió", - "containerRuntimeDocker": "Docker", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "Docker", "feature_docker_desc": "Veure contenidors, llegir registres i observar estadístiques de qualsevol host que executi Docker." + }, + "settings": { + "host": { + "enable": { + "label": "Activa Docker" + }, + "containerRuntime": { + "label": "Temps d'execució del contenidor", + "description": "Trieu la CLI utilitzada per a la gestió de contenidors en aquest amfitrió", + "docker": "Docker", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/cs_CZ.json b/plugins/docker/locales/translated/cs_CZ.json index 96919b628..6ab0c84b1 100644 --- a/plugins/docker/locales/translated/cs_CZ.json +++ b/plugins/docker/locales/translated/cs_CZ.json @@ -99,18 +99,21 @@ "nav": { "docker": "Přístavní dělník" }, - "hosts": { - "tabDocker": "Přístavní dělník", - "dockerIntegration": "Integrace Dockeru", - "enableDockerMonitor": "Povolit Docker", - "enableDockerMonitorDesc": "Monitorování a správa kontejnerů na tomto hostiteli pomocí Dockeru", - "containerRuntime": "Běh kontejneru", - "containerRuntimeDesc": "Vyberte rozhraní CLI používané pro správu kontejnerů na tomto hostiteli", - "containerRuntimeDocker": "Přístavní dělník", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "Přístavní dělník", "feature_docker_desc": "Zobrazujte kontejnery, čtěte protokoly a sledujte statistiky na jakémkoli hostiteli s Dockerem." + }, + "settings": { + "host": { + "enable": { + "label": "Povolit Docker" + }, + "containerRuntime": { + "label": "Běh kontejneru", + "description": "Vyberte rozhraní CLI používané pro správu kontejnerů na tomto hostiteli", + "docker": "Přístavní dělník", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/da_DK.json b/plugins/docker/locales/translated/da_DK.json index 5a382b61f..314d987fc 100644 --- a/plugins/docker/locales/translated/da_DK.json +++ b/plugins/docker/locales/translated/da_DK.json @@ -99,18 +99,21 @@ "nav": { "docker": "Docker" }, - "hosts": { - "tabDocker": "Docker", - "dockerIntegration": "Docker-integration", - "enableDockerMonitor": "Aktivér Docker", - "enableDockerMonitorDesc": "Overvåg og administrer containere på denne vært via Docker", - "containerRuntime": "Containerkørselstid", - "containerRuntimeDesc": "Vælg den CLI, der bruges til containeradministration på denne vært", - "containerRuntimeDocker": "Docker", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "Docker", "feature_docker_desc": "Se containere, læs logfiler og se statistikker på enhver vært, der kører Docker." + }, + "settings": { + "host": { + "enable": { + "label": "Aktivér Docker" + }, + "containerRuntime": { + "label": "Containerkørselstid", + "description": "Vælg den CLI, der bruges til containeradministration på denne vært", + "docker": "Docker", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/de_DE.json b/plugins/docker/locales/translated/de_DE.json index 0d020dee7..360d204bb 100644 --- a/plugins/docker/locales/translated/de_DE.json +++ b/plugins/docker/locales/translated/de_DE.json @@ -99,18 +99,21 @@ "nav": { "docker": "Docker" }, - "hosts": { - "tabDocker": "Docker", - "dockerIntegration": "Docker-Integration", - "enableDockerMonitor": "Docker aktivieren", - "enableDockerMonitorDesc": "Container auf diesem Host über Docker überwachen und verwalten", - "containerRuntime": "Container-Runtime", - "containerRuntimeDesc": "Wählen Sie die CLI für die Container-Verwaltung auf diesem Host", - "containerRuntimeDocker": "Docker", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "Docker", "feature_docker_desc": "Container anzeigen, Protokolle lesen und Statistiken auf jedem Host, auf dem Docker ausgeführt wird, einsehen." + }, + "settings": { + "host": { + "enable": { + "label": "Docker aktivieren" + }, + "containerRuntime": { + "label": "Container-Runtime", + "description": "Wählen Sie die CLI für die Container-Verwaltung auf diesem Host", + "docker": "Docker", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/el_GR.json b/plugins/docker/locales/translated/el_GR.json index c51ae9939..077a46d95 100644 --- a/plugins/docker/locales/translated/el_GR.json +++ b/plugins/docker/locales/translated/el_GR.json @@ -99,18 +99,21 @@ "nav": { "docker": "Λιμενεργάτης" }, - "hosts": { - "tabDocker": "Λιμενεργάτης", - "dockerIntegration": "Ενσωμάτωση Docker", - "enableDockerMonitor": "Ενεργοποίηση Docker", - "enableDockerMonitorDesc": "Παρακολούθηση και διαχείριση κοντέινερ σε αυτόν τον κεντρικό υπολογιστή μέσω του Docker", - "containerRuntime": "Χρόνος εκτέλεσης κοντέινερ", - "containerRuntimeDesc": "Επιλέξτε το CLI που χρησιμοποιείται για τη διαχείριση κοντέινερ σε αυτόν τον κεντρικό υπολογιστή", - "containerRuntimeDocker": "Λιμενεργάτης", - "containerRuntimePodman": "Πόντμαν" - }, "onboarding": { "feature_docker": "Λιμενεργάτης", "feature_docker_desc": "Δείτε κοντέινερ, διαβάστε αρχεία καταγραφής και παρακολουθήστε στατιστικά στοιχεία σε οποιονδήποτε κεντρικό υπολογιστή που εκτελεί Docker." + }, + "settings": { + "host": { + "enable": { + "label": "Ενεργοποίηση Docker" + }, + "containerRuntime": { + "label": "Χρόνος εκτέλεσης κοντέινερ", + "description": "Επιλέξτε το CLI που χρησιμοποιείται για τη διαχείριση κοντέινερ σε αυτόν τον κεντρικό υπολογιστή", + "docker": "Λιμενεργάτης", + "podman": "Πόντμαν" + } + } } } diff --git a/plugins/docker/locales/translated/es_ES.json b/plugins/docker/locales/translated/es_ES.json index 057c962df..f34e6e325 100644 --- a/plugins/docker/locales/translated/es_ES.json +++ b/plugins/docker/locales/translated/es_ES.json @@ -99,18 +99,21 @@ "nav": { "docker": "Docker" }, - "hosts": { - "tabDocker": "Docker", - "dockerIntegration": "Integración con Docker", - "enableDockerMonitor": "Activar Docker", - "enableDockerMonitorDesc": "Supervisar y gestionar contenedores en este host mediante Docker.", - "containerRuntime": "Runtime de contenedores", - "containerRuntimeDesc": "Elegir la CLI utilizada para la gestión de contenedores en este host.", - "containerRuntimeDocker": "Docker", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "Estibador", "feature_docker_desc": "Vea los contenedores, lea los registros y observe las estadísticas en cualquier host que ejecute Docker." + }, + "settings": { + "host": { + "enable": { + "label": "Activar Docker" + }, + "containerRuntime": { + "label": "Runtime de contenedores", + "description": "Elegir la CLI utilizada para la gestión de contenedores en este host.", + "docker": "Docker", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/fi_FI.json b/plugins/docker/locales/translated/fi_FI.json index e7e618b61..db7817049 100644 --- a/plugins/docker/locales/translated/fi_FI.json +++ b/plugins/docker/locales/translated/fi_FI.json @@ -99,18 +99,21 @@ "nav": { "docker": "Satamatyöläinen" }, - "hosts": { - "tabDocker": "Satamatyöläinen", - "dockerIntegration": "Docker-integraatio", - "enableDockerMonitor": "Ota Docker käyttöön", - "enableDockerMonitorDesc": "Valvo ja hallinnoi tämän isännän säilöjä Dockerin kautta", - "containerRuntime": "Säilön suoritusaika", - "containerRuntimeDesc": "Valitse CLI, jota käytetään säilön hallintaan tässä isännässä", - "containerRuntimeDocker": "Satamatyöläinen", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "Satamatyöläinen", "feature_docker_desc": "Katso säilöjä, lue lokeja ja seuraa tilastoja millä tahansa Dockeria käyttävällä isännällä." + }, + "settings": { + "host": { + "enable": { + "label": "Ota Docker käyttöön" + }, + "containerRuntime": { + "label": "Säilön suoritusaika", + "description": "Valitse CLI, jota käytetään säilön hallintaan tässä isännässä", + "docker": "Satamatyöläinen", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/fr_FR.json b/plugins/docker/locales/translated/fr_FR.json index 1c4d2693d..3f3c90e9c 100644 --- a/plugins/docker/locales/translated/fr_FR.json +++ b/plugins/docker/locales/translated/fr_FR.json @@ -99,18 +99,21 @@ "nav": { "docker": "Docker" }, - "hosts": { - "tabDocker": "Docker", - "dockerIntegration": "Intégration Docker", - "enableDockerMonitor": "Activer Docker", - "enableDockerMonitorDesc": "Surveiller et gérer les conteneurs sur cet hôte via Docker", - "containerRuntime": "Moteur d'exécution de conteneurs", - "containerRuntimeDesc": "Choisissez le CLI utilisé pour la gestion des conteneurs sur cet hôte", - "containerRuntimeDocker": "Docker", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "Docker", "feature_docker_desc": "Consultez les conteneurs, lisez les journaux et surveillez les statistiques sur n'importe quel hôte exécutant Docker." + }, + "settings": { + "host": { + "enable": { + "label": "Activer Docker" + }, + "containerRuntime": { + "label": "Moteur d'exécution de conteneurs", + "description": "Choisissez le CLI utilisé pour la gestion des conteneurs sur cet hôte", + "docker": "Docker", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/he_IL.json b/plugins/docker/locales/translated/he_IL.json index 50db67f46..0e6e792c5 100644 --- a/plugins/docker/locales/translated/he_IL.json +++ b/plugins/docker/locales/translated/he_IL.json @@ -99,18 +99,21 @@ "nav": { "docker": "דוקר" }, - "hosts": { - "tabDocker": "דוקר", - "dockerIntegration": "אינטגרציה עם Docker", - "enableDockerMonitor": "הפעל את Docker", - "enableDockerMonitorDesc": "ניטור וניהול של מכולות במארח זה באמצעות Docker", - "containerRuntime": "זמן ריצה של המכולה", - "containerRuntimeDesc": "בחר את ממשק שורת הפקודה (CLI) המשמש לניהול מכולות במארח זה", - "containerRuntimeDocker": "דוקר", - "containerRuntimePodman": "פודמן" - }, "onboarding": { "feature_docker": "דוקר", "feature_docker_desc": "ראה קונטיינרים, קרא יומני רישום וצפה בסטטיסטיקות על כל מארח שמפעיל את Docker." + }, + "settings": { + "host": { + "enable": { + "label": "הפעל את Docker" + }, + "containerRuntime": { + "label": "זמן ריצה של המכולה", + "description": "בחר את ממשק שורת הפקודה (CLI) המשמש לניהול מכולות במארח זה", + "docker": "דוקר", + "podman": "פודמן" + } + } } } diff --git a/plugins/docker/locales/translated/hi_IN.json b/plugins/docker/locales/translated/hi_IN.json index 2ddadfd83..519961105 100644 --- a/plugins/docker/locales/translated/hi_IN.json +++ b/plugins/docker/locales/translated/hi_IN.json @@ -99,18 +99,21 @@ "nav": { "docker": "Docker" }, - "hosts": { - "tabDocker": "Docker", - "dockerIntegration": "Docker एकीकरण", - "enableDockerMonitor": "Docker सक्षम करें", - "enableDockerMonitorDesc": "इस होस्ट पर Docker के माध्यम से कंटेनरों की निगरानी और प्रबंधन करें", - "containerRuntime": "कंटेनर रनटाइम", - "containerRuntimeDesc": "इस होस्ट पर कंटेनर प्रबंधन के लिए उपयोग किए जाने वाले CLI को चुनें", - "containerRuntimeDocker": "Docker", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "डाक में काम करनेवाला मज़दूर", "feature_docker_desc": "डॉकर चलाने वाले किसी भी होस्ट पर कंटेनर देखें, लॉग पढ़ें और आंकड़े देखें।" + }, + "settings": { + "host": { + "enable": { + "label": "Docker सक्षम करें" + }, + "containerRuntime": { + "label": "कंटेनर रनटाइम", + "description": "इस होस्ट पर कंटेनर प्रबंधन के लिए उपयोग किए जाने वाले CLI को चुनें", + "docker": "Docker", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/hu_HU.json b/plugins/docker/locales/translated/hu_HU.json index ec317ac09..90d649572 100644 --- a/plugins/docker/locales/translated/hu_HU.json +++ b/plugins/docker/locales/translated/hu_HU.json @@ -99,18 +99,21 @@ "nav": { "docker": "Dokkmunkás" }, - "hosts": { - "tabDocker": "Dokkmunkás", - "dockerIntegration": "Docker-integráció", - "enableDockerMonitor": "Docker engedélyezése", - "enableDockerMonitorDesc": "Dockeren keresztül figyelheti és kezelheti a konténereket ezen a gazdagépen", - "containerRuntime": "Konténer futásideje", - "containerRuntimeDesc": "Válassza ki a gazdagépen a konténerkezeléshez használt parancssori felületet", - "containerRuntimeDocker": "Dokkmunkás", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "Dokkmunkás", "feature_docker_desc": "Tekintsd meg a konténereket, olvasd el a naplókat és figyeld a statisztikákat bármelyik Dockert futtató gazdagépen." + }, + "settings": { + "host": { + "enable": { + "label": "Docker engedélyezése" + }, + "containerRuntime": { + "label": "Konténer futásideje", + "description": "Válassza ki a gazdagépen a konténerkezeléshez használt parancssori felületet", + "docker": "Dokkmunkás", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/id_ID.json b/plugins/docker/locales/translated/id_ID.json index 8852358ec..c32fe2d02 100644 --- a/plugins/docker/locales/translated/id_ID.json +++ b/plugins/docker/locales/translated/id_ID.json @@ -99,18 +99,21 @@ "nav": { "docker": "Buruh pelabuhan" }, - "hosts": { - "tabDocker": "Buruh pelabuhan", - "dockerIntegration": "Integrasi Docker", - "enableDockerMonitor": "Aktifkan Docker", - "enableDockerMonitorDesc": "Pantau dan kelola kontainer di host ini melalui Docker.", - "containerRuntime": "Lingkungan Eksekusi Kontainer", - "containerRuntimeDesc": "Pilih CLI yang digunakan untuk manajemen kontainer pada host ini.", - "containerRuntimeDocker": "Buruh pelabuhan", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "Buruh pelabuhan", "feature_docker_desc": "Lihat kontainer, baca log, dan pantau statistik pada host mana pun yang menjalankan Docker." + }, + "settings": { + "host": { + "enable": { + "label": "Aktifkan Docker" + }, + "containerRuntime": { + "label": "Lingkungan Eksekusi Kontainer", + "description": "Pilih CLI yang digunakan untuk manajemen kontainer pada host ini.", + "docker": "Buruh pelabuhan", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/it_IT.json b/plugins/docker/locales/translated/it_IT.json index e8cdb6be1..c437fc338 100644 --- a/plugins/docker/locales/translated/it_IT.json +++ b/plugins/docker/locales/translated/it_IT.json @@ -99,18 +99,21 @@ "nav": { "docker": "Docker" }, - "hosts": { - "tabDocker": "Docker", - "dockerIntegration": "Integrazione Docker", - "enableDockerMonitor": "Abilita Docker", - "enableDockerMonitorDesc": "Monitora e gestisci i container su questo host tramite Docker", - "containerRuntime": "Runtime container", - "containerRuntimeDesc": "Scegli la CLI utilizzata per la gestione dei container su questo host", - "containerRuntimeDocker": "Docker", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "Docker", "feature_docker_desc": "Visualizza i container, leggi i log e controlla le statistiche su qualsiasi host che esegue Docker." + }, + "settings": { + "host": { + "enable": { + "label": "Abilita Docker" + }, + "containerRuntime": { + "label": "Runtime container", + "description": "Scegli la CLI utilizzata per la gestione dei container su questo host", + "docker": "Docker", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/ja_JP.json b/plugins/docker/locales/translated/ja_JP.json index 191baff1b..332d69537 100644 --- a/plugins/docker/locales/translated/ja_JP.json +++ b/plugins/docker/locales/translated/ja_JP.json @@ -99,18 +99,21 @@ "nav": { "docker": "Docker" }, - "hosts": { - "tabDocker": "Docker", - "dockerIntegration": "Docker統合", - "enableDockerMonitor": "Docker有効化", - "enableDockerMonitorDesc": "Docker経由でこのホスト上のコンテナを監視・管理します。", - "containerRuntime": "コンテナランタイム", - "containerRuntimeDesc": "このホストのコンテナ管理に使用するCLIを選択します。", - "containerRuntimeDocker": "Docker", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "ドッカー", "feature_docker_desc": "Dockerを実行している任意のホスト上で、コンテナの表示、ログの読み取り、統計情報の監視が可能です。" + }, + "settings": { + "host": { + "enable": { + "label": "Docker有効化" + }, + "containerRuntime": { + "label": "コンテナランタイム", + "description": "このホストのコンテナ管理に使用するCLIを選択します。", + "docker": "Docker", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/ko_KR.json b/plugins/docker/locales/translated/ko_KR.json index 164c2b24b..3800135d1 100644 --- a/plugins/docker/locales/translated/ko_KR.json +++ b/plugins/docker/locales/translated/ko_KR.json @@ -99,18 +99,21 @@ "nav": { "docker": "Docker" }, - "hosts": { - "tabDocker": "Docker", - "dockerIntegration": "Docker 통합", - "enableDockerMonitor": "Docker 활성화", - "enableDockerMonitorDesc": "Docker를 통해 이 호스트의 컨테이너를 모니터링하고 관리합니다.", - "containerRuntime": "컨테이너 런타임", - "containerRuntimeDesc": "이 호스트에서 컨테이너 관리에 사용할 CLI를 선택합니다.", - "containerRuntimeDocker": "Docker", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "도커", "feature_docker_desc": "Docker가 실행 중인 모든 호스트에서 컨테이너를 확인하고, 로그를 읽고, 통계를 볼 수 있습니다." + }, + "settings": { + "host": { + "enable": { + "label": "Docker 활성화" + }, + "containerRuntime": { + "label": "컨테이너 런타임", + "description": "이 호스트에서 컨테이너 관리에 사용할 CLI를 선택합니다.", + "docker": "Docker", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/nl_NL.json b/plugins/docker/locales/translated/nl_NL.json index 8c8abbf4a..af8fdcf58 100644 --- a/plugins/docker/locales/translated/nl_NL.json +++ b/plugins/docker/locales/translated/nl_NL.json @@ -99,18 +99,21 @@ "nav": { "docker": "Docker" }, - "hosts": { - "tabDocker": "Docker", - "dockerIntegration": "Docker-integratie", - "enableDockerMonitor": "Docker inschakelen", - "enableDockerMonitorDesc": "Containers op deze host bewaken en beheren via Docker.", - "containerRuntime": "Container Runtime", - "containerRuntimeDesc": "Kies de CLI die op deze host wordt gebruikt voor containerbeheer.", - "containerRuntimeDocker": "Docker", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "Docker", "feature_docker_desc": "Bekijk containers, lees logboeken en bekijk statistieken op elke host waarop Docker draait." + }, + "settings": { + "host": { + "enable": { + "label": "Docker inschakelen" + }, + "containerRuntime": { + "label": "Container Runtime", + "description": "Kies de CLI die op deze host wordt gebruikt voor containerbeheer.", + "docker": "Docker", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/no_NO.json b/plugins/docker/locales/translated/no_NO.json index 3fdb7f2f1..c8af84512 100644 --- a/plugins/docker/locales/translated/no_NO.json +++ b/plugins/docker/locales/translated/no_NO.json @@ -99,18 +99,21 @@ "nav": { "docker": "Dokker" }, - "hosts": { - "tabDocker": "Dokker", - "dockerIntegration": "Docker-integrasjon", - "enableDockerMonitor": "Aktiver Docker", - "enableDockerMonitorDesc": "Overvåk og administrer containere på denne verten via Docker", - "containerRuntime": "Containerkjøringstid", - "containerRuntimeDesc": "Velg CRI-et som brukes for containeradministrasjon på denne verten", - "containerRuntimeDocker": "Dokker", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "Dokker", "feature_docker_desc": "Se containere, les logger og se statistikk på alle verter som kjører Docker." + }, + "settings": { + "host": { + "enable": { + "label": "Aktiver Docker" + }, + "containerRuntime": { + "label": "Containerkjøringstid", + "description": "Velg CRI-et som brukes for containeradministrasjon på denne verten", + "docker": "Dokker", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/pl_PL.json b/plugins/docker/locales/translated/pl_PL.json index 54e3bde21..35dd84d29 100644 --- a/plugins/docker/locales/translated/pl_PL.json +++ b/plugins/docker/locales/translated/pl_PL.json @@ -99,18 +99,21 @@ "nav": { "docker": "Docker" }, - "hosts": { - "tabDocker": "Docker", - "dockerIntegration": "Integracja z Dockerem", - "enableDockerMonitor": "Włącz Docker", - "enableDockerMonitorDesc": "Monitoruj i zarządzaj kontenerami na tym hoście za pomocą Dockera", - "containerRuntime": "Środowisko wykonawcze kontenera", - "containerRuntimeDesc": "Wybierz CLI używane do zarządzania kontenerami na tym hoście", - "containerRuntimeDocker": "Docker", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "Docker", "feature_docker_desc": "Przeglądaj kontenery, czytaj logi i śledź statystyki na dowolnym hoście, na którym działa Docker." + }, + "settings": { + "host": { + "enable": { + "label": "Włącz Docker" + }, + "containerRuntime": { + "label": "Środowisko wykonawcze kontenera", + "description": "Wybierz CLI używane do zarządzania kontenerami na tym hoście", + "docker": "Docker", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/pt_BR.json b/plugins/docker/locales/translated/pt_BR.json index e4d3eb899..8ada980d9 100644 --- a/plugins/docker/locales/translated/pt_BR.json +++ b/plugins/docker/locales/translated/pt_BR.json @@ -99,18 +99,21 @@ "nav": { "docker": "Docker" }, - "hosts": { - "tabDocker": "Docker", - "dockerIntegration": "Integração do Docker", - "enableDockerMonitor": "Habilitar Docker", - "enableDockerMonitorDesc": "Monitore e gerencie contêineres neste host via Docker.", - "containerRuntime": "Tempo de execução do contêiner", - "containerRuntimeDesc": "Escolha a CLI usada para gerenciamento de contêineres neste host.", - "containerRuntimeDocker": "Docker", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "Docker", "feature_docker_desc": "Visualize contêineres, leia registros e acompanhe as estatísticas em qualquer host que execute o Docker." + }, + "settings": { + "host": { + "enable": { + "label": "Habilitar Docker" + }, + "containerRuntime": { + "label": "Tempo de execução do contêiner", + "description": "Escolha a CLI usada para gerenciamento de contêineres neste host.", + "docker": "Docker", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/pt_PT.json b/plugins/docker/locales/translated/pt_PT.json index 138d379e1..a72b1b55f 100644 --- a/plugins/docker/locales/translated/pt_PT.json +++ b/plugins/docker/locales/translated/pt_PT.json @@ -99,18 +99,21 @@ "nav": { "docker": "Docker" }, - "hosts": { - "tabDocker": "Docker", - "dockerIntegration": "Integração com Docker", - "enableDockerMonitor": "Ativar Docker", - "enableDockerMonitorDesc": "Monitorizar e gerir contentores neste host via Docker", - "containerRuntime": "Runtime de Contentores", - "containerRuntimeDesc": "Escolha a CLI usada para a gestão de contentores neste host", - "containerRuntimeDocker": "Docker", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "Docker", "feature_docker_desc": "Visualize contentores, leia registos e acompanhe as estatísticas em qualquer host que execute o Docker." + }, + "settings": { + "host": { + "enable": { + "label": "Ativar Docker" + }, + "containerRuntime": { + "label": "Runtime de Contentores", + "description": "Escolha a CLI usada para a gestão de contentores neste host", + "docker": "Docker", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/ro_RO.json b/plugins/docker/locales/translated/ro_RO.json index f81deceb9..2c8c9384b 100644 --- a/plugins/docker/locales/translated/ro_RO.json +++ b/plugins/docker/locales/translated/ro_RO.json @@ -99,18 +99,21 @@ "nav": { "docker": "Docher" }, - "hosts": { - "tabDocker": "Docher", - "dockerIntegration": "Integrare Docker", - "enableDockerMonitor": "Activează Docker", - "enableDockerMonitorDesc": "Monitorizați și gestionați containerele de pe această gazdă prin Docker", - "containerRuntime": "Runtime de container", - "containerRuntimeDesc": "Alegeți interfața CLI utilizată pentru gestionarea containerelor pe această gazdă", - "containerRuntimeDocker": "Docher", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "Docher", "feature_docker_desc": "Vizualizați containere, citiți jurnale și urmăriți statistici pe orice gazdă care rulează Docker." + }, + "settings": { + "host": { + "enable": { + "label": "Activează Docker" + }, + "containerRuntime": { + "label": "Runtime de container", + "description": "Alegeți interfața CLI utilizată pentru gestionarea containerelor pe această gazdă", + "docker": "Docher", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/ru_RU.json b/plugins/docker/locales/translated/ru_RU.json index 671411170..1b11e1af9 100644 --- a/plugins/docker/locales/translated/ru_RU.json +++ b/plugins/docker/locales/translated/ru_RU.json @@ -99,18 +99,21 @@ "nav": { "docker": "Docker" }, - "hosts": { - "tabDocker": "Docker", - "dockerIntegration": "Интеграция Docker", - "enableDockerMonitor": "Включить Docker", - "enableDockerMonitorDesc": "Отслеживать и управлять контейнерами на этом хосте через Docker", - "containerRuntime": "Среда выполнения контейнеров", - "containerRuntimeDesc": "Выберите CLI, используемый для управления контейнерами на этом хосте", - "containerRuntimeDocker": "Docker", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "Docker", "feature_docker_desc": "Просматривайте контейнеры, читайте журналы и отслеживайте статистику на любом хосте, где запущен Docker." + }, + "settings": { + "host": { + "enable": { + "label": "Включить Docker" + }, + "containerRuntime": { + "label": "Среда выполнения контейнеров", + "description": "Выберите CLI, используемый для управления контейнерами на этом хосте", + "docker": "Docker", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/sr_SP.json b/plugins/docker/locales/translated/sr_SP.json index 00a2ee5f3..72911733a 100644 --- a/plugins/docker/locales/translated/sr_SP.json +++ b/plugins/docker/locales/translated/sr_SP.json @@ -99,18 +99,21 @@ "nav": { "docker": "Докер" }, - "hosts": { - "tabDocker": "Докер", - "dockerIntegration": "Интеграција Докера", - "enableDockerMonitor": "Омогући Докер", - "enableDockerMonitorDesc": "Пратите и управљајте контејнерима на овом хосту путем Докера", - "containerRuntime": "Време извршавања контејнера", - "containerRuntimeDesc": "Изаберите CLI који се користи за управљање контејнерима на овом хосту", - "containerRuntimeDocker": "Докер", - "containerRuntimePodman": "Подман" - }, "onboarding": { "feature_docker": "Докер", "feature_docker_desc": "Погледајте контејнере, читајте логове и пратите статистику на било ком хосту који користи Docker." + }, + "settings": { + "host": { + "enable": { + "label": "Омогући Докер" + }, + "containerRuntime": { + "label": "Време извршавања контејнера", + "description": "Изаберите CLI који се користи за управљање контејнерима на овом хосту", + "docker": "Докер", + "podman": "Подман" + } + } } } diff --git a/plugins/docker/locales/translated/sv_SE.json b/plugins/docker/locales/translated/sv_SE.json index a49891e02..0d2b02ee5 100644 --- a/plugins/docker/locales/translated/sv_SE.json +++ b/plugins/docker/locales/translated/sv_SE.json @@ -99,18 +99,21 @@ "nav": { "docker": "Hamnarbetare" }, - "hosts": { - "tabDocker": "Hamnarbetare", - "dockerIntegration": "Docker-integration", - "enableDockerMonitor": "Aktivera Docker", - "enableDockerMonitorDesc": "Övervaka och hantera containrar på den här värden via Docker", - "containerRuntime": "Containerkörningstid", - "containerRuntimeDesc": "Välj det CRI som används för containerhantering på den här värden", - "containerRuntimeDocker": "Hamnarbetare", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "Hamnarbetare", "feature_docker_desc": "Se containrar, läs loggar och titta på statistik för alla värdar som kör Docker." + }, + "settings": { + "host": { + "enable": { + "label": "Aktivera Docker" + }, + "containerRuntime": { + "label": "Containerkörningstid", + "description": "Välj det CRI som används för containerhantering på den här värden", + "docker": "Hamnarbetare", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/th_TH.json b/plugins/docker/locales/translated/th_TH.json index bdea9fc5f..ca109591a 100644 --- a/plugins/docker/locales/translated/th_TH.json +++ b/plugins/docker/locales/translated/th_TH.json @@ -99,18 +99,21 @@ "nav": { "docker": "ด็อกเกอร์" }, - "hosts": { - "tabDocker": "ด็อกเกอร์", - "dockerIntegration": "การผสานรวม Docker", - "enableDockerMonitor": "เปิดใช้งาน Docker", - "enableDockerMonitorDesc": "ตรวจสอบและจัดการคอนเทนเนอร์บนโฮสต์นี้ผ่าน Docker", - "containerRuntime": "รันไทม์คอนเทนเนอร์", - "containerRuntimeDesc": "เลือก CLI ที่ใช้สำหรับการจัดการคอนเทนเนอร์บนโฮสต์นี้", - "containerRuntimeDocker": "ด็อกเกอร์", - "containerRuntimePodman": "พอดแมน" - }, "onboarding": { "feature_docker": "ด็อกเกอร์", "feature_docker_desc": "ดูคอนเทนเนอร์ อ่านบันทึก และตรวจสอบสถิติบนโฮสต์ใดก็ได้ที่ใช้งาน Docker" + }, + "settings": { + "host": { + "enable": { + "label": "เปิดใช้งาน Docker" + }, + "containerRuntime": { + "label": "รันไทม์คอนเทนเนอร์", + "description": "เลือก CLI ที่ใช้สำหรับการจัดการคอนเทนเนอร์บนโฮสต์นี้", + "docker": "ด็อกเกอร์", + "podman": "พอดแมน" + } + } } } diff --git a/plugins/docker/locales/translated/tr_TR.json b/plugins/docker/locales/translated/tr_TR.json index 5b0e444ea..9488e1b0f 100644 --- a/plugins/docker/locales/translated/tr_TR.json +++ b/plugins/docker/locales/translated/tr_TR.json @@ -99,18 +99,21 @@ "nav": { "docker": "Docker" }, - "hosts": { - "tabDocker": "Docker", - "dockerIntegration": "Docker Entegrasyonu", - "enableDockerMonitor": "Docker'ı Etkinleştir", - "enableDockerMonitorDesc": "Docker aracılığıyla bu ana bilgisayardaki kapsayıcıları izle ve yönet", - "containerRuntime": "Kapsayıcı Çalışma Zamanı", - "containerRuntimeDesc": "Bu ana bilgisayarda kapsayıcı yönetimi için kullanılan CLI'yi seçin", - "containerRuntimeDocker": "Docker", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "Liman işçisi", "feature_docker_desc": "Docker çalıştıran herhangi bir sunucudaki konteynerleri görüntüleyin, günlükleri okuyun ve istatistikleri izleyin." + }, + "settings": { + "host": { + "enable": { + "label": "Docker'ı Etkinleştir" + }, + "containerRuntime": { + "label": "Kapsayıcı Çalışma Zamanı", + "description": "Bu ana bilgisayarda kapsayıcı yönetimi için kullanılan CLI'yi seçin", + "docker": "Docker", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/uk_UA.json b/plugins/docker/locales/translated/uk_UA.json index 2a7c7bc7f..ca5309da8 100644 --- a/plugins/docker/locales/translated/uk_UA.json +++ b/plugins/docker/locales/translated/uk_UA.json @@ -99,18 +99,21 @@ "nav": { "docker": "Докер" }, - "hosts": { - "tabDocker": "Докер", - "dockerIntegration": "Інтеграція Docker", - "enableDockerMonitor": "Увімкнути Докер", - "enableDockerMonitorDesc": "Моніторинг та керування контейнерами на цьому хості через Docker", - "containerRuntime": "Середовище виконання контейнера", - "containerRuntimeDesc": "Виберіть інтерфейс командного рядка, який використовується для керування контейнерами на цьому хості", - "containerRuntimeDocker": "Докер", - "containerRuntimePodman": "Подман" - }, "onboarding": { "feature_docker": "Докер", "feature_docker_desc": "Переглядайте контейнери, читайте журнали та переглядайте статистику на будь-якому хості, на якому працює Docker." + }, + "settings": { + "host": { + "enable": { + "label": "Увімкнути Докер" + }, + "containerRuntime": { + "label": "Середовище виконання контейнера", + "description": "Виберіть інтерфейс командного рядка, який використовується для керування контейнерами на цьому хості", + "docker": "Докер", + "podman": "Подман" + } + } } } diff --git a/plugins/docker/locales/translated/vi_VN.json b/plugins/docker/locales/translated/vi_VN.json index d4ca5f532..616b6054f 100644 --- a/plugins/docker/locales/translated/vi_VN.json +++ b/plugins/docker/locales/translated/vi_VN.json @@ -99,18 +99,21 @@ "nav": { "docker": "Docker" }, - "hosts": { - "tabDocker": "Docker", - "dockerIntegration": "Tích hợp Docker", - "enableDockerMonitor": "Bật Docker", - "enableDockerMonitorDesc": "Giám sát và quản lý container trên máy chủ này qua Docker", - "containerRuntime": "Môi trường chạy container", - "containerRuntimeDesc": "Chọn CLI dùng để quản lý container trên máy chủ này", - "containerRuntimeDocker": "Docker", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "Docker", "feature_docker_desc": "Xem các container, đọc nhật ký và theo dõi số liệu thống kê trên bất kỳ máy chủ nào đang chạy Docker." + }, + "settings": { + "host": { + "enable": { + "label": "Bật Docker" + }, + "containerRuntime": { + "label": "Môi trường chạy container", + "description": "Chọn CLI dùng để quản lý container trên máy chủ này", + "docker": "Docker", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/zh_CN.json b/plugins/docker/locales/translated/zh_CN.json index eb8e7e431..6a572a12b 100644 --- a/plugins/docker/locales/translated/zh_CN.json +++ b/plugins/docker/locales/translated/zh_CN.json @@ -99,18 +99,21 @@ "nav": { "docker": "Docker" }, - "hosts": { - "tabDocker": "Docker", - "dockerIntegration": "Docker 集成", - "enableDockerMonitor": "启用 Docker", - "enableDockerMonitorDesc": "通过 Docker 监控和管理此主机上的容器", - "containerRuntime": "容器运行时", - "containerRuntimeDesc": "选择此主机上用于容器管理的 CLI", - "containerRuntimeDocker": "Docker", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "Docker", "feature_docker_desc": "在任何运行 Docker 的主机上查看容器、读取日志及监控运行状态。" + }, + "settings": { + "host": { + "enable": { + "label": "启用 Docker" + }, + "containerRuntime": { + "label": "容器运行时", + "description": "选择此主机上用于容器管理的 CLI", + "docker": "Docker", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/locales/translated/zh_TW.json b/plugins/docker/locales/translated/zh_TW.json index 92ff524fd..75f80ae7d 100644 --- a/plugins/docker/locales/translated/zh_TW.json +++ b/plugins/docker/locales/translated/zh_TW.json @@ -99,18 +99,21 @@ "nav": { "docker": "Docker" }, - "hosts": { - "tabDocker": "Docker", - "dockerIntegration": "Docker 整合", - "enableDockerMonitor": "啟用 Docker", - "enableDockerMonitorDesc": "透過 Docker 監控及管理此主機上的容器", - "containerRuntime": "容器執行環境", - "containerRuntimeDesc": "選擇用於此主機容器管理的 CLI", - "containerRuntimeDocker": "Docker", - "containerRuntimePodman": "Podman" - }, "onboarding": { "feature_docker": "Docker", "feature_docker_desc": "查看執行 Docker 的任何主機上的容器、讀取日誌和查看統計資料。" + }, + "settings": { + "host": { + "enable": { + "label": "啟用 Docker" + }, + "containerRuntime": { + "label": "容器執行環境", + "description": "選擇用於此主機容器管理的 CLI", + "docker": "Docker", + "podman": "Podman" + } + } } } diff --git a/plugins/docker/manifest.json b/plugins/docker/manifest.json index 531b5c29e..495f08c8a 100644 --- a/plugins/docker/manifest.json +++ b/plugins/docker/manifest.json @@ -19,9 +19,20 @@ "credentials:use", "ssh:connect", "network:serve", - "audit:read", "ui:surface" ], + "provides": [ + { + "service": "docker.containers", + "version": "1.0.0", + "permission": "docker.use" + }, + { + "service": "docker.events", + "version": "1.0.0", + "permission": "docker.use" + } + ], "contributes": { "tabs": [ { @@ -31,10 +42,39 @@ "openFrom": ["host-context-menu", "palette"] } ], - "hostCapability": { - "key": "enableDocker", - "labelKey": "hosts.dockerIntegration", - "editorTab": "docker" + "permissions": [ + { + "name": "use", + "titleKey": "permissions.use.title", + "descriptionKey": "permissions.use.description", + "defaultRoles": ["admin", "user"] + } + ], + "settings": { + "host": { + "enableKey": "enableDocker", + "enableLabelKey": "settings.host.enable.label", + "fields": [ + { + "key": "containerRuntime", + "type": "select", + "labelKey": "settings.host.containerRuntime.label", + "descriptionKey": "settings.host.containerRuntime.description", + "requires": "enableDocker", + "default": "docker", + "options": [ + { + "value": "docker", + "labelKey": "settings.host.containerRuntime.docker" + }, + { + "value": "podman", + "labelKey": "settings.host.containerRuntime.podman" + } + ] + } + ] + } } }, "locales": "locales", diff --git a/plugins/docker/src/backend/connect.ts b/plugins/docker/src/backend/connect.ts new file mode 100644 index 000000000..2a59635bb --- /dev/null +++ b/plugins/docker/src/backend/connect.ts @@ -0,0 +1,404 @@ +/** + * The Docker panel's interactive connect. + * + * Core's pipeline does the handshake; this only bridges its prompt channel to + * HTTP. A connect that needs a person (a TOTP code, a Warpgate sign-in, a + * password the host does not store) answers the request that is waiting with + * what to ask, and the next request (connect-totp, connect-warpgate) carries + * the answer back and waits for whatever comes after it: the session, another + * prompt, or an error. + */ + +import type { Client } from "ssh2"; +import type { + PluginContext, + PluginSshHost, + PluginSshPromptRequest, +} from "@termix/plugin-sdk/backend"; +import type { ContainerRuntime } from "./container-runtime.js"; +import type { DockerSessions } from "./sessions.js"; +import { + connectionLog, + getErrorMessage, + type ConnectionLogLine, + type DockerLogger, +} from "./helpers.js"; + +/** What one HTTP request answers with. */ +export interface ConnectStep { + status: number; + body: Record; +} + +const TOTP_WAIT_MS = 3 * 60 * 1000; +const WARPGATE_WAIT_MS = 5 * 60 * 1000; +const STEP_WAIT_MS = 90 * 1000; +// Covers the longest a person may take on a prompt, plus the handshake. +const CONNECT_TIMEOUT_MS = WARPGATE_WAIT_MS + 30 * 1000; + +const PASSWORD_PROMPT = /password/i; + +/** Hands each step to whichever request is waiting for one. */ +export class PendingConnect { + constructor( + readonly userId: string, + readonly hostId: number, + ) {} + + expiresAt = Date.now() + TOTP_WAIT_MS; + /** What the open prompt expects, or null when nothing is being asked. */ + awaiting: "totp" | "warpgate" | null = null; + abandoned = false; + private waiter: ((step: ConnectStep) => void) | null = null; + private queued: ConnectStep | null = null; + private answer: ((value: string | null) => void) | null = null; + + emit(step: ConnectStep): void { + if (this.waiter) { + const waiter = this.waiter; + this.waiter = null; + waiter(step); + } else { + this.queued = step; + } + } + + next(timeoutMs = STEP_WAIT_MS): Promise { + if (this.queued) { + const step = this.queued; + this.queued = null; + return Promise.resolve(step); + } + return new Promise((resolve) => { + const timer = setTimeout(() => { + if (this.waiter !== settle) return; + this.waiter = null; + this.cancel(); + resolve({ + status: 408, + body: { error: "Connection timed out. Please reconnect." }, + }); + }, timeoutMs); + const settle = (step: ConnectStep) => { + clearTimeout(timer); + resolve(step); + }; + this.waiter = settle; + }); + } + + ask(kind: "totp" | "warpgate", waitMs: number): Promise { + this.awaiting = kind; + this.expiresAt = Date.now() + waitMs; + return new Promise((resolve) => { + this.answer = resolve; + }); + } + + respond(value: string | null): void { + const answer = this.answer; + this.answer = null; + this.awaiting = null; + answer?.(value); + } + + /** Nobody will read the result: give up the prompt and drop the client. */ + cancel(): void { + this.abandoned = true; + this.respond(null); + } +} + +function classifyConnectError( + message: string, + logs: ConnectionLogLine[], +): { hostKeyChanged: boolean; authFailed: boolean } { + if (message.includes("ENOTFOUND") || message.includes("getaddrinfo")) { + logs.push( + connectionLog("error", "dns", `DNS resolution failed: ${message}`), + ); + } else if ( + message.includes("ECONNREFUSED") || + message.includes("ETIMEDOUT") + ) { + logs.push( + connectionLog("error", "tcp", `TCP connection failed: ${message}`), + ); + } else if (message.includes("verification failed")) { + logs.push( + connectionLog( + "error", + "handshake", + "SSH host key has changed. For security, please open a Terminal connection to this host first to verify and accept the new key fingerprint.", + ), + ); + return { hostKeyChanged: true, authFailed: false }; + } else if ( + message.includes("handshake") || + message.includes("key exchange") + ) { + logs.push( + connectionLog("error", "handshake", `SSH handshake failed: ${message}`), + ); + } else if (/authentication/i.test(message)) { + logs.push( + connectionLog("error", "auth", `Authentication failed: ${message}`), + ); + return { hostKeyChanged: false, authFailed: true }; + } else { + logs.push( + connectionLog("error", "error", `SSH connection failed: ${message}`), + ); + } + return { hostKeyChanged: false, authFailed: false }; +} + +export interface StartConnectInput { + sessionId: string; + userId: string; + host: PluginSshHost; + runtime: ContainerRuntime; + logs: ConnectionLogLine[]; +} + +/** + * Starts a connect and returns the first step for the request that asked. + * The pending entry lives in `sessions` until the connect settles, so the + * answer routes can find it. + */ +export async function startConnect( + ctx: PluginContext, + sessions: DockerSessions, + log: DockerLogger, + input: StartConnectInput, +): Promise { + const { sessionId, userId, host, runtime, logs } = input; + const hostId = host.id; + const authType = (host.authType as string) || "none"; + const credentialless = !ctx.ssh.requiresSecret(authType); + + const pending = new PendingConnect(userId, hostId); + sessions.setPending(sessionId, pending); + + const parkTotp = (prompt: string, extra: Record) => { + logs.push(connectionLog("info", "docker_auth", "Verification required")); + const answer = pending.ask("totp", TOTP_WAIT_MS); + pending.emit({ + status: 200, + body: { + requires_totp: true, + sessionId, + prompt, + connectionLogs: logs, + ...extra, + }, + }); + return answer; + }; + + const ask = async ( + request: PluginSshPromptRequest, + ): Promise => { + if (pending.abandoned) return null; + switch (request.kind) { + case "totp": + return parkTotp(request.prompt, request.retry ? { retry: true } : {}); + case "warpgate": { + logs.push( + connectionLog( + "info", + "docker_auth", + "Warpgate authentication required", + ), + ); + const answer = pending.ask("warpgate", WARPGATE_WAIT_MS); + pending.emit({ + status: 200, + body: { + requires_warpgate: true, + sessionId, + url: request.url, + securityKey: request.securityKey, + connectionLogs: logs, + }, + }); + return answer; + } + case "input": { + const isPassword = PASSWORD_PROMPT.test(request.prompt); + if (isPassword && credentialless) { + pending.abandoned = true; + pending.emit({ + status: 200, + body: { status: "auth_required", reason: "no_keyboard" }, + }); + return null; + } + if (!isPassword && !request.isPush) return ""; + return parkTotp(request.prompt, isPassword ? { isPassword: true } : {}); + } + } + }; + + logs.push(connectionLog("info", "dns", `Resolving DNS for ${host.ip}`)); + logs.push( + connectionLog("info", "tcp", `Connecting to ${host.ip}:${host.port}`), + ); + logs.push(connectionLog("info", "handshake", "Initiating SSH handshake")); + + ctx.ssh + .connect(host, { + purpose: "docker", + timeoutMs: CONNECT_TIMEOUT_MS, + prompt: { ask }, + }) + .then( + (connection) => { + sessions.clearPending(sessionId); + ctx.hosts.status.reportLogin(hostId, { ok: true }); + if (pending.abandoned) { + connection.dispose(); + return; + } + + const session = { + id: sessionId, + client: connection.client, + dispose: connection.dispose, + hostId, + userId, + runtime, + isWindows: false, + lastActive: Date.now(), + activeOperations: 0, + }; + sessions.add(session); + + connection.client.exec("ver", (err, stream) => { + if (err || !stream) return; + let output = ""; + stream.on("data", (d: Buffer) => { + output += d.toString(); + }); + stream.on("close", () => { + if (output.toLowerCase().includes("windows")) { + session.isWindows = true; + } + }); + stream.stderr.on("data", () => {}); + }); + + void ctx.audit.record({ + action: "docker_connect", + resourceType: "host", + resourceId: String(hostId), + resourceName: (host.name as string) || undefined, + success: true, + }); + + logs.push( + connectionLog( + "success", + "connected", + "SSH connection established successfully", + ), + ); + pending.emit({ + status: 200, + body: { success: true, status: "success", connectionLogs: logs }, + }); + }, + (error: unknown) => { + sessions.clearPending(sessionId); + const message = getErrorMessage(error, "SSH connection failed"); + if (pending.abandoned) return; + + const outcome = (error as { outcome?: Record }) + .outcome; + if (outcome?.status === "interaction-required") { + logs.push(connectionLog("error", "docker_auth", message)); + pending.emit({ + status: 401, + body: { + error: message, + requiresAuthInteraction: outcome.interaction, + ...(typeof outcome.flag === "string" + ? { [outcome.flag]: true } + : {}), + connectionLogs: logs, + }, + }); + return; + } + if (outcome?.status === "error") { + logs.push(connectionLog("error", "docker_auth", message)); + pending.emit({ + status: 400, + body: { error: message, connectionLogs: logs }, + }); + return; + } + + const { hostKeyChanged, authFailed } = classifyConnectError( + message, + logs, + ); + if (hostKeyChanged || authFailed) { + ctx.hosts.status.reportLogin(hostId, { ok: false, hostKeyChanged }); + } + log.error("Docker SSH connection failed", { + hostId, + userId, + error: message, + }); + + if (credentialless && authFailed) { + pending.emit({ + status: 200, + body: { + status: "auth_required", + reason: "no_keyboard", + connectionLogs: logs, + }, + }); + return; + } + pending.emit({ + status: 500, + body: { success: false, message, connectionLogs: logs }, + }); + }, + ); + + return pending.next(); +} + +/** Carries a person's answer to a parked connect and waits for what's next. */ +export async function answerConnect( + sessions: DockerSessions, + sessionId: string, + userId: string, + kind: "totp" | "warpgate", + value: string, +): Promise { + const pending = sessions.getPending(sessionId); + if (!(pending instanceof PendingConnect) || pending.userId !== userId) { + return { + status: 404, + body: { error: "Authentication session expired. Please reconnect." }, + }; + } + if (pending.awaiting !== kind) { + return { + status: 400, + body: { + error: + kind === "warpgate" + ? "Session is not a Warpgate session" + : "Session is not waiting for a code", + }, + }; + } + pending.respond(value); + return pending.next(); +} diff --git a/plugins/docker/src/backend/console.ts b/plugins/docker/src/backend/console.ts index 51cc3aee8..76ec2f4fb 100644 --- a/plugins/docker/src/backend/console.ts +++ b/plugins/docker/src/backend/console.ts @@ -1,652 +1,343 @@ -// Core imports below point at TypeScript source so tsc can type-check them. -// The plugin build rewrites the prefix to the compiled output path; see -// packages/plugin-sdk/cli/lib/legacy-core-imports.mjs. -import { getErrorMessage } from "../../../../src/backend/utils/error-message.js"; -import { StringDecoder } from "string_decoder"; -import type { Client as SSHClient } from "ssh2"; -import type { PluginSshHost } from "@termix/plugin-sdk/backend"; -import { connectSsh } from "./ssh.js"; -import { WebSocketServer, WebSocket } from "ws"; -import { AuthManager } from "../../../../src/backend/utils/auth-manager.js"; -import { systemLogger } from "../../../../src/backend/utils/logger.js"; -import type { SSHHost } from "../../../../src/types/index.js"; +import { AsyncResource } from "node:async_hooks"; +import { StringDecoder } from "node:string_decoder"; +import type { Client, ClientChannel } from "ssh2"; +import { WebSocket, type RawData } from "ws"; +import type { + PluginContext, + PluginWebSocketConnection, +} from "@termix/plugin-sdk/backend"; import { containerCommand, - getContainerRuntimeConfig, type ContainerRuntime, } from "./container-runtime.js"; +import { readDockerHostSettings } from "./host-settings.js"; import { - hostAddressMismatch, + CONTAINER_ID_RE, HOST_ADDRESS_MISMATCH_MESSAGE, HOST_NOT_ON_THIS_SERVER_MESSAGE, -} from "../../../../src/backend/hosts/host-identity.js"; -import { extractWebSocketToken } from "../../../../src/backend/utils/ws-auth.js"; -import { asObject, asString, - MAX_WS_MESSAGE_BYTES, + getErrorMessage, + hostAddressMismatch, parseWsMessage, toTerminalDimension, -} from "../../../../src/backend/utils/ws-message.js"; + type DockerLogger, +} from "./helpers.js"; -const sshLogger = systemLogger; +const SHELLS = ["bash", "sh", "ash", "zsh"]; +const PING_MS = 30_000; -interface SSHSession { - client: SSHClient; - stream: import("ssh2").ClientChannel | null; - isConnected: boolean; - containerId?: string; - shell?: string; - hostId?: number; - containerRuntime?: ContainerRuntime; +interface ConsoleSession { + client: Client; + dispose: () => void; + stream: ClientChannel | null; + hostId: number; + containerId: string; + runtime: ContainerRuntime; } -const activeSessions = new Map(); - -/** - * Created by startConsoleServer(), not at module load. - * - * Importing this file used to bind a port as a side effect, which meant the - * plugin could not be disabled and re-enabled: the module stays in the ESM - * cache, so the second activate() got a server that had already been closed. - * There is no port now -- core routes /plugin-ws/docker/console here -- but - * the same lifecycle applies to the server object itself. - */ -let wss: WebSocketServer | null = null; - -function handleConnection(ws: WebSocket, req: import("http").IncomingMessage) { - void onConsoleConnection(ws, req); -} - -/** Builds the console WebSocket server. Call from the plugin's activate(). */ -export function startConsoleServer(): Promise { - if (wss) return Promise.resolve(); - - const server = new WebSocketServer({ - noServer: true, - maxPayload: MAX_WS_MESSAGE_BYTES, - }); - - // One bad socket must not take the server down. - server.on("error", (error) => { - sshLogger.error("Docker console WebSocket server error", error, { - operation: "wss_error", - }); - }); - server.on("connection", handleConnection); - - wss = server; - return Promise.resolve(); -} - -/** - * Hands one upgrade to the console server. - * - * Registered as a public ctx.ws route because onConsoleConnection does its own - * per-message authentication, which is where the container and host checks - * live too. - */ -export function handleConsoleUpgrade( - request: import("http").IncomingMessage, - socket: import("stream").Duplex, - head: Buffer, -): void { - const server = wss; - if (!server) { - socket.destroy(); - return; - } - server.handleUpgrade(request, socket, head, (ws) => { - server.emit("connection", ws, request); - }); -} - -async function detectShell( - session: SSHSession, +function shellExists( + client: Client, + runtime: ContainerRuntime, containerId: string, -): Promise { - const shells = ["bash", "sh", "ash"]; - - for (const shell of shells) { - try { - await new Promise((resolve, reject) => { - session.client.exec( - containerCommand( - session.containerRuntime, - `exec ${containerId} which ${shell}`, - ), - (err, stream) => { - if (err) return reject(err); - - let output = ""; - stream.on("data", (data: Buffer) => { - output += data.toString(); - }); - - stream.on("close", (code: number) => { - if (code === 0 && output.trim()) { - resolve(); - } else { - reject(new Error(`Shell ${shell} not found`)); - } - }); - - stream.stderr.on("data", () => {}); - stream.stderr.on("error", () => {}); - stream.on("error", (streamErr) => { - reject(streamErr); - }); - }, + shell: string, +): Promise { + return new Promise((resolve) => { + client.exec( + containerCommand(runtime, `exec ${containerId} which ${shell}`), + (err, stream) => { + if (err) return resolve(false); + let output = ""; + stream.on("data", (data: Buffer) => { + output += data.toString(); + }); + stream.on("close", (code: number) => + resolve(code === 0 && output.trim().length > 0), ); - }); + stream.stderr.on("data", () => {}); + stream.stderr.on("error", () => {}); + stream.on("error", () => resolve(false)); + }, + ); + }); +} - return shell; - } catch { - continue; - } +async function pickShell( + client: Client, + runtime: ContainerRuntime, + containerId: string, + requested: string | undefined, +): Promise { + if (requested && (await shellExists(client, runtime, containerId, requested))) + return requested; + for (const shell of ["bash", "sh", "ash"]) { + if (await shellExists(client, runtime, containerId, shell)) return shell; } - return "sh"; } -async function onConsoleConnection( - ws: WebSocket, - req: import("http").IncomingMessage, -) { - const token = extractWebSocketToken(req); - - if (!token) { - ws.close(1008, "Authentication required"); - return; - } - - const authManagerInstance = AuthManager.getInstance(); - let payload; - try { - payload = await authManagerInstance.verifyJWTToken(token); - } catch (error) { - sshLogger.warn("Docker console JWT verification failed", { - operation: "docker_console_auth_error", - error: getErrorMessage(error), - }); - ws.close(1008, "Authentication required"); - return; - } - if (!payload?.userId || payload.pendingTOTP) { - ws.close(1008, "Authentication required"); - return; - } - - const userId = payload.userId; - const sessionId = `docker-console-${Date.now()}-${Math.random()}`; - sshLogger.info("Docker console WebSocket connected", { - operation: "docker_console_connect", - sessionId, - userId, +/** + * The container console at /plugin-ws/docker/console. + * + * Public with optionalAuth: core still resolves the caller's token, and the + * handler refuses a socket without a user or without docker.use. Every live + * console is closed when the plugin deactivates. + */ +export function registerConsole(ctx: PluginContext, log: DockerLogger): void { + const live = new Set<() => void>(); + ctx.disposables.add(() => { + for (const end of [...live]) end(); + live.clear(); }); - let sshSession: SSHSession | null = null; - - const wsPingInterval = setInterval(() => { - if (ws.readyState === WebSocket.OPEN) { - ws.ping(); + const onConnection = async (connection: PluginWebSocketConnection) => { + const ws = connection.socket as WebSocket; + const userId = connection.userId; + if (!userId || !(await ctx.rbac.hasFor(userId, "use"))) { + ws.close(1008, "Authentication required"); + return; } - }, 30000); - const cleanup = () => { - clearInterval(wsPingInterval); - if (!sshSession) return; - sshSession.stream?.end(); - sshSession.client.end(); - activeSessions.delete(sessionId); - sshSession = null; - }; + let session: ConsoleSession | null = null; + const send = (message: Record) => { + if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(message)); + }; - ws.on("message", async (data) => { - try { - const message = parseWsMessage(data); + const endSession = () => { + if (!session) return; + const current = session; + session = null; + current.stream?.end(); + current.dispose(); + }; - switch (message.type) { - case "connect": { - const connectData = asObject(message.data); - const hostConfig = asObject( - connectData.hostConfig, - ) as unknown as SSHHost; - const containerId = asString(connectData.containerId); - const shell = asString(connectData.shell) || undefined; - const cols = toTerminalDimension(connectData.cols) || 80; - const rows = toTerminalDimension(connectData.rows) || 24; + const pingTimer = setInterval(() => { + if (ws.readyState === WebSocket.OPEN) ws.ping(); + }, PING_MS); - const hostId = hostConfig?.id; + const cleanup = () => { + clearInterval(pingTimer); + endSession(); + live.delete(shutdown); + }; + const shutdown = () => { + cleanup(); + try { + ws.close(1001, "Docker plugin disabled"); + } catch { + // already closed + } + }; + live.add(shutdown); - if (!hostId || !containerId) { - ws.send( - JSON.stringify({ - type: "error", - message: "Host configuration and container ID are required", - }), - ); - return; - } + const connect = async (data: unknown) => { + if (!connection.isDataUnlocked()) { + send({ + type: "error", + message: "Session expired - please log in again", + }); + return; + } + const payload = asObject(data); + const hostConfig = asObject(payload.hostConfig); + const hostId = Number(hostConfig.id); + const syncId = + typeof hostConfig.syncId === "string" ? hostConfig.syncId : null; + const containerId = asString(payload.containerId); + const shell = asString(payload.shell) || undefined; + const cols = toTerminalDimension(payload.cols) || 80; + const rows = toTerminalDimension(payload.rows) || 24; - if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(containerId)) { - ws.send( - JSON.stringify({ - type: "error", - message: "Invalid container ID", - }), - ); - return; - } + if (!hostId || !containerId) { + send({ + type: "error", + message: "Host configuration and container ID are required", + }); + return; + } + if (!CONTAINER_ID_RE.test(containerId)) { + send({ type: "error", message: "Invalid container ID" }); + return; + } + if (shell && !SHELLS.includes(shell)) { + send({ type: "error", message: "Invalid shell" }); + return; + } - const allowedShells = ["bash", "sh", "ash", "zsh"]; - if (shell && !allowedShells.includes(shell)) { - ws.send( - JSON.stringify({ - type: "error", - message: "Invalid shell", - }), - ); - return; - } + endSession(); - if (!hostConfig.enableDocker) { - ws.send( - JSON.stringify({ - type: "error", - message: "Docker is not enabled on this host", - }), - ); - return; - } - - try { - // Resolve host with credentials server-side - const { resolveHostById, resolveHostBySyncId } = - await import("../../../../src/backend/hosts/host-resolver.js"); - // syncId names the host on both sides of a sync pair; the numeric - // id only names it in the database the client is displaying. - const hostSyncId = hostConfig?.syncId; - const resolvedHost = hostSyncId - ? await resolveHostBySyncId(hostSyncId, userId) - : await resolveHostById(hostId, userId); - - if (!resolvedHost) { - ws.send( - JSON.stringify({ - type: "error", - message: hostSyncId - ? HOST_NOT_ON_THIS_SERVER_MESSAGE - : "Host not found", - }), - ); - return; - } - - // The connection below dials resolvedHost.ip outright, so if this - // server has a different machine under the id the client sent, the - // console opens on that machine's Docker daemon instead. - if ( - !hostSyncId && - hostAddressMismatch(hostConfig?.ip, resolvedHost.ip) - ) { - sshLogger.error( - "Refusing Docker console: host id resolves to a different address here", - undefined, - { - operation: "docker_console_host_id_mismatch", - hostId, - userId, - clientIp: hostConfig?.ip, - resolvedIp: resolvedHost.ip, - }, - ); - ws.send( - JSON.stringify({ - type: "error", - message: HOST_ADDRESS_MISMATCH_MESSAGE, - }), - ); - return; - } - - if (!resolvedHost.enableDocker) { - ws.send( - JSON.stringify({ - type: "error", - message: - "Docker is not enabled for this host. Enable it in Host Settings.", - }), - ); - return; - } - - // The console socket is public and authenticates itself, so the - // acting user is carried on the host rather than the request. - const { client } = await connectSsh( - { - ...(resolvedHost as unknown as PluginSshHost), - userId, - } as PluginSshHost, - { purpose: "docker-console", timeoutMs: 65000 }, - ); - - const { runtime: containerRuntime } = getContainerRuntimeConfig( - resolvedHost.dockerConfig, - ); - - sshSession = { - client, - stream: null, - isConnected: true, - containerId, - hostId: resolvedHost.id, - containerRuntime, - }; - - activeSessions.set(sessionId, sshSession); - - let shellToUse = shell || "bash"; - - if (shell) { - try { - await new Promise((resolve, reject) => { - client.exec( - containerCommand( - containerRuntime, - `exec ${containerId} which ${shell}`, - ), - (err, stream) => { - if (err) return reject(err); - - let output = ""; - stream.on("data", (data: Buffer) => { - output += data.toString(); - }); - - stream.on("close", (code: number) => { - if (code === 0 && output.trim()) { - resolve(); - } else { - reject(new Error(`Shell ${shell} not available`)); - } - }); - - stream.stderr.on("data", () => {}); - stream.stderr.on("error", () => {}); - stream.on("error", (streamErr) => { - reject(streamErr); - }); - }, - ); - }); - } catch { - sshLogger.warn( - `Requested shell ${shell} not found, detecting available shell`, - { - operation: "shell_validation", - sessionId, - containerId, - requestedShell: shell, - }, - ); - shellToUse = await detectShell(sshSession, containerId); - } - } else { - shellToUse = await detectShell(sshSession, containerId); - } - - sshSession.shell = shellToUse; - - const execCommand = containerCommand( - containerRuntime, - `exec -it ${containerId} /bin/${shellToUse}`, - ); - sshLogger.info("Attaching to Docker container", { - operation: "docker_attach", - sessionId, - userId, - hostId: resolvedHost.id, - containerId, - }); - - client.exec( - execCommand, - { - pty: { - term: "xterm-256color", - cols, - rows, - }, - }, - (err, stream) => { - if (err) { - sshLogger.error("Failed to create docker exec", err, { - operation: "docker_exec", - sessionId, - containerId, - }); - - ws.send( - JSON.stringify({ - type: "error", - message: `Failed to start console: ${err.message}`, - }), - ); - return; - } - - sshSession!.stream = stream; - sshLogger.success("Docker container attached", { - operation: "docker_attach_success", - sessionId, - userId, - hostId: resolvedHost.id, - containerId, - }); - - // Buffers incomplete multi-byte UTF-8 sequences across chunk - // boundaries so box-drawing/special characters don't get - // corrupted when a character is split across TCP packets. - const decoder = new StringDecoder("utf-8"); - - stream.on("data", (data: Buffer) => { - if (ws.readyState === WebSocket.OPEN) { - const text = decoder.write(data); - if (!text) return; - ws.send( - JSON.stringify({ - type: "output", - data: text, - }), - ); - } - }); - - stream.stderr.on("data", () => {}); - stream.stderr.on("error", () => {}); - - stream.on("error", (streamErr) => { - sshLogger.error("Docker console stream error", streamErr, { - operation: "docker_console_stream_error", - sessionId, - containerId, - }); - if (ws.readyState === WebSocket.OPEN) { - ws.send( - JSON.stringify({ - type: "error", - message: `Console error: ${streamErr.message}`, - }), - ); - } - }); - - stream.on("close", () => { - if (ws.readyState === WebSocket.OPEN) { - ws.send( - JSON.stringify({ - type: "disconnected", - message: "Console session ended", - }), - ); - } - - if (sshSession) { - sshSession.client.end(); - activeSessions.delete(sessionId); - } - }); - - ws.send( - JSON.stringify({ - type: "connected", - data: { - shell: shellToUse, - requestedShell: shell, - shellChanged: shell && shell !== shellToUse, - }, - }), - ); - }, - ); - } catch (error) { - sshLogger.error("Failed to connect to container", error, { - operation: "console_connect", - sessionId, - containerId, - }); - - ws.send( - JSON.stringify({ - type: "error", - message: getErrorMessage( - error, - "Failed to connect to container", - ), - }), - ); - } - break; + try { + // syncId names the host on both sides of a sync pair; the numeric + // id only names it in the database the client is displaying. + const host = await ctx.ssh.resolveHost(hostId, { syncId }); + if (!host) { + send({ + type: "error", + message: syncId + ? HOST_NOT_ON_THIS_SERVER_MESSAGE + : "Host not found", + }); + return; + } + if (!syncId && hostAddressMismatch(hostConfig.ip, host.ip)) { + log.error("Refusing Docker console: host id resolves elsewhere", { + hostId, + userId, + }); + send({ type: "error", message: HOST_ADDRESS_MISMATCH_MESSAGE }); + return; } - case "input": { - if (sshSession && sshSession.stream) { - const input = asString(message.data); - if (input) sshSession.stream.write(input); - } - break; + const settings = await readDockerHostSettings(ctx, host.id); + if (!settings.enabled) { + send({ + type: "error", + message: + "Docker is not enabled for this host. Enable it in Host Settings.", + }); + return; } - case "resize": { - if (sshSession && sshSession.stream) { - const dimensions = asObject(message.data); - const cols = toTerminalDimension(dimensions.cols); - const rows = toTerminalDimension(dimensions.rows); + const { client, dispose } = await ctx.ssh.connect(host, { + purpose: "docker-console", + timeoutMs: 65_000, + }); + if (ws.readyState !== WebSocket.OPEN) { + dispose(); + return; + } + const current: ConsoleSession = { + client, + dispose, + stream: null, + hostId: host.id, + containerId, + runtime: settings.runtime, + }; + session = current; + + const shellToUse = await pickShell( + client, + settings.runtime, + containerId, + shell, + ); + if (session !== current) return; + + client.exec( + containerCommand( + settings.runtime, + `exec -it ${containerId} /bin/${shellToUse}`, + ), + { pty: { term: "xterm-256color", cols, rows } }, + (err, stream) => { + if (err) { + send({ + type: "error", + message: `Failed to start console: ${err.message}`, + }); + return; + } + if (session !== current) { + stream.end(); + return; + } + current.stream = stream; + + // Buffers split multi-byte UTF-8 sequences across chunks. + const decoder = new StringDecoder("utf-8"); + stream.on("data", (chunk: Buffer) => { + const text = decoder.write(chunk); + if (text) send({ type: "output", data: text }); + }); + stream.stderr.on("data", () => {}); + stream.stderr.on("error", () => {}); + stream.on("error", (streamErr: Error) => { + send({ + type: "error", + message: `Console error: ${streamErr.message}`, + }); + }); + stream.on("close", () => { + send({ type: "disconnected", message: "Console session ended" }); + if (session === current) endSession(); + }); + + send({ + type: "connected", + data: { + shell: shellToUse, + requestedShell: shell, + shellChanged: !!shell && shell !== shellToUse, + }, + }); + }, + ); + } catch (error) { + log.error("Failed to connect to container", error, { + hostId, + containerId, + }); + send({ + type: "error", + message: getErrorMessage(error, "Failed to connect to container"), + }); + } + }; + + ws.on( + "message", + AsyncResource.bind(async (raw: RawData) => { + let type: string; + let data: unknown; + try { + ({ type, data } = parseWsMessage(raw)); + } catch (error) { + send({ type: "error", message: getErrorMessage(error) }); + return; + } + + switch (type) { + case "connect": + await connect(data); + break; + case "input": { + const input = asString(data); + if (input) session?.stream?.write(input); + break; + } + case "resize": { + const size = asObject(data); + const cols = toTerminalDimension(size.cols); + const rows = toTerminalDimension(size.rows); if (cols && rows) - sshSession.stream.setWindow(rows, cols, rows, cols); + session?.stream?.setWindow(rows, cols, rows, cols); + break; } - break; - } - - case "disconnect": { - if (sshSession) { - if (sshSession.stream) { - sshSession.stream.end(); - } - sshSession.client.end(); - activeSessions.delete(sessionId); - - ws.send( - JSON.stringify({ + case "disconnect": + if (session) { + endSession(); + send({ type: "disconnected", message: "Disconnected from container", - }), - ); - } - break; + }); + } + break; + case "ping": + send({ type: "pong" }); + break; + default: + log.warn("Unknown docker console message", { type }); } + }), + ); - case "ping": { - if (ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ type: "pong" })); - } - break; - } - - default: - sshLogger.warn("Unknown message type", { - operation: "ws_message", - type: message.type, - }); - } - } catch (error) { - sshLogger.error("WebSocket message error", error, { - operation: "ws_message", - sessionId, - }); - - ws.send( - JSON.stringify({ - type: "error", - message: getErrorMessage(error, "An error occurred"), - }), - ); - } - }); - - ws.on("close", () => { - sshLogger.info("Docker console disconnected", { - operation: "docker_console_disconnect", - sessionId, - userId, - hostId: sshSession?.hostId, - containerId: sshSession?.containerId, + ws.on("close", cleanup); + ws.on("error", (error: Error) => { + log.error("Docker console socket error", error); + cleanup(); }); - cleanup(); - }); + }; - ws.on("error", (error) => { - sshLogger.error("WebSocket error", error, { - operation: "ws_error", - sessionId, - }); - - cleanup(); - }); -} - -/** - * Closes every live console session and the WebSocket server. Called from the - * plugin's deactivate(). - * - * There is deliberately no process signal handler here. Core owns shutdown: - * gracefulShutdown in src/backend/starter.ts calls shutdownPlugins(), which - * runs every plugin's deactivate. A plugin calling process.exit() itself - * skipped the rest of that sequence, including other plugins and the - * database flush. - */ -export function closeConsoleServer(): Promise { - activeSessions.forEach((session) => { - if (session.stream) { - session.stream.end(); - } - session.client.end(); - }); - - activeSessions.clear(); - - const server = wss; - wss = null; - if (!server) return Promise.resolve(); - - server.off("connection", handleConnection); - for (const client of server.clients) { - try { - client.close(1001, "Docker plugin disabled"); - } catch { - // Already gone. - } - } - return new Promise((resolve) => server.close(() => resolve())); + ctx.ws.route("/console", onConnection, { public: true, optionalAuth: true }); } diff --git a/plugins/docker/src/backend/container-routes.ts b/plugins/docker/src/backend/container-routes.ts deleted file mode 100644 index 6bcacf926..000000000 --- a/plugins/docker/src/backend/container-routes.ts +++ /dev/null @@ -1,1133 +0,0 @@ -// Core imports below point at TypeScript source so tsc can type-check them. -// The plugin build rewrites the prefix to the compiled output path; see -// packages/plugin-sdk/cli/lib/legacy-core-imports.mjs. -import { getErrorMessage } from "../../../../src/backend/utils/error-message.js"; -import type express from "express"; -import { logger } from "../../../../src/backend/utils/logger.js"; -import { - containerCommand, - type ContainerRuntime, -} from "./container-runtime.js"; - -const sshLogger = logger; - -type DockerSession = { - isConnected: boolean; - userId?: string; - lastActive: number; - activeOperations: number; - hostId?: number; - isWindows?: boolean; - containerRuntime?: ContainerRuntime; -}; - -type PendingDockerTotpSession = unknown; - -type ExecuteDockerCommand = ( - session: DockerSession, - command: string, - sessionId: string, - userId: string, - hostId?: number, -) => Promise; - -type DockerContainerRoutesDeps = { - sshSessions: Record; - pendingTOTPSessions: Record; - getRequestUserId: (req: express.Request) => string | undefined; - executeDockerCommand: ExecuteDockerCommand; - dockerTimestampPattern: RegExp; -}; - -export function registerDockerContainerRoutes( - app: express.Express, - { - sshSessions, - pendingTOTPSessions, - getRequestUserId, - executeDockerCommand, - dockerTimestampPattern: DOCKER_TIMESTAMP_RE, - }: DockerContainerRoutesDeps, -): void { - const getRuntime = (session: DockerSession) => - session.containerRuntime ?? "docker"; - const getOwnedSession = (sessionId: string, userId: string) => { - const session = sshSessions[sessionId]; - return session?.userId === userId ? session : undefined; - }; - - /** - * @openapi - * /docker/containers/{sessionId}: - * get: - * summary: List all containers - * description: Lists all Docker containers on the host. - * tags: - * - Docker - * parameters: - * - in: path - * name: sessionId - * required: true - * schema: - * type: string - * - in: query - * name: all - * schema: - * type: boolean - * responses: - * 200: - * description: A list of containers. - * 400: - * description: SSH session not found or not connected. - * 500: - * description: Failed to list containers. - */ - app.get("/docker/containers/:sessionId", async (req, res) => { - const { sessionId } = req.params; - const all = req.query.all !== "false"; - const userId = getRequestUserId(req); - - if (!userId) { - return res.status(401).json({ error: "Authentication required" }); - } - - if (pendingTOTPSessions[sessionId]) { - return res.status(400).json({ - error: "Connection pending authentication", - code: "AUTH_PENDING", - }); - } - - const session = getOwnedSession(sessionId, userId); - - if (!session || !session.isConnected) { - return res.status(400).json({ - error: "SSH session not found or not connected", - }); - } - - session.lastActive = Date.now(); - session.activeOperations++; - - try { - const allFlag = all ? "-a " : ""; - const formatStr = session.isWindows - ? `"{\\"id\\":\\"{{.ID}}\\",\\"name\\":\\"{{.Names}}\\",\\"image\\":\\"{{.Image}}\\",\\"status\\":\\"{{.Status}}\\",\\"state\\":\\"{{.State}}\\",\\"ports\\":\\"{{.Ports}}\\",\\"created\\":\\"{{.CreatedAt}}\\"}"` - : `'{"id":"{{.ID}}","name":"{{.Names}}","image":"{{.Image}}","status":"{{.Status}}","state":"{{.State}}","ports":"{{.Ports}}","created":"{{.CreatedAt}}"}' `; - const command = containerCommand( - getRuntime(session), - `ps ${allFlag}--format ${formatStr}`, - ); - - const output = await executeDockerCommand( - session, - command, - sessionId, - userId, - session.hostId, - ); - - const containers = output - .split("\n") - .filter((line) => line.trim()) - .map((line) => { - try { - return JSON.parse(line); - } catch { - sshLogger.warn("Failed to parse container line", { - operation: "parse_container", - line, - }); - return null; - } - }) - .filter((c) => c !== null); - - session.activeOperations--; - - res.json(containers); - } catch (error) { - session.activeOperations--; - sshLogger.error("Failed to list Docker containers", error, { - operation: "list_containers", - sessionId, - userId, - }); - - res.status(500).json({ - error: getErrorMessage(error, "Failed to list containers"), - }); - } - }); - - /** - * @openapi - * /docker/containers/{sessionId}/{containerId}: - * get: - * summary: Get container details - * description: Retrieves detailed information about a specific container. - * tags: - * - Docker - * parameters: - * - in: path - * name: sessionId - * required: true - * schema: - * type: string - * - in: path - * name: containerId - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Container details. - * 400: - * description: SSH session not found or not connected. - * 404: - * description: Container not found. - * 500: - * description: Failed to get container details. - */ - app.get("/docker/containers/:sessionId/:containerId", async (req, res) => { - const { sessionId, containerId } = req.params; - const userId = getRequestUserId(req); - - if (!userId) { - return res.status(401).json({ error: "Authentication required" }); - } - - const session = getOwnedSession(sessionId, userId); - - if (!session || !session.isConnected) { - return res.status(400).json({ - error: "SSH session not found or not connected", - }); - } - - session.lastActive = Date.now(); - session.activeOperations++; - - try { - const command = containerCommand( - getRuntime(session), - `inspect ${containerId}`, - ); - const output = await executeDockerCommand( - session, - command, - sessionId, - userId, - session.hostId, - ); - const details = JSON.parse(output); - - session.activeOperations--; - - if (details && details.length > 0) { - res.json(details[0]); - } else { - res.status(404).json({ - error: "Container not found", - code: "CONTAINER_NOT_FOUND", - }); - } - } catch (error) { - session.activeOperations--; - - const errorMsg = getErrorMessage(error, ""); - if (errorMsg.includes("No such container")) { - return res.status(404).json({ - error: "Container not found", - code: "CONTAINER_NOT_FOUND", - }); - } - - sshLogger.error("Failed to get container details", error, { - operation: "get_container_details", - sessionId, - containerId, - userId, - }); - - res.status(500).json({ - error: errorMsg || "Failed to get container details", - }); - } - }); - - /** - * @openapi - * /docker/containers/{sessionId}/{containerId}/start: - * post: - * summary: Start container - * description: Starts a specific container. - * tags: - * - Docker - * parameters: - * - in: path - * name: sessionId - * required: true - * schema: - * type: string - * - in: path - * name: containerId - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Container started successfully. - * 400: - * description: SSH session not found or not connected. - * 404: - * description: Container not found. - * 500: - * description: Failed to start container. - */ - app.post( - "/docker/containers/:sessionId/:containerId/start", - async (req, res) => { - const { sessionId, containerId } = req.params; - const userId = getRequestUserId(req); - - if (!userId) { - return res.status(401).json({ error: "Authentication required" }); - } - - const session = getOwnedSession(sessionId, userId); - - if (!session || !session.isConnected) { - return res.status(400).json({ - error: "SSH session not found or not connected", - }); - } - - session.lastActive = Date.now(); - session.activeOperations++; - - try { - sshLogger.info("Docker container operation", { - operation: "docker_container_op", - sessionId, - userId, - hostId: session.hostId, - containerId, - action: "start", - }); - await executeDockerCommand( - session, - containerCommand(getRuntime(session), `start ${containerId}`), - sessionId, - userId, - session.hostId, - ); - - session.activeOperations--; - - res.json({ - success: true, - message: "Container started successfully", - }); - } catch (error) { - session.activeOperations--; - - const errorMsg = getErrorMessage(error, ""); - if (errorMsg.includes("No such container")) { - return res.status(404).json({ - success: false, - error: "Container not found", - code: "CONTAINER_NOT_FOUND", - }); - } - - sshLogger.error("Failed to start container", error, { - operation: "start_container", - sessionId, - containerId, - userId, - }); - - res.status(500).json({ - success: false, - error: errorMsg || "Failed to start container", - }); - } - }, - ); - - /** - * @openapi - * /docker/containers/{sessionId}/{containerId}/stop: - * post: - * summary: Stop container - * description: Stops a specific container. - * tags: - * - Docker - * parameters: - * - in: path - * name: sessionId - * required: true - * schema: - * type: string - * - in: path - * name: containerId - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Container stopped successfully. - * 400: - * description: SSH session not found or not connected. - * 404: - * description: Container not found. - * 500: - * description: Failed to stop container. - */ - app.post( - "/docker/containers/:sessionId/:containerId/stop", - async (req, res) => { - const { sessionId, containerId } = req.params; - const userId = getRequestUserId(req); - - if (!userId) { - return res.status(401).json({ error: "Authentication required" }); - } - - const session = getOwnedSession(sessionId, userId); - - if (!session || !session.isConnected) { - return res.status(400).json({ - error: "SSH session not found or not connected", - }); - } - - session.lastActive = Date.now(); - session.activeOperations++; - - try { - sshLogger.info("Docker container operation", { - operation: "docker_container_op", - sessionId, - userId, - hostId: session.hostId, - containerId, - action: "stop", - }); - await executeDockerCommand( - session, - containerCommand(getRuntime(session), `stop ${containerId}`), - sessionId, - userId, - session.hostId, - ); - - session.activeOperations--; - - res.json({ - success: true, - message: "Container stopped successfully", - }); - } catch (error) { - session.activeOperations--; - - const errorMsg = getErrorMessage(error, ""); - if (errorMsg.includes("No such container")) { - return res.status(404).json({ - success: false, - error: "Container not found", - code: "CONTAINER_NOT_FOUND", - }); - } - - sshLogger.error("Failed to stop container", error, { - operation: "stop_container", - sessionId, - containerId, - userId, - }); - - res.status(500).json({ - success: false, - error: errorMsg || "Failed to stop container", - }); - } - }, - ); - - /** - * @openapi - * /docker/containers/{sessionId}/{containerId}/restart: - * post: - * summary: Restart container - * description: Restarts a specific container. - * tags: - * - Docker - * parameters: - * - in: path - * name: sessionId - * required: true - * schema: - * type: string - * - in: path - * name: containerId - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Container restarted successfully. - * 400: - * description: SSH session not found or not connected. - * 404: - * description: Container not found. - * 500: - * description: Failed to restart container. - */ - app.post( - "/docker/containers/:sessionId/:containerId/restart", - async (req, res) => { - const { sessionId, containerId } = req.params; - const userId = getRequestUserId(req); - - if (!userId) { - return res.status(401).json({ error: "Authentication required" }); - } - - const session = getOwnedSession(sessionId, userId); - - if (!session || !session.isConnected) { - return res.status(400).json({ - error: "SSH session not found or not connected", - }); - } - - session.lastActive = Date.now(); - session.activeOperations++; - - try { - sshLogger.info("Docker container operation", { - operation: "docker_container_op", - sessionId, - userId, - hostId: session.hostId, - containerId, - action: "restart", - }); - await executeDockerCommand( - session, - containerCommand(getRuntime(session), `restart ${containerId}`), - sessionId, - userId, - session.hostId, - ); - - session.activeOperations--; - - res.json({ - success: true, - message: "Container restarted successfully", - }); - } catch (error) { - session.activeOperations--; - - const errorMsg = getErrorMessage(error, ""); - if (errorMsg.includes("No such container")) { - return res.status(404).json({ - success: false, - error: "Container not found", - code: "CONTAINER_NOT_FOUND", - }); - } - - sshLogger.error("Failed to restart container", error, { - operation: "restart_container", - sessionId, - containerId, - userId, - }); - - res.status(500).json({ - success: false, - error: errorMsg || "Failed to restart container", - }); - } - }, - ); - - /** - * @openapi - * /docker/containers/{sessionId}/{containerId}/pause: - * post: - * summary: Pause container - * description: Pauses a specific container. - * tags: - * - Docker - * parameters: - * - in: path - * name: sessionId - * required: true - * schema: - * type: string - * - in: path - * name: containerId - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Container paused successfully. - * 400: - * description: SSH session not found or not connected. - * 404: - * description: Container not found. - * 500: - * description: Failed to pause container. - */ - app.post( - "/docker/containers/:sessionId/:containerId/pause", - async (req, res) => { - const { sessionId, containerId } = req.params; - const userId = getRequestUserId(req); - - if (!userId) { - return res.status(401).json({ error: "Authentication required" }); - } - - const session = getOwnedSession(sessionId, userId); - - if (!session || !session.isConnected) { - return res.status(400).json({ - error: "SSH session not found or not connected", - }); - } - - session.lastActive = Date.now(); - session.activeOperations++; - - try { - sshLogger.info("Docker container operation", { - operation: "docker_container_op", - sessionId, - userId, - hostId: session.hostId, - containerId, - action: "pause", - }); - await executeDockerCommand( - session, - containerCommand(getRuntime(session), `pause ${containerId}`), - sessionId, - userId, - session.hostId, - ); - - session.activeOperations--; - - res.json({ - success: true, - message: "Container paused successfully", - }); - } catch (error) { - session.activeOperations--; - - const errorMsg = getErrorMessage(error, ""); - if (errorMsg.includes("No such container")) { - return res.status(404).json({ - success: false, - error: "Container not found", - code: "CONTAINER_NOT_FOUND", - }); - } - - sshLogger.error("Failed to pause container", error, { - operation: "pause_container", - sessionId, - containerId, - userId, - }); - - res.status(500).json({ - success: false, - error: errorMsg || "Failed to pause container", - }); - } - }, - ); - - /** - * @openapi - * /docker/containers/{sessionId}/{containerId}/unpause: - * post: - * summary: Unpause container - * description: Unpauses a specific container. - * tags: - * - Docker - * parameters: - * - in: path - * name: sessionId - * required: true - * schema: - * type: string - * - in: path - * name: containerId - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Container unpaused successfully. - * 400: - * description: SSH session not found or not connected. - * 404: - * description: Container not found. - * 500: - * description: Failed to unpause container. - */ - app.post( - "/docker/containers/:sessionId/:containerId/unpause", - async (req, res) => { - const { sessionId, containerId } = req.params; - const userId = getRequestUserId(req); - - if (!userId) { - return res.status(401).json({ error: "Authentication required" }); - } - - const session = getOwnedSession(sessionId, userId); - - if (!session || !session.isConnected) { - return res.status(400).json({ - error: "SSH session not found or not connected", - }); - } - - session.lastActive = Date.now(); - session.activeOperations++; - - try { - sshLogger.info("Docker container operation", { - operation: "docker_container_op", - sessionId, - userId, - hostId: session.hostId, - containerId, - action: "unpause", - }); - await executeDockerCommand( - session, - containerCommand(getRuntime(session), `unpause ${containerId}`), - sessionId, - userId, - session.hostId, - ); - - session.activeOperations--; - - res.json({ - success: true, - message: "Container unpaused successfully", - }); - } catch (error) { - session.activeOperations--; - - const errorMsg = getErrorMessage(error, ""); - if (errorMsg.includes("No such container")) { - return res.status(404).json({ - success: false, - error: "Container not found", - code: "CONTAINER_NOT_FOUND", - }); - } - - sshLogger.error("Failed to unpause container", error, { - operation: "unpause_container", - sessionId, - containerId, - userId, - }); - - res.status(500).json({ - success: false, - error: errorMsg || "Failed to unpause container", - }); - } - }, - ); - - /** - * @openapi - * /docker/containers/{sessionId}/{containerId}/remove: - * delete: - * summary: Remove container - * description: Removes a specific container. - * tags: - * - Docker - * parameters: - * - in: path - * name: sessionId - * required: true - * schema: - * type: string - * - in: path - * name: containerId - * required: true - * schema: - * type: string - * - in: query - * name: force - * schema: - * type: boolean - * responses: - * 200: - * description: Container removed successfully. - * 400: - * description: SSH session not found or not connected, or cannot remove a running container. - * 404: - * description: Container not found. - * 500: - * description: Failed to remove container. - */ - app.delete( - "/docker/containers/:sessionId/:containerId/remove", - async (req, res) => { - const { sessionId, containerId } = req.params; - const force = req.query.force === "true"; - const userId = getRequestUserId(req); - - if (!userId) { - return res.status(401).json({ error: "Authentication required" }); - } - - const session = getOwnedSession(sessionId, userId); - - if (!session || !session.isConnected) { - return res.status(400).json({ - error: "SSH session not found or not connected", - }); - } - - session.lastActive = Date.now(); - session.activeOperations++; - - try { - sshLogger.info("Docker container operation", { - operation: "docker_container_op", - sessionId, - userId, - hostId: session.hostId, - containerId, - action: "remove", - }); - const forceFlag = force ? "-f " : ""; - await executeDockerCommand( - session, - containerCommand( - getRuntime(session), - `rm ${forceFlag}${containerId}`, - ), - sessionId, - userId, - session.hostId, - ); - - session.activeOperations--; - - res.json({ - success: true, - message: "Container removed successfully", - }); - } catch (error) { - session.activeOperations--; - - const errorMsg = getErrorMessage(error, ""); - if (errorMsg.includes("No such container")) { - return res.status(404).json({ - success: false, - error: "Container not found", - code: "CONTAINER_NOT_FOUND", - }); - } - - if (errorMsg.includes("cannot remove a running container")) { - return res.status(400).json({ - success: false, - error: - "Cannot remove a running container. Stop it first or use force.", - code: "CONTAINER_RUNNING", - }); - } - - sshLogger.error("Failed to remove container", error, { - operation: "remove_container", - sessionId, - containerId, - userId, - }); - - res.status(500).json({ - success: false, - error: errorMsg || "Failed to remove container", - }); - } - }, - ); - - /** - * @openapi - * /docker/containers/{sessionId}/{containerId}/logs: - * get: - * summary: Get container logs - * description: Retrieves logs for a specific container. - * tags: - * - Docker - * parameters: - * - in: path - * name: sessionId - * required: true - * schema: - * type: string - * - in: path - * name: containerId - * required: true - * schema: - * type: string - * - in: query - * name: tail - * schema: - * type: integer - * - in: query - * name: timestamps - * schema: - * type: boolean - * - in: query - * name: since - * schema: - * type: string - * - in: query - * name: until - * schema: - * type: string - * responses: - * 200: - * description: Container logs. - * 400: - * description: SSH session not found or not connected. - * 404: - * description: Container not found. - * 500: - * description: Failed to get container logs. - */ - app.get( - "/docker/containers/:sessionId/:containerId/logs", - async (req, res) => { - const { sessionId, containerId } = req.params; - const tail = req.query.tail ? parseInt(req.query.tail as string) : 100; - const timestamps = req.query.timestamps === "true"; - const since = req.query.since as string; - const until = req.query.until as string; - const userId = getRequestUserId(req); - - if (!userId) { - return res.status(401).json({ error: "Authentication required" }); - } - - const session = getOwnedSession(sessionId, userId); - - if (!session || !session.isConnected) { - return res.status(400).json({ - error: "SSH session not found or not connected", - }); - } - - session.lastActive = Date.now(); - session.activeOperations++; - - try { - let command = containerCommand( - getRuntime(session), - `logs ${containerId}`, - ); - - if (tail && tail > 0) { - command += ` --tail ${Math.floor(tail)}`; - } - - if (timestamps) { - command += " --timestamps"; - } - - if (since && DOCKER_TIMESTAMP_RE.test(since)) { - command += ` --since ${since}`; - } - - if (until && DOCKER_TIMESTAMP_RE.test(until)) { - command += ` --until ${until}`; - } - - command += " 2>&1"; - - const logs = await executeDockerCommand( - session, - command, - sessionId, - userId, - session.hostId, - ); - - session.activeOperations--; - - res.json({ - success: true, - logs, - }); - } catch (error) { - session.activeOperations--; - - const errorMsg = getErrorMessage(error, ""); - if (errorMsg.includes("No such container")) { - return res.status(404).json({ - success: false, - error: "Container not found", - code: "CONTAINER_NOT_FOUND", - }); - } - - sshLogger.error("Failed to get container logs", error, { - operation: "get_logs", - sessionId, - containerId, - userId, - }); - - res.status(500).json({ - success: false, - error: errorMsg || "Failed to get container logs", - }); - } - }, - ); - - /** - * @openapi - * /docker/containers/{sessionId}/{containerId}/stats: - * get: - * summary: Get container stats - * description: Retrieves stats for a specific container. - * tags: - * - Docker - * parameters: - * - in: path - * name: sessionId - * required: true - * schema: - * type: string - * - in: path - * name: containerId - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Container stats. - * 400: - * description: SSH session not found or not connected. - * 404: - * description: Container not found. - * 500: - * description: Failed to get container stats. - */ - app.get( - "/docker/containers/:sessionId/:containerId/stats", - async (req, res) => { - const { sessionId, containerId } = req.params; - const userId = getRequestUserId(req); - - if (!userId) { - return res.status(401).json({ error: "Authentication required" }); - } - - const session = getOwnedSession(sessionId, userId); - - if (!session || !session.isConnected) { - return res.status(400).json({ - error: "SSH session not found or not connected", - }); - } - - session.lastActive = Date.now(); - session.activeOperations++; - - try { - const statsFormatStr = session.isWindows - ? `"{\\"cpu\\":\\"{{.CPUPerc}}\\",\\"memory\\":\\"{{.MemUsage}}\\",\\"memoryPercent\\":\\"{{.MemPerc}}\\",\\"netIO\\":\\"{{.NetIO}}\\",\\"blockIO\\":\\"{{.BlockIO}}\\",\\"pids\\":\\"{{.PIDs}}\\"}"` - : `'{"cpu":"{{.CPUPerc}}","memory":"{{.MemUsage}}","memoryPercent":"{{.MemPerc}}","netIO":"{{.NetIO}}","blockIO":"{{.BlockIO}}","pids":"{{.PIDs}}"}' `; - const command = containerCommand( - getRuntime(session), - `stats ${containerId} --no-stream --format ${statsFormatStr}`, - ); - - const output = await executeDockerCommand( - session, - command, - sessionId, - userId, - session.hostId, - ); - const rawStats = JSON.parse(output.trim()); - - const memoryParts = rawStats.memory.split(" / "); - const memoryUsed = memoryParts[0]?.trim() || "0B"; - const memoryLimit = memoryParts[1]?.trim() || "0B"; - - const netIOParts = rawStats.netIO.split(" / "); - const netInput = netIOParts[0]?.trim() || "0B"; - const netOutput = netIOParts[1]?.trim() || "0B"; - - const blockIOParts = rawStats.blockIO.split(" / "); - const blockRead = blockIOParts[0]?.trim() || "0B"; - const blockWrite = blockIOParts[1]?.trim() || "0B"; - - const stats = { - cpu: rawStats.cpu, - memoryUsed, - memoryLimit, - memoryPercent: rawStats.memoryPercent, - netInput, - netOutput, - blockRead, - blockWrite, - pids: rawStats.pids, - }; - - session.activeOperations--; - - res.json(stats); - } catch (error) { - session.activeOperations--; - - const errorMsg = getErrorMessage(error, ""); - if (errorMsg.includes("No such container")) { - return res.status(404).json({ - success: false, - error: "Container not found", - code: "CONTAINER_NOT_FOUND", - }); - } - - sshLogger.error("Failed to get container stats", error, { - operation: "get_stats", - sessionId, - containerId, - userId, - }); - - res.status(500).json({ - success: false, - error: errorMsg || "Failed to get container stats", - }); - } - }, - ); -} diff --git a/plugins/docker/src/backend/container-runtime.ts b/plugins/docker/src/backend/container-runtime.ts index 00c740d5e..d896df299 100644 --- a/plugins/docker/src/backend/container-runtime.ts +++ b/plugins/docker/src/backend/container-runtime.ts @@ -4,33 +4,6 @@ export function normalizeContainerRuntime(value: unknown): ContainerRuntime { return value === "podman" ? "podman" : "docker"; } -export function getContainerRuntimeConfig(raw: unknown): { - runtime: ContainerRuntime; -} { - if (!raw) { - return { runtime: "docker" }; - } - - let config: unknown = raw; - if (typeof raw === "string") { - try { - config = JSON.parse(raw) as Record; - } catch { - return { runtime: "docker" }; - } - } - - if (!config || typeof config !== "object") { - return { runtime: "docker" }; - } - - return { - runtime: normalizeContainerRuntime( - (config as Record).runtime, - ), - }; -} - /** * Non-interactive SSH shells don't source ~/.zprofile or ~/.bash_profile, * so PATH additions from installers like Homebrew or OrbStack are missing. diff --git a/plugins/docker/src/backend/container-state.ts b/plugins/docker/src/backend/container-state.ts new file mode 100644 index 000000000..a56bb0782 --- /dev/null +++ b/plugins/docker/src/backend/container-state.ts @@ -0,0 +1,84 @@ +/** + * Container state snapshots and the events two of them imply, for the + * docker.events service. + * + * Events come from diffing successive `ps -a` snapshots, so the poll interval + * is the resolution: a container that stops and starts between two polls is + * not reported. That is the tradeoff for not holding a `docker events` stream + * open against every watched host. + */ + +export interface ContainerState { + /** Docker's own state word: running, exited, restarting, ... */ + state: string; + /** Health from the status text, when the image declares a healthcheck. */ + unhealthy: boolean; +} + +export type DockerEventName = "exited" | "started" | "unhealthy" | "restarting"; + +export const PS_STATE_FORMAT = `'{"name":"{{.Names}}","state":"{{.State}}","status":"{{.Status}}"}'`; + +/** Parses `ps -a --format PS_STATE_FORMAT` output, one JSON object per line. */ +export function parseContainerStates( + output: string, +): Map { + const states = new Map(); + + for (const line of output.split("\n")) { + const trimmed = line.trim(); + if (!trimmed) continue; + + try { + const parsed = JSON.parse(trimmed) as { + name?: string; + state?: string; + status?: string; + }; + if (!parsed.name) continue; + + states.set(parsed.name, { + state: (parsed.state ?? "").toLowerCase(), + unhealthy: /\(unhealthy\)/i.test(parsed.status ?? ""), + }); + } catch { + // A partial line is not worth failing the whole poll over. + } + } + + return states; +} + +/** + * The events a pair of snapshots implies. A container missing from the + * previous snapshot counts as newly seen rather than started, so the first + * poll after a restart does not replay every running container. + */ +export function diffContainerStates( + previous: Map, + current: Map, +): Array<{ container: string; event: DockerEventName }> { + const events: Array<{ container: string; event: DockerEventName }> = []; + + for (const [name, now] of current) { + const before = previous.get(name); + if (!before) continue; + + if (before.state !== now.state) { + if (now.state === "exited") + events.push({ container: name, event: "exited" }); + else if (now.state === "running") + events.push({ container: name, event: "started" }); + else if (now.state === "restarting") + events.push({ container: name, event: "restarting" }); + } + + // Health is independent of state: a container can go unhealthy while it + // stays up, which is exactly the case worth alerting on. + if (!before.unhealthy && now.unhealthy) { + events.push({ container: name, event: "unhealthy" }); + } + } + + return events; +} diff --git a/plugins/docker/src/backend/helpers.ts b/plugins/docker/src/backend/helpers.ts new file mode 100644 index 000000000..70f4ab4b1 --- /dev/null +++ b/plugins/docker/src/backend/helpers.ts @@ -0,0 +1,154 @@ +import type { RawData } from "ws"; +import type { PluginLogger } from "@termix/plugin-sdk/backend"; + +export function getErrorMessage( + error: unknown, + fallback = "Unknown error", +): string { + return error instanceof Error ? error.message : fallback; +} + +export type Meta = Record; + +export interface DockerLogger { + info: (message: string, meta?: Meta) => void; + warn: (message: string, meta?: Meta) => void; + /** An error, or structured context, or both. */ + error: (message: string, errorOrMeta?: unknown, meta?: Meta) => void; +} + +function withMeta(message: string, meta?: Meta): string { + if (!meta || Object.keys(meta).length === 0) return message; + try { + return `${message} ${JSON.stringify(meta)}`; + } catch { + return message; + } +} + +/** ctx.log with the structured context the old core logger carried. */ +export function createLogger(log: PluginLogger): DockerLogger { + return { + info: (message, meta) => log.info(withMeta(message, meta)), + warn: (message, meta) => log.warn(withMeta(message, meta)), + error: (message, errorOrMeta, meta) => { + if (errorOrMeta instanceof Error) { + log.error(withMeta(message, meta), errorOrMeta); + return; + } + log.error( + withMeta(message, { + ...((errorOrMeta as Meta | undefined) ?? {}), + ...meta, + }), + ); + }, + }; +} + +export type LogLevel = "info" | "success" | "warning" | "error"; + +/** One line of the connection log the panel shows while connecting. */ +export interface ConnectionLogLine { + type: LogLevel; + stage: string; + message: string; +} + +export function connectionLog( + type: LogLevel, + stage: string, + message: string, +): ConnectionLogLine { + return { type, stage, message }; +} + +export const CONTAINER_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/; +export const DOCKER_TIMESTAMP_RE = /^[0-9T:.Z+-]+$/; + +// Cap on a single decoded text frame. Control messages are tiny, and console +// input is bounded by what a user can type or paste. +export const MAX_WS_MESSAGE_BYTES = 1024 * 1024; + +function rawByteLength(raw: RawData): number { + if (Buffer.isBuffer(raw)) return raw.length; + if (Array.isArray(raw)) + return raw.reduce((sum, part) => sum + part.length, 0); + if (raw instanceof ArrayBuffer) return raw.byteLength; + return 0; +} + +/** Parses a frame into `{ type, data }`, throwing on anything else. */ +export function parseWsMessage(raw: RawData): { type: string; data: unknown } { + if (rawByteLength(raw) > MAX_WS_MESSAGE_BYTES) { + throw new Error("Message too large"); + } + let parsed: unknown; + try { + parsed = JSON.parse(raw.toString()); + } catch { + throw new Error("Invalid JSON"); + } + if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error("Message must be a JSON object"); + } + const { type, data } = parsed as { type?: unknown; data?: unknown }; + if (typeof type !== "string") { + throw new Error("Message type must be a string"); + } + return { type, data }; +} + +export function asObject(data: unknown): Record { + return data !== null && typeof data === "object" && !Array.isArray(data) + ? (data as Record) + : {}; +} + +export function asString(value: unknown): string { + return typeof value === "string" ? value : ""; +} + +/** A sane integer width or height, or 0 when the value is unusable. */ +export function toTerminalDimension(value: unknown): number { + const n = typeof value === "number" ? value : Number(value); + if (!Number.isFinite(n)) return 0; + const rounded = Math.floor(n); + if (rounded < 1) return 0; + return Math.min(rounded, 10000); +} + +function normalizeHostAddress(value: unknown): string { + if (typeof value !== "string") return ""; + return value + .replace(/^\[|\]$/g, "") + .trim() + .toLowerCase(); +} + +/** + * Whether a numeric host id resolved to a different machine than the client + * meant. Ids from the desktop app and a sync server drift apart, and the + * resolved row supplies the credentials, so a mismatch must be refused. + */ +export function hostAddressMismatch( + clientAddress: unknown, + resolvedAddress: unknown, +): boolean { + const resolved = normalizeHostAddress(resolvedAddress); + if (!resolved) return false; + return resolved !== normalizeHostAddress(clientAddress); +} + +export const HOST_ADDRESS_MISMATCH_MESSAGE = + "Host mismatch: this server resolved the selected host to a different machine, so the connection was refused. " + + "The host ids on this device and on the sync server have drifted apart. " + + 'Set the connection origin to "This device" for this host, or re-run a full sync, then try again.'; + +export const HOST_NOT_ON_THIS_SERVER_MESSAGE = + "This host does not exist on the sync server, so the connection was refused. " + + 'Run a sync so the server knows about it, or set the connection origin to "This device" for this host.'; + +export function newSessionId(prefix: string): string { + return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; +} diff --git a/plugins/docker/src/backend/host-settings.ts b/plugins/docker/src/backend/host-settings.ts new file mode 100644 index 000000000..c449b8f86 --- /dev/null +++ b/plugins/docker/src/backend/host-settings.ts @@ -0,0 +1,70 @@ +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import { + normalizeContainerRuntime, + type ContainerRuntime, +} from "./container-runtime.js"; + +export const PLUGIN_ID = "docker"; + +export interface DockerHostSettings { + enabled: boolean; + runtime: ContainerRuntime; +} + +export async function readDockerHostSettings( + ctx: PluginContext, + hostId: number, +): Promise { + const values = await ctx.settings.getAll("host", hostId); + return { + enabled: values.enableDocker === true, + runtime: normalizeContainerRuntime(values.containerRuntime), + }; +} + +function asObject(value: unknown): Record | undefined { + let parsed = value; + if (typeof value === "string") { + try { + parsed = JSON.parse(value); + } catch { + return undefined; + } + } + return parsed && typeof parsed === "object" && !Array.isArray(parsed) + ? (parsed as Record) + : undefined; +} + +function asBool(value: unknown): boolean | undefined { + if (typeof value === "boolean") return value; + if (value === 1 || value === "1" || value === "true") return true; + if (value === 0 || value === "0" || value === "false") return false; + return undefined; +} + +/** + * The docker host settings in an imported or created host row: the nested + * `pluginSettings.docker` of a Termix export first, then the flat + * `enableDocker` and `dockerConfig.runtime` hosts had before 2.9.0 (older + * exports, proxmox guests marked as Docker hosts). + */ +export function normalizeImportedHost( + raw: Record, +): Record | null { + const nested = asObject(asObject(raw.pluginSettings)?.[PLUGIN_ID]) ?? {}; + const out: Record = {}; + + const enabled = asBool( + nested.enableDocker !== undefined ? nested.enableDocker : raw.enableDocker, + ); + if (enabled !== undefined) out.enableDocker = enabled; + + const runtime = + nested.containerRuntime ?? asObject(raw.dockerConfig)?.runtime; + if (runtime === "docker" || runtime === "podman") { + out.containerRuntime = runtime; + } + + return Object.keys(out).length > 0 ? out : null; +} diff --git a/plugins/docker/src/backend/index.ts b/plugins/docker/src/backend/index.ts index edbc3c06f..7af6b0d91 100644 --- a/plugins/docker/src/backend/index.ts +++ b/plugins/docker/src/backend/index.ts @@ -1,82 +1,33 @@ -import express, { type Router } from "express"; +import type { Router } from "express"; import type { PluginContext } from "@termix/plugin-sdk/backend"; -import { setPluginSsh } from "./ssh.js"; -import { logger } from "../../../../src/backend/utils/logger.js"; -import { AuthManager } from "../../../../src/backend/utils/auth-manager.js"; -import { registerDockerContainerRoutes } from "./container-routes.js"; -import { - sshSessions, - pendingTOTPSessions, - executeDockerCommand, -} from "./session-manager.js"; -import { - DOCKER_TIMESTAMP_RE, - getRequestUserId, - registerDockerSshRoutes, -} from "./routes.js"; -import { - closeConsoleServer, - handleConsoleUpgrade, - startConsoleServer, -} from "./console.js"; +import { createLogger } from "./helpers.js"; +import { createDockerSessions } from "./sessions.js"; +import { registerRoutes } from "./routes.js"; +import { registerConsole } from "./console.js"; +import { registerServices } from "./services.js"; +import { normalizeImportedHost } from "./host-settings.js"; -let authManagerInstance: ReturnType | null = - null; +export type { + DockerContainerAction, + DockerContainerSummary, + DockerEvent, + DockerEventsV1, + DockerServiceV1, +} from "./services.js"; export async function activate(ctx: PluginContext) { - setPluginSsh(ctx.ssh); - ctx.disposables.add(() => setPluginSsh(null)); - // An express app rather than the router directly, so the existing - // register*Routes helpers keep the Application they expect. Core mounts it - // at /plugin-api/docker and runs compression, CORS, cookies, auth and body - // parsing in front of it. - const app = express(); - app.use((_req, res, next) => { - res.setHeader("Cache-Control", "no-store"); - next(); - }); + const log = createLogger(ctx.log); + const sessions = createDockerSessions(ctx.schedule, log); + ctx.disposables.add(() => sessions.closeAll()); - registerDockerSshRoutes(app); - registerDockerContainerRoutes(app, { - sshSessions, - pendingTOTPSessions, - getRequestUserId, - executeDockerCommand, - dockerTimestampPattern: DOCKER_TIMESTAMP_RE, - }); - - ctx.http.router({ bodyLimit: "100mb" }).use(app); - - authManagerInstance = AuthManager.getInstance(); - try { - await authManagerInstance.initialize(); - } catch (err) { - logger.error("Failed to initialize Docker backend", err, { - operation: "startup", - }); - } - - await startConsoleServer(); - - // Public because the console authenticates each session over the socket - // itself, where the container and host checks also live. - ctx.ws.upgrade( - "/console", - (request, socket, head) => - handleConsoleUpgrade( - request as Parameters[0], - socket as Parameters[1], - head as Buffer, - ), - { public: true }, - ); + registerRoutes(ctx.http.router(), { ctx, sessions, log }); + registerConsole(ctx, log); + registerServices(ctx, log); + ctx.registry.provide("docker.hostImportNormalizer", normalizeImportedHost); ctx.log.info( "Docker mounted at /plugin-api/docker and /plugin-ws/docker/console", ); } -export async function deactivate() { - await closeConsoleServer(); - authManagerInstance = null; -} +export async function deactivate() {} diff --git a/plugins/docker/src/backend/routes.ts b/plugins/docker/src/backend/routes.ts index 4cb3b0477..812e79f33 100644 --- a/plugins/docker/src/backend/routes.ts +++ b/plugins/docker/src/backend/routes.ts @@ -1,69 +1,110 @@ -// Core imports below point at TypeScript source so tsc can type-check them. -// The plugin build rewrites the prefix to the compiled output path; see -// packages/plugin-sdk/cli/lib/legacy-core-imports.mjs. -import { getErrorMessage } from "../../../../src/backend/utils/error-message.js"; -import express from "express"; -import axios from "axios"; -import { Client as SSHClient } from "ssh2"; -import type { PluginSshHost } from "@termix/plugin-sdk/backend"; -import { pluginSsh } from "./ssh.js"; -import { logger } from "../../../../src/backend/utils/logger.js"; +import type { Request, Response, Router } from "express"; +import type { PluginContext, PluginSshHost } from "@termix/plugin-sdk/backend"; +import { answerConnect, startConnect } from "./connect.js"; +import { containerCommand, getRuntimeLabel } from "./container-runtime.js"; +import { readDockerHostSettings } from "./host-settings.js"; +import type { DockerSession, DockerSessions } from "./sessions.js"; import { - logAudit, - getAuditUsername, - getRequestMeta, -} from "../../../../src/backend/utils/audit-logger.js"; -import { createCurrentHostRepository } from "../../../../src/backend/database/repositories/factory.js"; -import { resolveHostById } from "../../../../src/backend/hosts/host-resolver.js"; -import { createConnectionLog } from "../../../../src/backend/hosts/connection-log.js"; -import { DataCrypto } from "../../../../src/backend/utils/data-crypto.js"; -import { AuthManager } from "../../../../src/backend/utils/auth-manager.js"; -import type { - AuthenticatedRequest, - SSHHost, -} from "../../../../src/types/index.js"; -import type { - LogEntry, - ConnectionStage, -} from "../../../../src/types/connection-log.js"; -import { - containerCommand, - getContainerRuntimeConfig, - getRuntimeLabel, -} from "./container-runtime.js"; -import { - type SSHSession, - sshSessions, - pendingTOTPSessions, - cleanupSession, - scheduleSessionCleanup, - executeDockerCommand, -} from "./session-manager.js"; + CONTAINER_ID_RE, + DOCKER_TIMESTAMP_RE, + connectionLog, + getErrorMessage, + type ConnectionLogLine, + type DockerLogger, +} from "./helpers.js"; -const sshLogger = logger; -const authManager = AuthManager.getInstance(); +const CONTAINER_ACTIONS = [ + "start", + "stop", + "restart", + "pause", + "unpause", +] as const; +type ContainerAction = (typeof CONTAINER_ACTIONS)[number]; -const CONTAINER_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/; -export const DOCKER_TIMESTAMP_RE = /^[0-9T:.Z+-]+$/; +const LIST_FORMAT_UNIX = `'{"id":"{{.ID}}","name":"{{.Names}}","image":"{{.Image}}","status":"{{.Status}}","state":"{{.State}}","ports":"{{.Ports}}","created":"{{.CreatedAt}}"}' `; +const LIST_FORMAT_WINDOWS = `"{\\"id\\":\\"{{.ID}}\\",\\"name\\":\\"{{.Names}}\\",\\"image\\":\\"{{.Image}}\\",\\"status\\":\\"{{.Status}}\\",\\"state\\":\\"{{.State}}\\",\\"ports\\":\\"{{.Ports}}\\",\\"created\\":\\"{{.CreatedAt}}\\"}"`; +const STATS_FORMAT_UNIX = `'{"cpu":"{{.CPUPerc}}","memory":"{{.MemUsage}}","memoryPercent":"{{.MemPerc}}","netIO":"{{.NetIO}}","blockIO":"{{.BlockIO}}","pids":"{{.PIDs}}"}' `; +const STATS_FORMAT_WINDOWS = `"{\\"cpu\\":\\"{{.CPUPerc}}\\",\\"memory\\":\\"{{.MemUsage}}\\",\\"memoryPercent\\":\\"{{.MemPerc}}\\",\\"netIO\\":\\"{{.NetIO}}\\",\\"blockIO\\":\\"{{.BlockIO}}\\",\\"pids\\":\\"{{.PIDs}}\\"}"`; -export function getRequestUserId(req: express.Request): string | undefined { - return (req as AuthenticatedRequest).userId; +export function listFormat(isWindows: boolean): string { + return isWindows ? LIST_FORMAT_WINDOWS : LIST_FORMAT_UNIX; } -export function registerDockerSshRoutes(app: express.Express): void { - app.param("containerId", (req, res, next, value) => { +/** Parses `ps --format listFormat()` output, skipping lines that are not JSON. */ +export function parseContainerList(output: string): Record[] { + return output + .split("\n") + .filter((line) => line.trim()) + .flatMap((line) => { + try { + return [JSON.parse(line) as Record]; + } catch { + return []; + } + }); +} + +function notFound(error: unknown): boolean { + return getErrorMessage(error, "").includes("No such container"); +} + +interface Deps { + ctx: PluginContext; + sessions: DockerSessions; + log: DockerLogger; +} + +export function registerRoutes(router: Router, { ctx, sessions, log }: Deps) { + router.use((_req, res, next) => { + res.setHeader("Cache-Control", "no-store"); + next(); + }); + router.use(ctx.rbac.require("use") as never); + + router.param("containerId", (_req, res, next, value) => { if (!CONTAINER_ID_RE.test(value)) { - return res.status(400).json({ error: "Invalid container ID" }); + res.status(400).json({ error: "Invalid container ID" }); + return; } next(); }); + const userOf = (res: Response): string | null => { + const userId = ctx.currentActor(); + if (!userId) { + res.status(401).json({ error: "Authentication required" }); + return null; + } + return userId; + }; + + /** The caller's live session for the path's sessionId, or an error reply. */ + const sessionOf = (req: Request, res: Response): DockerSession | null => { + const userId = userOf(res); + if (!userId) return null; + const sessionId = String(req.params.sessionId ?? ""); + if (sessions.getPending(sessionId)) { + res.status(400).json({ + error: "Connection pending authentication", + code: "AUTH_PENDING", + }); + return null; + } + const session = sessions.get(sessionId, userId); + if (!session) { + res.status(400).json({ error: "SSH session not found or not connected" }); + return null; + } + return session; + }; + /** * @openapi - * /docker/ssh/connect: + * /plugin-api/docker/ssh/connect: * post: * summary: Establish SSH session for Docker - * description: Establishes an SSH session to a host for Docker operations. + * description: Opens an SSH session to a host for Docker operations. When the host asks for a code, a Warpgate sign-in or a password, the reply says so and the session waits for connect-totp or connect-warpgate. * tags: * - Docker * requestBody: @@ -72,74 +113,47 @@ export function registerDockerSshRoutes(app: express.Express): void { * application/json: * schema: * type: object + * required: + * - sessionId + * - hostId + * properties: + * sessionId: + * type: string + * hostId: + * type: integer + * userProvidedPassword: + * type: string + * userProvidedSshKey: + * type: string + * userProvidedKeyPassword: + * type: string * responses: * 200: - * description: SSH connection established. + * description: Connected, or a prompt the user has to answer. * 400: * description: Missing sessionId or hostId. - * 401: - * description: Authentication required. * 403: - * description: Docker is not enabled for this host. + * description: Docker is not enabled for this host, or the user lacks docker.use. * 404: * description: Host not found. * 500: * description: SSH connection failed. */ - app.post("/docker/ssh/connect", async (req, res) => { + router.post("/ssh/connect", async (req, res) => { + const userId = userOf(res); + if (!userId) return; const { sessionId, hostId, userProvidedPassword, userProvidedSshKey, userProvidedKeyPassword, - useSocks5, - socks5Host, - socks5Port, - socks5Username, - socks5Password, - socks5ProxyChain, - } = req.body; - const userId = getRequestUserId(req); + } = (req.body ?? {}) as Record; + const logs: ConnectionLogLine[] = []; - const connectionLogs: Array> = []; - - if (!userId) { - sshLogger.error("Docker SSH connection rejected: no authenticated user", { - operation: "docker_connect_auth", - sessionId, - }); - connectionLogs.push( - createConnectionLog( - "error", - "docker_connecting", - "Authentication required", - ), - ); - return res - .status(401) - .json({ error: "Authentication required", connectionLogs }); - } - - if (!DataCrypto.canUserAccessData(userId)) { - connectionLogs.push( - createConnectionLog("error", "docker_connecting", "Session expired"), - ); - return res.status(401).json({ - error: "Session expired - please log in again", - code: "SESSION_EXPIRED", - connectionLogs, - }); - } - - if (!sessionId || !hostId) { - sshLogger.warn("Missing Docker SSH connection parameters", { - operation: "docker_connect", - sessionId, - hasHostId: !!hostId, - }); - connectionLogs.push( - createConnectionLog( + if (typeof sessionId !== "string" || !sessionId || !hostId) { + logs.push( + connectionLog( "error", "docker_connecting", "Missing connection parameters", @@ -147,11 +161,12 @@ export function registerDockerSshRoutes(app: express.Express): void { ); return res .status(400) - .json({ error: "Missing sessionId or hostId", connectionLogs }); + .json({ error: "Missing sessionId or hostId", connectionLogs: logs }); } + const numericHostId = Number(hostId); - connectionLogs.push( - createConnectionLog( + logs.push( + connectionLog( "info", "docker_connecting", "Initiating Docker SSH connection", @@ -159,46 +174,20 @@ export function registerDockerSshRoutes(app: express.Express): void { ); try { - const resolvedHost = await resolveHostById(hostId, userId); - if (!resolvedHost) { - connectionLogs.push( - createConnectionLog("error", "docker_connecting", "Host not found"), + const host = await ctx.ssh.resolveHost(numericHostId); + if (!host) { + logs.push( + connectionLog("error", "docker_connecting", "Host not found"), ); return res .status(404) - .json({ error: "Host not found", connectionLogs }); + .json({ error: "Host not found", connectionLogs: logs }); } - const host = resolvedHost as SSHHost; - if (typeof host.jumpHosts === "string" && host.jumpHosts) { - try { - host.jumpHosts = JSON.parse(host.jumpHosts); - } catch (e) { - sshLogger.error("Failed to parse jump hosts", e, { - hostId: host.id, - }); - host.jumpHosts = []; - } - } - if (typeof host.terminalConfig === "string" && host.terminalConfig) { - try { - host.terminalConfig = JSON.parse(host.terminalConfig as string); - } catch { - host.terminalConfig = undefined; - } - } - const { runtime: containerRuntime } = getContainerRuntimeConfig( - host.dockerConfig, - ); - - if (!host.enableDocker) { - sshLogger.warn("Docker not enabled for host", { - operation: "docker_connect", - hostId, - userId, - }); - connectionLogs.push( - createConnectionLog( + const settings = await readDockerHostSettings(ctx, numericHostId); + if (!settings.enabled) { + logs.push( + connectionLog( "error", "docker_connecting", "Docker is not enabled for this host", @@ -208,486 +197,170 @@ export function registerDockerSshRoutes(app: express.Express): void { error: "Docker is not enabled for this host. Enable it in Host Settings.", code: "DOCKER_DISABLED", - connectionLogs, + connectionLogs: logs, }); } - connectionLogs.push( - createConnectionLog( + sessions.close(sessionId); + logs.push( + connectionLog( "info", "docker_auth", "Resolving authentication credentials", ), ); - if (sshSessions[sessionId]) { - cleanupSession(sessionId); - } - - if (pendingTOTPSessions[sessionId]) { - try { - pendingTOTPSessions[sessionId].client.end(); - } catch { - // expected - } - delete pendingTOTPSessions[sessionId]; - } - - const connectTarget = { - ...(host as unknown as PluginSshHost), - id: hostId, + const target: PluginSshHost = { + ...host, + id: numericHostId, port: host.port || 22, - userId, - // The Docker panel sends its own proxy settings with the request. - useSocks5, - socks5Host, - socks5Port, - socks5Username, - socks5Password, - socks5ProxyChain, - } as PluginSshHost; - if (userProvidedPassword) { - connectTarget.password = userProvidedPassword; - connectTarget.authType = "password"; - } - if (userProvidedSshKey) { - connectTarget.key = userProvidedSshKey; - connectTarget.authType = "key"; - } - if (userProvidedKeyPassword) { - connectTarget.keyPassword = userProvidedKeyPassword; - } - const resolvedPassword = connectTarget.password as string | undefined; - const authType = (connectTarget.authType as string) || "none"; - - const client = new SSHClient(); - const ssh = pluginSsh(); - const prepared = await ssh.prepare(connectTarget, { - purpose: "docker", - client, - log: (level, message) => - connectionLogs.push( - createConnectionLog(level, "docker_auth", message), - ), - }); - const config = prepared.config; - - if (prepared.outcome.status !== "ready") { - const outcome = prepared.outcome; - connectionLogs.push( - createConnectionLog("error", "docker_auth", outcome.message), - ); - if (outcome.status === "interaction-required") { - return res.status(401).json({ - error: outcome.message, - requiresAuthInteraction: outcome.interaction, - ...(outcome.flag ? { [outcome.flag]: true } : {}), - connectionLogs, - }); - } - return res.status(400).json({ error: outcome.message, connectionLogs }); - } - - // Auth types with no stored secret answer a password prompt with - // "auth_required" so the panel can ask for credentials. - const credentialless = !ssh.requiresSecret(authType); - - let responseSent = false; - connectionLogs.push( - createConnectionLog("info", "dns", `Resolving DNS for ${host.ip}`), - ); - connectionLogs.push( - createConnectionLog( - "info", - "tcp", - `Connecting to ${host.ip}:${host.port || 22}`, - ), - ); - connectionLogs.push( - createConnectionLog("info", "handshake", "Initiating SSH handshake"), - ); - - client.on("ready", () => { - if (responseSent) return; - responseSent = true; - - connectionLogs.push( - createConnectionLog( - "success", - "connected", - "SSH connection established successfully", - ), - ); - - const session: SSHSession = { - client, - isConnected: true, - lastActive: Date.now(), - activeOperations: 0, - hostId, - userId, - containerRuntime, - }; - - sshSessions[sessionId] = session; - scheduleSessionCleanup(sessionId); - - client.exec("ver", (err, stream) => { - if (!err && stream) { - let output = ""; - stream.on("data", (d: Buffer) => { - output += d.toString(); - }); - stream.on("close", () => { - if (output.toLowerCase().includes("windows")) { - session.isWindows = true; - } - }); - stream.stderr.on("data", () => {}); - } - }); - - void (async () => { - const { ipAddress, userAgent } = getRequestMeta(req); - await logAudit({ - userId, - username: await getAuditUsername(userId), - action: "docker_connect", - resourceType: "host", - resourceId: hostId ? String(hostId) : undefined, - ipAddress, - userAgent, - success: true, - }); - })(); - - res.json({ - success: true, - message: "SSH connection established", - connectionLogs, - }); - }); - - client.on("error", (err) => { - if (responseSent) { - sshLogger.error( - "Docker SSH connection error after response sent", - err, - { - operation: "docker_connect_after_response", - sessionId, - hostId, - userId, - }, - ); - - if (pendingTOTPSessions[sessionId]) { - delete pendingTOTPSessions[sessionId]; - } - return; - } - responseSent = true; - - sshLogger.error("Docker SSH connection failed", err, { - operation: "docker_connect", - sessionId, - hostId, - userId, - }); - - let errorStage: ConnectionStage; - if ( - err.message.includes("ENOTFOUND") || - err.message.includes("getaddrinfo") - ) { - errorStage = "dns"; - connectionLogs.push( - createConnectionLog( - "error", - errorStage, - `DNS resolution failed: ${err.message}`, - ), - ); - } else if ( - err.message.includes("ECONNREFUSED") || - err.message.includes("ETIMEDOUT") - ) { - errorStage = "tcp"; - connectionLogs.push( - createConnectionLog( - "error", - errorStage, - `TCP connection failed: ${err.message}`, - ), - ); - } else if ( - err.message.includes("handshake") || - err.message.includes("key exchange") - ) { - errorStage = "handshake"; - connectionLogs.push( - createConnectionLog( - "error", - errorStage, - `SSH handshake failed: ${err.message}`, - ), - ); - } else if ( - err.message.includes("authentication") || - err.message.includes("Authentication") - ) { - errorStage = "auth"; - connectionLogs.push( - createConnectionLog( - "error", - errorStage, - `Authentication failed: ${err.message}`, - ), - ); - } else if (err.message.includes("verification failed")) { - errorStage = "handshake"; - connectionLogs.push( - createConnectionLog( - "error", - errorStage, - `SSH host key has changed. For security, please open a Terminal connection to this host first to verify and accept the new key fingerprint.`, - ), - ); - } else { - connectionLogs.push( - createConnectionLog( - "error", - "error", - `SSH connection failed: ${err.message}`, - ), - ); - } - - if ( - credentialless && - (err.message.includes("authentication") || - err.message.includes( - "All configured authentication methods failed", - )) - ) { - res.json({ - status: "auth_required", - reason: "no_keyboard", - connectionLogs, - }); - } else { - res.status(500).json({ - success: false, - message: err.message || "SSH connection failed", - connectionLogs, - }); - } - }); - - client.on("close", () => { - if (sshSessions[sessionId]) { - sshSessions[sessionId].isConnected = false; - cleanupSession(sessionId); - } - - if (pendingTOTPSessions[sessionId]) { - delete pendingTOTPSessions[sessionId]; - } - }); - - const parkForAnswer = ( - finish: (responses: string[]) => void, - prompts: Array<{ prompt: string; echo: boolean }>, - promptIndex: number, - isWarpgate = false, - ) => { - pendingTOTPSessions[sessionId] = { - client, - finish, - config, - createdAt: Date.now(), - sessionId, - hostId, - ip: host.ip, - port: host.port || 22, - username: host.username, - userId, - prompts, - totpPromptIndex: promptIndex, - resolvedPassword, - totpAttempts: 0, - containerRuntime, - ...(isWarpgate ? { isWarpgate: true } : {}), - }; }; - - client.on( - "keyboard-interactive", - ( - name: string, - instructions: string, - _instructionsLang: string, - prompts: Array<{ prompt: string; echo: boolean }>, - finish: (responses: string[]) => void, - ) => { - const decision = ssh.classifyKeyboardInteractive( - { name, instructions, prompts }, - connectTarget, - ); - const autoFinish = () => - finish(ssh.autoResponses(prompts, resolvedPassword)); - - if (decision.kind === "auto") { - finish(decision.responses); - return; - } - - if (decision.kind === "warpgate") { - if (responseSent) return; - responseSent = true; - parkForAnswer(finish, prompts, -1, true); - connectionLogs.push( - createConnectionLog( - "info", - "docker_auth", - "Warpgate authentication required", - ), - ); - res.json({ - requires_warpgate: true, - sessionId, - url: decision.url, - securityKey: decision.securityKey, - connectionLogs, - }); - return; - } - - const promptText = prompts[decision.promptIndex].prompt; - const isPasswordPrompt = /password/i.test(promptText); - - if ( - decision.kind === "input" && - !decision.isPush && - !isPasswordPrompt - ) { - autoFinish(); - return; - } - - if (decision.kind === "input" && isPasswordPrompt && credentialless) { - if (responseSent) return; - responseSent = true; - client.end(); - res.json({ status: "auth_required", reason: "no_keyboard" }); - return; - } - - if (responseSent || pendingTOTPSessions[sessionId]) { - autoFinish(); - return; - } - responseSent = true; - parkForAnswer(finish, prompts, decision.promptIndex); - - if (decision.kind === "input" && isPasswordPrompt) { - res.json({ - requires_totp: true, - sessionId, - prompt: promptText, - isPassword: true, - }); - return; - } - - connectionLogs.push( - createConnectionLog( - "info", - "docker_auth", - "TOTP verification required", - ), - ); - res.json({ - requires_totp: true, - sessionId, - prompt: promptText, - connectionLogs, - }); - }, - ); - - if (host.jumpHosts && host.jumpHosts.length > 0) { - connectionLogs.push( - createConnectionLog( - "info", - "jump", - `Connecting via ${host.jumpHosts.length} jump host(s)`, - ), - ); - } else if (useSocks5) { - connectionLogs.push( - createConnectionLog("info", "proxy", "Connecting via proxy"), - ); + if (typeof userProvidedPassword === "string" && userProvidedPassword) { + target.password = userProvidedPassword; + target.authType = "password"; + } + if (typeof userProvidedSshKey === "string" && userProvidedSshKey) { + target.key = userProvidedSshKey; + target.authType = "key"; + } + if ( + typeof userProvidedKeyPassword === "string" && + userProvidedKeyPassword + ) { + target.keyPassword = userProvidedKeyPassword; } - try { - const transport = await ssh.openTransport(connectTarget, config); - if (transport.jumpClient) { - const jumpClient = transport.jumpClient as SSHClient; - client.on("close", () => jumpClient.end()); - } - } catch (transportError) { - sshLogger.error("Docker SSH transport failed", transportError, { - operation: "docker_transport", - sessionId, - hostId, - }); - connectionLogs.push( - createConnectionLog( - "error", - host.jumpHosts && host.jumpHosts.length > 0 ? "jump" : "proxy", - getErrorMessage(transportError), - ), - ); - if (!responseSent) { - responseSent = true; - return res.status(500).json({ - error: getErrorMessage(transportError), - connectionLogs, - }); - } - return; - } - - client.connect(config); - } catch (error) { - sshLogger.error("Docker SSH connection error", error, { - operation: "docker_connect", + const step = await startConnect(ctx, sessions, log, { sessionId, - hostId, + userId, + host: target, + runtime: settings.runtime, + logs, + }); + return res.status(step.status).json(step.body); + } catch (error) { + log.error("Docker SSH connection error", error, { + hostId: numericHostId, userId, }); - connectionLogs.push( - createConnectionLog( + logs.push( + connectionLog( "error", "docker_connecting", `Connection error: ${getErrorMessage(error)}`, ), ); - res.status(500).json({ + return res.status(500).json({ success: false, message: getErrorMessage(error), - connectionLogs, + connectionLogs: logs, }); } }); /** * @openapi - * /docker/ssh/disconnect: + * /plugin-api/docker/ssh/connect-totp: + * post: + * summary: Answer a verification prompt + * description: Sends the code (or password) the host asked for and waits for the next step of the connect. + * tags: + * - Docker + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - sessionId + * - totpCode + * properties: + * sessionId: + * type: string + * totpCode: + * type: string + * responses: + * 200: + * description: Connected, or another prompt. + * 400: + * description: Session ID and code required, or the session is not waiting for a code. + * 404: + * description: The connect expired. + * 408: + * description: The host did not answer in time. + */ + router.post("/ssh/connect-totp", async (req, res) => { + const userId = userOf(res); + if (!userId) return; + const { sessionId, totpCode } = (req.body ?? {}) as Record; + if (typeof sessionId !== "string" || typeof totpCode !== "string") { + return res + .status(400) + .json({ error: "Session ID and TOTP code required" }); + } + const step = await answerConnect( + sessions, + sessionId, + userId, + "totp", + totpCode.trim(), + ); + return res.status(step.status).json(step.body); + }); + + /** + * @openapi + * /plugin-api/docker/ssh/connect-warpgate: + * post: + * summary: Complete Warpgate authentication + * description: Continues a connect after the user finished the Warpgate sign-in in the browser. + * tags: + * - Docker + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - sessionId + * properties: + * sessionId: + * type: string + * responses: + * 200: + * description: Connected, or another prompt. + * 400: + * description: Session ID required, or the session is not a Warpgate session. + * 404: + * description: The connect expired. + * 408: + * description: The host did not answer in time. + */ + router.post("/ssh/connect-warpgate", async (req, res) => { + const userId = userOf(res); + if (!userId) return; + const { sessionId } = (req.body ?? {}) as Record; + if (typeof sessionId !== "string" || !sessionId) { + return res.status(400).json({ error: "Session ID required" }); + } + const step = await answerConnect( + sessions, + sessionId, + userId, + "warpgate", + "", + ); + return res.status(step.status).json(step.body); + }); + + /** + * @openapi + * /plugin-api/docker/ssh/disconnect: * post: * summary: Disconnect SSH session - * description: Closes an active SSH session for Docker operations. + * description: Closes the caller's Docker SSH session. * tags: * - Docker * requestBody: @@ -705,393 +378,26 @@ export function registerDockerSshRoutes(app: express.Express): void { * 400: * description: Session ID is required. */ - app.post("/docker/ssh/disconnect", async (req, res) => { - const { sessionId } = req.body; - - if (!sessionId) { + router.post("/ssh/disconnect", (req, res) => { + const userId = userOf(res); + if (!userId) return; + const { sessionId } = (req.body ?? {}) as Record; + if (typeof sessionId !== "string" || !sessionId) { return res.status(400).json({ error: "Session ID is required" }); } - - cleanupSession(sessionId); - - res.json({ success: true, message: "SSH session disconnected" }); + const pending = sessions.getPending(sessionId); + if (sessions.get(sessionId, userId) || pending?.userId === userId) { + sessions.close(sessionId); + } + return res.json({ success: true, message: "SSH session disconnected" }); }); /** * @openapi - * /docker/ssh/connect-totp: - * post: - * summary: Verify TOTP and complete connection - * description: Verifies the TOTP code and completes the SSH connection. - * tags: - * - Docker - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * sessionId: - * type: string - * totpCode: - * type: string - * responses: - * 200: - * description: TOTP verified, SSH connection established. - * 400: - * description: Session ID and TOTP code required. - * 401: - * description: Invalid TOTP code. - * 404: - * description: TOTP session expired. - */ - app.post("/docker/ssh/connect-totp", async (req, res) => { - const { sessionId, totpCode } = req.body; - const userId = getRequestUserId(req); - - if (!userId) { - sshLogger.error("TOTP verification rejected: no authenticated user", { - operation: "docker_totp_auth", - sessionId, - }); - return res.status(401).json({ error: "Authentication required" }); - } - - if (!sessionId || !totpCode) { - return res - .status(400) - .json({ error: "Session ID and TOTP code required" }); - } - - const session = pendingTOTPSessions[sessionId]; - - if (!session) { - sshLogger.warn("TOTP session not found or expired", { - operation: "docker_totp_verify", - sessionId, - userId, - availableSessions: Object.keys(pendingTOTPSessions), - }); - return res - .status(404) - .json({ error: "TOTP session expired. Please reconnect." }); - } - - if (Date.now() - session.createdAt > 180000) { - delete pendingTOTPSessions[sessionId]; - try { - session.client.end(); - } catch { - // expected - } - sshLogger.warn("TOTP session timeout before code submission", { - operation: "docker_totp_verify", - sessionId, - userId, - age: Date.now() - session.createdAt, - }); - return res - .status(408) - .json({ error: "TOTP session timeout. Please reconnect." }); - } - - const responses = (session.prompts || []).map((p, index) => { - if (index === session.totpPromptIndex) { - return totpCode; - } - if (/password/i.test(p.prompt) && session.resolvedPassword) { - return session.resolvedPassword; - } - return ""; - }); - - let responseSent = false; - - const responseTimeout = setTimeout(() => { - if (!responseSent) { - responseSent = true; - delete pendingTOTPSessions[sessionId]; - sshLogger.warn("TOTP verification timeout", { - operation: "docker_totp_verify", - sessionId, - userId, - }); - res.status(408).json({ error: "TOTP verification timeout" }); - } - }, 60000); - - session.client.once("ready", () => { - if (responseSent) return; - responseSent = true; - clearTimeout(responseTimeout); - - delete pendingTOTPSessions[sessionId]; - - setTimeout(() => { - sshSessions[sessionId] = { - client: session.client, - isConnected: true, - lastActive: Date.now(), - activeOperations: 0, - hostId: session.hostId, - userId, - containerRuntime: session.containerRuntime, - }; - scheduleSessionCleanup(sessionId); - - res.json({ - status: "success", - message: "TOTP verified, SSH connection established", - }); - - if (session.hostId && session.userId) { - (async () => { - try { - const hostRow = - await createCurrentHostRepository().findByIdForUser( - session.userId!, - session.hostId!, - ); - - const hostName = - hostRow?.name || - `${session.username}@${session.ip}:${session.port}`; - - await axios.post( - "http://localhost:30006/activity/log", - { - type: "docker", - hostId: session.hostId, - hostName, - }, - { - headers: { - Authorization: `Bearer ${await authManager.generateJWTToken(session.userId!)}`, - }, - }, - ); - } catch (error) { - sshLogger.warn("Failed to log Docker activity (TOTP)", { - operation: "activity_log_error", - userId: session.userId, - hostId: session.hostId, - error: getErrorMessage(error), - }); - } - })(); - } - }, 200); - }); - - session.client.once("error", (err) => { - if (responseSent) return; - responseSent = true; - clearTimeout(responseTimeout); - - delete pendingTOTPSessions[sessionId]; - - sshLogger.error("TOTP verification failed", { - operation: "docker_totp_verify", - sessionId, - userId, - error: err.message, - }); - - res.status(401).json({ status: "error", message: "Invalid TOTP code" }); - }); - - session.finish(responses); - }); - - /** - * @openapi - * /docker/ssh/connect-warpgate: - * post: - * summary: Complete Warpgate authentication - * description: Submits empty response to complete Warpgate authentication after user completes browser auth. - * tags: - * - Docker - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - sessionId - * properties: - * sessionId: - * type: string - * description: Session ID from initial connection attempt - * responses: - * 200: - * description: Warpgate authentication completed successfully. - * 401: - * description: Authentication failed or unauthorized. - * 404: - * description: Warpgate session expired. - */ - app.post("/docker/ssh/connect-warpgate", async (req, res) => { - const { sessionId } = req.body; - const userId = getRequestUserId(req); - - if (!userId) { - sshLogger.error("Warpgate verification rejected: no authenticated user", { - operation: "docker_warpgate_auth", - sessionId, - }); - return res.status(401).json({ error: "Authentication required" }); - } - - if (!sessionId) { - return res.status(400).json({ error: "Session ID required" }); - } - - const session = pendingTOTPSessions[sessionId]; - - if (!session) { - sshLogger.warn("Warpgate session not found or expired", { - operation: "docker_warpgate_verify", - sessionId, - userId, - availableSessions: Object.keys(pendingTOTPSessions), - }); - return res - .status(404) - .json({ error: "Warpgate session expired. Please reconnect." }); - } - - if (!session.isWarpgate) { - return res - .status(400) - .json({ error: "Session is not a Warpgate session" }); - } - - if (Date.now() - session.createdAt > 300000) { - delete pendingTOTPSessions[sessionId]; - try { - session.client.end(); - } catch { - // expected - } - sshLogger.warn("Warpgate session timeout before completion", { - operation: "docker_warpgate_verify", - sessionId, - userId, - age: Date.now() - session.createdAt, - }); - return res - .status(408) - .json({ error: "Warpgate session timeout. Please reconnect." }); - } - - let responseSent = false; - - const responseTimeout = setTimeout(() => { - if (!responseSent) { - responseSent = true; - delete pendingTOTPSessions[sessionId]; - sshLogger.warn("Warpgate verification timeout", { - operation: "docker_warpgate_verify", - sessionId, - userId, - }); - res.status(408).json({ error: "Warpgate verification timeout" }); - } - }, 60000); - - session.client.once("ready", () => { - if (responseSent) return; - responseSent = true; - clearTimeout(responseTimeout); - - delete pendingTOTPSessions[sessionId]; - - setTimeout(() => { - sshSessions[sessionId] = { - client: session.client, - isConnected: true, - lastActive: Date.now(), - activeOperations: 0, - hostId: session.hostId, - userId, - containerRuntime: session.containerRuntime, - }; - scheduleSessionCleanup(sessionId); - - res.json({ - status: "success", - message: "Warpgate verified, SSH connection established", - }); - - if (session.hostId && session.userId) { - (async () => { - try { - const hostRow = - await createCurrentHostRepository().findByIdForUser( - session.userId!, - session.hostId!, - ); - - const hostName = - hostRow?.name || - `${session.username}@${session.ip}:${session.port}`; - - await axios.post( - "http://localhost:30006/activity/log", - { - type: "docker", - hostId: session.hostId, - hostName, - }, - { - headers: { - Authorization: `Bearer ${await authManager.generateJWTToken(session.userId!)}`, - }, - }, - ); - } catch (error) { - sshLogger.warn("Failed to log Docker activity (Warpgate)", { - operation: "activity_log_error", - userId: session.userId, - hostId: session.hostId, - error: getErrorMessage(error), - }); - } - })(); - } - }, 200); - }); - - session.client.once("error", (err) => { - if (responseSent) return; - responseSent = true; - clearTimeout(responseTimeout); - - delete pendingTOTPSessions[sessionId]; - - sshLogger.error("Warpgate verification failed", { - operation: "docker_warpgate_verify", - sessionId, - userId, - error: err.message, - }); - - res - .status(401) - .json({ status: "error", message: "Warpgate authentication failed" }); - }); - - session.finish([""]); - }); - - /** - * @openapi - * /docker/ssh/keepalive: + * /plugin-api/docker/ssh/keepalive: * post: * summary: Keep SSH session alive - * description: Keeps an active SSH session alive. + * description: Resets the idle timeout of the caller's Docker SSH session. * tags: * - Docker * requestBody: @@ -1109,44 +415,34 @@ export function registerDockerSshRoutes(app: express.Express): void { * 400: * description: Session ID is required or session not found. */ - app.post("/docker/ssh/keepalive", async (req, res) => { - const { sessionId } = req.body; - const userId = getRequestUserId(req); - - if (!sessionId) { + router.post("/ssh/keepalive", (req, res) => { + const userId = userOf(res); + if (!userId) return; + const { sessionId } = (req.body ?? {}) as Record; + if (typeof sessionId !== "string" || !sessionId) { return res.status(400).json({ error: "Session ID is required" }); } - - const session = sshSessions[sessionId]; - - if (!session || !session.isConnected) { + const session = sessions.get(sessionId, userId); + if (!session) { return res.status(400).json({ error: "SSH session not found or not connected", connected: false, }); } - - if (session.userId && session.userId !== userId) { - return res.status(403).json({ error: "Session access denied" }); - } - - session.lastActive = Date.now(); - scheduleSessionCleanup(sessionId); - - res.json({ + sessions.touch(session); + return res.json({ success: true, connected: true, - message: "Session keepalive successful", lastActive: session.lastActive, }); }); /** * @openapi - * /docker/ssh/status: + * /plugin-api/docker/ssh/status: * get: * summary: Check SSH session status - * description: Checks the status of an active SSH session. + * description: Whether the caller's Docker SSH session is connected. * tags: * - Docker * parameters: @@ -1161,31 +457,25 @@ export function registerDockerSshRoutes(app: express.Express): void { * 400: * description: Session ID is required. */ - app.get("/docker/ssh/status", async (req, res) => { - const sessionId = req.query.sessionId as string; - const userId = getRequestUserId(req); - - if (!sessionId) { + router.get("/ssh/status", (req, res) => { + const userId = userOf(res); + if (!userId) return; + const sessionId = req.query.sessionId; + if (typeof sessionId !== "string" || !sessionId) { return res.status(400).json({ error: "Session ID is required" }); } - - if (!userId) { - return res.status(401).json({ error: "Authentication required" }); - } - - const session = sshSessions[sessionId]; - const isConnected = - session?.userId === userId && session.isConnected === true; - - res.json({ success: true, connected: isConnected }); + return res.json({ + success: true, + connected: !!sessions.get(sessionId, userId), + }); }); /** * @openapi - * /docker/validate/{sessionId}: + * /plugin-api/docker/validate/{sessionId}: * get: * summary: Validate Docker availability - * description: Validates if Docker is available on the host. + * description: Checks that the container runtime is installed and its daemon answers. * tags: * - Docker * parameters: @@ -1199,121 +489,509 @@ export function registerDockerSshRoutes(app: express.Express): void { * description: Docker availability status. * 400: * description: SSH session not found or not connected. - * 500: - * description: Validation failed. */ - app.get("/docker/validate/:sessionId", async (req, res) => { - const { sessionId } = req.params; - const userId = getRequestUserId(req); + router.get("/validate/:sessionId", async (req, res) => { + const session = sessionOf(req, res); + if (!session) return; + const runtime = session.runtime; + const label = getRuntimeLabel(runtime); - if (!userId) { - return res.status(401).json({ error: "Authentication required" }); - } - - if (pendingTOTPSessions[sessionId]) { - return res.status(400).json({ - error: "Connection pending authentication", - code: "AUTH_PENDING", - }); - } - - const session = sshSessions[sessionId]; - - if (!session || !session.isConnected) { - return res.status(400).json({ - error: "SSH session not found or not connected", - }); - } - - if (session.userId !== userId) { - return res.status(403).json({ error: "Session access denied" }); - } - - session.lastActive = Date.now(); - session.activeOperations++; - - try { + await sessions.run(session, async () => { + let version = "unknown"; try { - const runtime = session.containerRuntime ?? "docker"; - const runtimeLabel = getRuntimeLabel(runtime); - const versionOutput = await executeDockerCommand( + const output = await sessions.exec( session, containerCommand(runtime, "--version"), - sessionId, - userId, - session.hostId, ); - const versionMatch = versionOutput.match( - /(?:Docker|podman) version ([^\s,]+)/i, - ); - const version = versionMatch ? versionMatch[1] : "unknown"; + version = + output.match(/(?:Docker|podman) version ([^\s,]+)/i)?.[1] ?? + "unknown"; + } catch { + res.json({ + available: false, + error: `${label} is not installed on this host.`, + code: "NOT_INSTALLED", + runtime, + }); + return; + } - try { - await executeDockerCommand( - session, - containerCommand(runtime, "ps"), - sessionId, - userId, - session.hostId, - ); - - session.activeOperations--; - return res.json({ - available: true, - version, + try { + await sessions.exec(session, containerCommand(runtime, "ps")); + res.json({ available: true, version, runtime }); + } catch (error) { + const message = getErrorMessage(error, ""); + if (message.includes("Cannot connect to the Docker daemon")) { + res.json({ + available: false, + error: `${label} daemon is not running or accessible`, + code: "DAEMON_NOT_RUNNING", runtime, }); - } catch (daemonError) { - session.activeOperations--; - const errorMsg = getErrorMessage(daemonError, ""); - - if (errorMsg.includes("Cannot connect to the Docker daemon")) { - return res.json({ - available: false, - error: `${runtimeLabel} daemon is not running or accessible`, - code: "DAEMON_NOT_RUNNING", - runtime, - }); - } - - if (errorMsg.includes("permission denied")) { - return res.json({ - available: false, - error: `Permission denied accessing ${runtimeLabel}`, - code: "PERMISSION_DENIED", - runtime, - }); - } - - return res.json({ + } else if (message.includes("permission denied")) { + res.json({ available: false, - error: errorMsg, + error: `Permission denied accessing ${label}`, + code: "PERMISSION_DENIED", + runtime, + }); + } else { + res.json({ + available: false, + error: message, code: "DOCKER_ERROR", runtime, }); } - } catch { - session.activeOperations--; - const runtime = session.containerRuntime ?? "docker"; - const runtimeLabel = getRuntimeLabel(runtime); - return res.json({ - available: false, - error: `${runtimeLabel} is not installed on this host.`, - code: "NOT_INSTALLED", - runtime, - }); + } + }); + }); + + /** + * @openapi + * /plugin-api/docker/containers/{sessionId}: + * get: + * summary: List all containers + * description: Lists the containers on the session's host. + * tags: + * - Docker + * parameters: + * - in: path + * name: sessionId + * required: true + * schema: + * type: string + * - in: query + * name: all + * schema: + * type: boolean + * responses: + * 200: + * description: A list of containers. + * 400: + * description: SSH session not found or not connected. + * 500: + * description: Failed to list containers. + */ + router.get("/containers/:sessionId", async (req, res) => { + const session = sessionOf(req, res); + if (!session) return; + const all = req.query.all !== "false"; + try { + const output = await sessions.run(session, () => + sessions.exec( + session, + containerCommand( + session.runtime, + `ps ${all ? "-a " : ""}--format ${listFormat(session.isWindows)}`, + ), + ), + ); + res.json(parseContainerList(output)); + } catch (error) { + log.error("Failed to list Docker containers", error, { + hostId: session.hostId, + }); + res.status(500).json({ + error: getErrorMessage(error, "Failed to list containers"), + }); + } + }); + + /** + * @openapi + * /plugin-api/docker/containers/{sessionId}/{containerId}: + * get: + * summary: Get container details + * description: The container's inspect output. + * tags: + * - Docker + * parameters: + * - in: path + * name: sessionId + * required: true + * schema: + * type: string + * - in: path + * name: containerId + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Container details. + * 400: + * description: SSH session not found or not connected. + * 404: + * description: Container not found. + * 500: + * description: Failed to get container details. + */ + router.get("/containers/:sessionId/:containerId", async (req, res) => { + const session = sessionOf(req, res); + if (!session) return; + const { containerId } = req.params; + try { + const output = await sessions.run(session, () => + sessions.exec( + session, + containerCommand(session.runtime, `inspect ${containerId}`), + ), + ); + const details = JSON.parse(output) as unknown[]; + if (details?.length > 0) { + res.json(details[0]); + } else { + res + .status(404) + .json({ error: "Container not found", code: "CONTAINER_NOT_FOUND" }); } } catch (error) { - session.activeOperations--; - sshLogger.error("Docker validation error", error, { - operation: "docker_validate", - sessionId, - userId, + if (notFound(error)) { + return res + .status(404) + .json({ error: "Container not found", code: "CONTAINER_NOT_FOUND" }); + } + log.error("Failed to get container details", error, { + hostId: session.hostId, + containerId, }); - res.status(500).json({ - available: false, - error: getErrorMessage(error, "Validation failed"), + error: getErrorMessage(error, "Failed to get container details"), + }); + } + }); + + /** + * @openapi + * /plugin-api/docker/containers/{sessionId}/{containerId}/{action}: + * post: + * summary: Start, stop, restart, pause or unpause a container + * description: Runs the container action on the session's host. + * tags: + * - Docker + * parameters: + * - in: path + * name: sessionId + * required: true + * schema: + * type: string + * - in: path + * name: containerId + * required: true + * schema: + * type: string + * - in: path + * name: action + * required: true + * schema: + * type: string + * enum: [start, stop, restart, pause, unpause] + * responses: + * 200: + * description: The action ran. + * 400: + * description: SSH session not found or not connected. + * 404: + * description: Container not found, or an unknown action. + * 500: + * description: The action failed. + */ + router.post( + "/containers/:sessionId/:containerId/:action", + async (req, res) => { + const action = req.params.action as ContainerAction; + if (!CONTAINER_ACTIONS.includes(action)) { + return res.status(404).json({ error: "Unknown container action" }); + } + const session = sessionOf(req, res); + if (!session) return; + const { containerId } = req.params; + try { + log.info("Docker container operation", { + hostId: session.hostId, + containerId, + action, + }); + await sessions.run(session, () => + sessions.exec( + session, + containerCommand(session.runtime, `${action} ${containerId}`), + ), + ); + res.json({ success: true, message: `Container ${action} succeeded` }); + } catch (error) { + if (notFound(error)) { + return res.status(404).json({ + success: false, + error: "Container not found", + code: "CONTAINER_NOT_FOUND", + }); + } + log.error(`Failed to ${action} container`, error, { + hostId: session.hostId, + containerId, + }); + res.status(500).json({ + success: false, + error: getErrorMessage(error, `Failed to ${action} container`), + }); + } + }, + ); + + /** + * @openapi + * /plugin-api/docker/containers/{sessionId}/{containerId}/remove: + * delete: + * summary: Remove container + * description: Removes a container, optionally by force. + * tags: + * - Docker + * parameters: + * - in: path + * name: sessionId + * required: true + * schema: + * type: string + * - in: path + * name: containerId + * required: true + * schema: + * type: string + * - in: query + * name: force + * schema: + * type: boolean + * responses: + * 200: + * description: Container removed. + * 400: + * description: Session not found, or the container is running and force was not set. + * 404: + * description: Container not found. + * 500: + * description: Failed to remove container. + */ + router.delete( + "/containers/:sessionId/:containerId/remove", + async (req, res) => { + const session = sessionOf(req, res); + if (!session) return; + const { containerId } = req.params; + const force = req.query.force === "true"; + try { + log.info("Docker container operation", { + hostId: session.hostId, + containerId, + action: "remove", + }); + await sessions.run(session, () => + sessions.exec( + session, + containerCommand( + session.runtime, + `rm ${force ? "-f " : ""}${containerId}`, + ), + ), + ); + res.json({ success: true, message: "Container removed successfully" }); + } catch (error) { + if (notFound(error)) { + return res.status(404).json({ + success: false, + error: "Container not found", + code: "CONTAINER_NOT_FOUND", + }); + } + if ( + getErrorMessage(error, "").includes( + "cannot remove a running container", + ) + ) { + return res.status(400).json({ + success: false, + error: + "Cannot remove a running container. Stop it first or use force.", + code: "CONTAINER_RUNNING", + }); + } + log.error("Failed to remove container", error, { + hostId: session.hostId, + containerId, + }); + res.status(500).json({ + success: false, + error: getErrorMessage(error, "Failed to remove container"), + }); + } + }, + ); + + /** + * @openapi + * /plugin-api/docker/containers/{sessionId}/{containerId}/logs: + * get: + * summary: Get container logs + * description: The container's log output. + * tags: + * - Docker + * parameters: + * - in: path + * name: sessionId + * required: true + * schema: + * type: string + * - in: path + * name: containerId + * required: true + * schema: + * type: string + * - in: query + * name: tail + * schema: + * type: integer + * - in: query + * name: timestamps + * schema: + * type: boolean + * - in: query + * name: since + * schema: + * type: string + * - in: query + * name: until + * schema: + * type: string + * responses: + * 200: + * description: Container logs. + * 400: + * description: SSH session not found or not connected. + * 404: + * description: Container not found. + * 500: + * description: Failed to get container logs. + */ + router.get("/containers/:sessionId/:containerId/logs", async (req, res) => { + const session = sessionOf(req, res); + if (!session) return; + const { containerId } = req.params; + const tail = req.query.tail ? parseInt(String(req.query.tail), 10) : 100; + const since = typeof req.query.since === "string" ? req.query.since : ""; + const until = typeof req.query.until === "string" ? req.query.until : ""; + + let command = containerCommand(session.runtime, `logs ${containerId}`); + if (tail && tail > 0) command += ` --tail ${Math.floor(tail)}`; + if (req.query.timestamps === "true") command += " --timestamps"; + if (since && DOCKER_TIMESTAMP_RE.test(since)) + command += ` --since ${since}`; + if (until && DOCKER_TIMESTAMP_RE.test(until)) + command += ` --until ${until}`; + command += " 2>&1"; + + try { + const logs = await sessions.run(session, () => + sessions.exec(session, command), + ); + res.json({ success: true, logs }); + } catch (error) { + if (notFound(error)) { + return res.status(404).json({ + success: false, + error: "Container not found", + code: "CONTAINER_NOT_FOUND", + }); + } + log.error("Failed to get container logs", error, { + hostId: session.hostId, + containerId, + }); + res.status(500).json({ + success: false, + error: getErrorMessage(error, "Failed to get container logs"), + }); + } + }); + + /** + * @openapi + * /plugin-api/docker/containers/{sessionId}/{containerId}/stats: + * get: + * summary: Get container stats + * description: One sample of the container's CPU, memory, network and block IO. + * tags: + * - Docker + * parameters: + * - in: path + * name: sessionId + * required: true + * schema: + * type: string + * - in: path + * name: containerId + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Container stats. + * 400: + * description: SSH session not found or not connected. + * 404: + * description: Container not found. + * 500: + * description: Failed to get container stats. + */ + router.get("/containers/:sessionId/:containerId/stats", async (req, res) => { + const session = sessionOf(req, res); + if (!session) return; + const { containerId } = req.params; + const format = session.isWindows ? STATS_FORMAT_WINDOWS : STATS_FORMAT_UNIX; + try { + const output = await sessions.run(session, () => + sessions.exec( + session, + containerCommand( + session.runtime, + `stats ${containerId} --no-stream --format ${format}`, + ), + ), + ); + const raw = JSON.parse(output.trim()) as Record; + const split = (value: string | undefined) => { + const [first, second] = (value ?? "").split(" / "); + return [first?.trim() || "0B", second?.trim() || "0B"]; + }; + const [memoryUsed, memoryLimit] = split(raw.memory); + const [netInput, netOutput] = split(raw.netIO); + const [blockRead, blockWrite] = split(raw.blockIO); + res.json({ + cpu: raw.cpu, + memoryUsed, + memoryLimit, + memoryPercent: raw.memoryPercent, + netInput, + netOutput, + blockRead, + blockWrite, + pids: raw.pids, + }); + } catch (error) { + if (notFound(error)) { + return res.status(404).json({ + success: false, + error: "Container not found", + code: "CONTAINER_NOT_FOUND", + }); + } + log.error("Failed to get container stats", error, { + hostId: session.hostId, + containerId, + }); + res.status(500).json({ + success: false, + error: getErrorMessage(error, "Failed to get container stats"), }); } }); diff --git a/plugins/docker/src/backend/services.ts b/plugins/docker/src/backend/services.ts new file mode 100644 index 000000000..e1e4c8aeb --- /dev/null +++ b/plugins/docker/src/backend/services.ts @@ -0,0 +1,256 @@ +import type { Client } from "ssh2"; +import type { PluginContext, PluginSshHost } from "@termix/plugin-sdk/backend"; +import { containerCommand } from "./container-runtime.js"; +import { + PS_STATE_FORMAT, + diffContainerStates, + parseContainerStates, + type ContainerState, + type DockerEventName, +} from "./container-state.js"; +import { readDockerHostSettings } from "./host-settings.js"; +import { execOnClient } from "./sessions.js"; +import { listFormat, parseContainerList } from "./routes.js"; +import { + CONTAINER_ID_RE, + getErrorMessage, + type DockerLogger, +} from "./helpers.js"; + +export const POOL = "docker"; +const TICK_MS = 15_000; +const POLL_INTERVAL_MS = 60_000; +const HOST_CACHE_MS = 5 * 60_000; + +export interface DockerContainerSummary { + id: string; + name: string; + image: string; + status: string; + state: string; + ports: string; + created: string; +} + +export type DockerContainerAction = + "start" | "stop" | "restart" | "pause" | "unpause" | "remove"; + +const ACTIONS: DockerContainerAction[] = [ + "start", + "stop", + "restart", + "pause", + "unpause", + "remove", +]; + +/** The "docker.containers" service, version 1. Runs as the caller's user. */ +export interface DockerServiceV1 { + listContainers: ( + hostId: number, + options?: { all?: boolean }, + ) => Promise; + action: ( + hostId: number, + container: string, + action: DockerContainerAction, + ) => Promise; +} + +export interface DockerEvent { + hostId: number; + container: string; + event: DockerEventName; +} + +/** The "docker.events" service, version 1. Runs as the caller's user. */ +export interface DockerEventsV1 { + /** + * Calls `listener` for container state changes on a host, polled about + * once a minute as the subscribing user. Subscribing the same listener + * again is a no-op, so a consumer can re-subscribe on a timer and pick up + * a restarted docker plugin. Resolves to an unsubscribe function. + */ + subscribe: ( + hostId: number, + listener: (event: DockerEvent) => void, + ) => Promise<() => void>; +} + +function actorOf(ctx: PluginContext): string { + const userId = ctx.currentActor(); + if (!userId) throw new Error("The docker service needs a calling user"); + return userId; +} + +export function registerServices(ctx: PluginContext, log: DockerLogger): void { + const runOnHost = async ( + hostId: number, + command: (runtime: "docker" | "podman") => string, + handle: (output: string) => T, + ): Promise => { + const { runtime } = await readDockerHostSettings(ctx, hostId); + const output = await ctx.ssh.withConnection( + hostId, + { pool: POOL, purpose: "docker" }, + (client) => execOnClient(client, command(runtime)), + ); + return handle(output); + }; + + ctx.services.provide("docker.containers", { + listContainers: async (hostId, options) => { + actorOf(ctx); + const all = options?.all !== false; + return runOnHost( + hostId, + (runtime) => + containerCommand( + runtime, + `ps ${all ? "-a " : ""}--format ${listFormat(false)}`, + ), + (output) => + parseContainerList(output) as unknown as DockerContainerSummary[], + ); + }, + action: async (hostId, container, action) => { + actorOf(ctx); + if (!CONTAINER_ID_RE.test(container)) { + throw new Error("Invalid container name"); + } + if (!ACTIONS.includes(action)) { + throw new Error(`Unknown container action: ${action}`); + } + const args = + action === "remove" ? `rm ${container}` : `${action} ${container}`; + await runOnHost( + hostId, + (runtime) => containerCommand(runtime, args), + () => undefined, + ); + }, + }); + + interface Watch { + userId: string; + hostId: number; + listeners: Set<(event: DockerEvent) => void>; + snapshot?: Map; + host?: { value: PluginSshHost; at: number }; + lastPolledAt: number; + failing: boolean; + } + const watches = new Map(); + ctx.disposables.add(() => watches.clear()); + + const hostFor = async (watch: Watch): Promise => { + if (watch.host && Date.now() - watch.host.at < HOST_CACHE_MS) { + return watch.host.value; + } + const host = await ctx.asUser(watch.userId, () => + ctx.ssh.resolveHost(watch.hostId), + ); + watch.host = host ? { value: host, at: Date.now() } : undefined; + return host; + }; + + const pollOne = async (watch: Watch) => { + try { + const host = await hostFor(watch); + if (!host) throw new Error("Host not found"); + const { runtime } = await readDockerHostSettings(ctx, watch.hostId); + const output = await ctx.ssh.withConnection( + host, + { pool: POOL, purpose: "docker" }, + (client) => + execOnClient( + client, + containerCommand(runtime, `ps -a --format ${PS_STATE_FORMAT}`), + ), + ); + const current = parseContainerStates(output); + const previous = watch.snapshot; + watch.snapshot = current; + watch.failing = false; + if (!previous) return; + for (const { container, event } of diffContainerStates( + previous, + current, + )) { + for (const listener of [...watch.listeners]) { + try { + listener({ hostId: watch.hostId, container, event }); + } catch (error) { + log.warn("Docker event listener failed", { + hostId: watch.hostId, + error: getErrorMessage(error), + }); + } + } + } + } catch (error) { + // Docker missing, the host down or the owner's data locked. A later + // success is a first observation again rather than a diff. + watch.snapshot = undefined; + watch.host = undefined; + if (!watch.failing) { + log.warn("Docker event poll failed", { + hostId: watch.hostId, + error: getErrorMessage(error), + }); + } + watch.failing = true; + } + }; + + ctx.schedule.every(TICK_MS, async () => { + const now = Date.now(); + for (const watch of [...watches.values()]) { + if (now - watch.lastPolledAt < POLL_INTERVAL_MS) continue; + watch.lastPolledAt = now; + await pollOne(watch); + } + }); + + ctx.events.on("host.updated", (payload) => { + const { hostId } = payload as { hostId?: number }; + if (!hostId) return; + ctx.ssh.dropPooled(POOL, hostId); + for (const watch of watches.values()) { + if (watch.hostId === hostId) watch.host = undefined; + } + }); + ctx.events.on("host.deleted", (payload) => { + const { hostId } = payload as { hostId?: number }; + if (!hostId) return; + for (const [key, watch] of watches) { + if (watch.hostId === hostId) watches.delete(key); + } + }); + + ctx.services.provide("docker.events", { + subscribe: async (hostId, listener) => { + const userId = actorOf(ctx); + const key = `${userId}:${hostId}`; + let watch = watches.get(key); + if (!watch) { + watch = { + userId, + hostId, + listeners: new Set(), + lastPolledAt: 0, + failing: false, + }; + watches.set(key, watch); + } + watch.listeners.add(listener); + const current = watch; + return () => { + current.listeners.delete(listener); + if (current.listeners.size === 0 && watches.get(key) === current) { + watches.delete(key); + } + }; + }, + }); +} diff --git a/plugins/docker/src/backend/session-manager.ts b/plugins/docker/src/backend/session-manager.ts deleted file mode 100644 index 3b1b93931..000000000 --- a/plugins/docker/src/backend/session-manager.ts +++ /dev/null @@ -1,174 +0,0 @@ -// Core imports below point at TypeScript source so tsc can type-check them. -// The plugin build rewrites the prefix to the compiled output path; see -// packages/plugin-sdk/cli/lib/legacy-core-imports.mjs. -import { Client as SSHClient } from "ssh2"; -import { logger } from "../../../../src/backend/utils/logger.js"; -import type { ContainerRuntime } from "./container-runtime.js"; - -const sshLogger = logger; - -export interface SSHSession { - client: SSHClient; - isConnected: boolean; - lastActive: number; - timeout?: NodeJS.Timeout; - activeOperations: number; - hostId?: number; - userId?: string; - isWindows?: boolean; - containerRuntime?: ContainerRuntime; -} - -export interface PendingTOTPSession { - client: SSHClient; - finish: (responses: string[]) => void; - config: Record; - createdAt: number; - sessionId: string; - hostId?: number; - ip?: string; - port?: number; - username?: string; - userId?: string; - prompts?: Array<{ prompt: string; echo: boolean }>; - totpPromptIndex?: number; - resolvedPassword?: string; - totpAttempts: number; - isWarpgate?: boolean; - containerRuntime?: ContainerRuntime; -} - -export const sshSessions: Record = {}; -export const pendingTOTPSessions: Record = {}; - -const SESSION_IDLE_TIMEOUT = 60 * 60 * 1000; - -setInterval(() => { - const now = Date.now(); - Object.keys(pendingTOTPSessions).forEach((sessionId) => { - const session = pendingTOTPSessions[sessionId]; - if (now - session.createdAt > 180000) { - try { - session.client.end(); - } catch { - // expected - } - delete pendingTOTPSessions[sessionId]; - } - }); -}, 60000); - -export function cleanupSession(sessionId: string) { - const session = sshSessions[sessionId]; - if (session) { - if (session.activeOperations > 0) { - sshLogger.warn( - `Deferring session cleanup for ${sessionId} - ${session.activeOperations} active operations`, - { - operation: "cleanup_deferred", - sessionId, - activeOperations: session.activeOperations, - }, - ); - scheduleSessionCleanup(sessionId); - return; - } - - try { - session.client.end(); - } catch { - // expected - } - clearTimeout(session.timeout); - delete sshSessions[sessionId]; - } -} - -export function scheduleSessionCleanup(sessionId: string) { - const session = sshSessions[sessionId]; - if (session) { - if (session.timeout) clearTimeout(session.timeout); - - session.timeout = setTimeout(() => { - cleanupSession(sessionId); - }, SESSION_IDLE_TIMEOUT); - } -} - -export async function executeDockerCommand( - session: SSHSession, - command: string, - sessionId?: string, - userId?: string, - hostId?: number, -): Promise { - const startTime = Date.now(); - sshLogger.info("Executing Docker command", { - operation: "docker_command_exec", - sessionId, - userId, - hostId, - command: command.split(" ")[1], - }); - return new Promise((resolve, reject) => { - session.client.exec(command, (err, stream) => { - if (err) { - sshLogger.error("Docker command execution error", err, { - operation: "execute_docker_command", - sessionId, - userId, - hostId, - command: command.split(" ")[1], - }); - return reject(err); - } - - let stdout = ""; - let stderr = ""; - - stream.on("close", (code: number) => { - if (code !== 0) { - sshLogger.error("Docker command failed", undefined, { - operation: "execute_docker_command", - sessionId, - userId, - hostId, - command: command.split(" ")[1], - exitCode: code, - stderr, - }); - reject(new Error(stderr || `Command exited with code ${code}`)); - } else { - sshLogger.success("Docker command completed", { - operation: "docker_command_success", - sessionId, - userId, - hostId, - command: command.split(" ")[1], - duration: Date.now() - startTime, - }); - resolve(stdout); - } - }); - - stream.on("data", (data: Buffer) => { - stdout += data.toString(); - }); - - stream.stderr.on("data", (data: Buffer) => { - stderr += data.toString(); - }); - - stream.on("error", (streamErr: Error) => { - sshLogger.error("Docker command stream error", streamErr, { - operation: "execute_docker_command", - sessionId, - userId, - hostId, - command: command.split(" ")[1], - }); - reject(streamErr); - }); - }); - }); -} diff --git a/plugins/docker/src/backend/sessions.ts b/plugins/docker/src/backend/sessions.ts new file mode 100644 index 000000000..4ee6dc247 --- /dev/null +++ b/plugins/docker/src/backend/sessions.ts @@ -0,0 +1,183 @@ +import type { Client } from "ssh2"; +import type { PluginSchedule } from "@termix/plugin-sdk/backend"; +import type { ContainerRuntime } from "./container-runtime.js"; +import type { DockerLogger } from "./helpers.js"; + +const SESSION_IDLE_MS = 60 * 60 * 1000; +const PENDING_SWEEP_MS = 60 * 1000; + +/** A live SSH connection behind the Docker panel, owned by one user. */ +export interface DockerSession { + id: string; + client: Client; + dispose: () => void; + hostId: number; + userId: string; + runtime: ContainerRuntime; + isWindows: boolean; + lastActive: number; + activeOperations: number; + cancelIdle?: () => void; +} + +/** A connect still waiting on a person (TOTP, Warpgate). */ +export interface PendingEntry { + userId: string; + hostId: number; + expiresAt: number; + /** Gives up: answers the open prompt with nothing. */ + cancel: () => void; +} + +export interface DockerSessions { + add: (session: DockerSession) => void; + /** The session when `userId` owns it. */ + get: (id: string, userId: string) => DockerSession | undefined; + close: (id: string) => void; + touch: (session: DockerSession) => void; + /** Runs `fn` with the session marked busy, so idle cleanup waits. */ + run: (session: DockerSession, fn: () => Promise) => Promise; + exec: (session: DockerSession, command: string) => Promise; + setPending: (id: string, entry: PendingEntry) => void; + getPending: (id: string) => PendingEntry | undefined; + clearPending: (id: string) => void; + closeAll: () => void; +} + +/** Runs one command and resolves stdout, rejecting on a non-zero exit. */ +export function execOnClient(client: Client, command: string): Promise { + return new Promise((resolve, reject) => { + client.exec(command, (err, stream) => { + if (err) return reject(err); + let stdout = ""; + let stderr = ""; + stream.on("close", (code: number) => { + if (code !== 0 && code !== null && code !== undefined) { + reject(new Error(stderr || `Command exited with code ${code}`)); + } else { + resolve(stdout); + } + }); + stream.on("data", (data: Buffer) => { + stdout += data.toString(); + }); + stream.stderr.on("data", (data: Buffer) => { + stderr += data.toString(); + }); + stream.on("error", (streamErr: Error) => reject(streamErr)); + }); + }); +} + +export function createDockerSessions( + schedule: PluginSchedule, + log: DockerLogger, +): DockerSessions { + const sessions = new Map(); + const pending = new Map(); + + const end = (session: DockerSession) => { + session.cancelIdle?.(); + sessions.delete(session.id); + session.dispose(); + }; + + const scheduleIdle = (session: DockerSession) => { + session.cancelIdle?.(); + session.cancelIdle = schedule.after(SESSION_IDLE_MS, () => { + if (sessions.get(session.id) !== session) return; + if (session.activeOperations > 0) { + scheduleIdle(session); + return; + } + end(session); + }); + }; + + schedule.every(PENDING_SWEEP_MS, () => { + const now = Date.now(); + for (const [id, entry] of pending) { + if (now < entry.expiresAt) continue; + pending.delete(id); + entry.cancel(); + } + }); + + return { + add: (session) => { + const existing = sessions.get(session.id); + if (existing) end(existing); + sessions.set(session.id, session); + session.client.once("close", () => { + if (sessions.get(session.id) === session) { + session.cancelIdle?.(); + sessions.delete(session.id); + } + }); + scheduleIdle(session); + }, + + get: (id, userId) => { + const session = sessions.get(id); + return session && session.userId === userId ? session : undefined; + }, + + close: (id) => { + const session = sessions.get(id); + if (session) end(session); + const entry = pending.get(id); + if (entry) { + pending.delete(id); + entry.cancel(); + } + }, + + touch: (session) => { + session.lastActive = Date.now(); + scheduleIdle(session); + }, + + run: async (session, fn) => { + session.lastActive = Date.now(); + session.activeOperations++; + try { + return await fn(); + } finally { + session.activeOperations--; + } + }, + + exec: async (session, command) => { + const started = Date.now(); + try { + return await execOnClient(session.client, command); + } catch (error) { + log.warn("Docker command failed", { + hostId: session.hostId, + command: command.split(" ")[2], + durationMs: Date.now() - started, + error: error instanceof Error ? error.message : String(error), + }); + throw error; + } + }, + + setPending: (id, entry) => { + const existing = pending.get(id); + if (existing) existing.cancel(); + pending.set(id, entry); + }, + + getPending: (id) => pending.get(id), + + clearPending: (id) => { + pending.delete(id); + }, + + closeAll: () => { + for (const session of [...sessions.values()]) end(session); + for (const entry of pending.values()) entry.cancel(); + pending.clear(); + }, + }; +} diff --git a/plugins/docker/src/backend/ssh.ts b/plugins/docker/src/backend/ssh.ts deleted file mode 100644 index b7a221543..000000000 --- a/plugins/docker/src/backend/ssh.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Client } from "ssh2"; -import type { - PluginSsh, - PluginSshConnection, - PluginSshConnectOptions, - PluginSshHost, -} from "@termix/plugin-sdk/backend"; - -let current: PluginSsh | null = null; - -/** Set in activate, cleared on deactivate. */ -export function setPluginSsh(ssh: PluginSsh | null): void { - current = ssh; -} - -export function pluginSsh(): PluginSsh { - if (!current) throw new Error("The plugin is not active"); - return current; -} - -/** withConnection with ssh2's Client type filled in. */ -export function withSshConnection( - host: number | PluginSshHost, - options: PluginSshConnectOptions & { pool: string }, - fn: (client: Client) => Promise, -): Promise { - return pluginSsh().withConnection(host, options, fn); -} - -/** connect with ssh2's Client type filled in. */ -export function connectSsh( - host: number | PluginSshHost, - options?: PluginSshConnectOptions, -): Promise> { - return pluginSsh().connect(host, options); -} diff --git a/plugins/docker/src/frontend/DockerApp.tsx b/plugins/docker/src/frontend/DockerApp.tsx index b2bd1a650..fda68d35d 100644 --- a/plugins/docker/src/frontend/DockerApp.tsx +++ b/plugins/docker/src/frontend/DockerApp.tsx @@ -1,8 +1,8 @@ +import { ConnectionScreen, FullScreenAppWrapper } from "@termix/plugin-sdk/ui"; import React from "react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; import { DockerManager } from "./DockerManager.tsx"; -import { FullScreenAppWrapper } from "@/features/FullScreenAppWrapper.tsx"; -import { ConnectionScreen } from "@/components/connection/ConnectionScreen.tsx"; +import { toDockerHost } from "./types"; interface DockerAppProps { hostId?: string; @@ -37,7 +37,9 @@ const DockerApp: React.FC = ({ hostId }) => { return ( , + )} title={hostConfig.name || `${hostConfig.username}@${hostConfig.ip}`} isVisible={true} isTopbarOpen={false} diff --git a/plugins/docker/src/frontend/DockerManager.tsx b/plugins/docker/src/frontend/DockerManager.tsx index 00cf43d94..9fe9b4231 100644 --- a/plugins/docker/src/frontend/DockerManager.tsx +++ b/plugins/docker/src/frontend/DockerManager.tsx @@ -1,10 +1,5 @@ /* eslint-disable react-hooks/exhaustive-deps */ import React from "react"; -import { Separator } from "@/components/separator.tsx"; -import { Alert, AlertDescription } from "@/components/alert.tsx"; -import { Button } from "@/components/button.tsx"; -import { Card } from "@/components/card.tsx"; -import { Input } from "@/components/input.tsx"; import { AlertCircle, Box, @@ -14,44 +9,46 @@ import { RefreshCw, Search, } from "lucide-react"; - import { - useTranslation, useConnectionRetry, + useHost, + useTranslation, } from "@termix/plugin-sdk/frontend"; -import type { SSHHost, DockerContainer, DockerValidation } from "@/types"; -import { logActivity, getSSHHosts } from "@/main-axios"; import { - connectDockerSession, - disconnectDockerSession, - listDockerContainers, - validateDockerAvailability, - keepaliveDockerSession, - verifyDockerTOTP, - verifyDockerWarpgate, -} from "./docker-api"; + Alert, + AlertDescription, + Button, + Card, + ConnectionLogProvider, + ConnectionScreen, + Input, + SSHAuthDialog, + Select2, + Separator, + TOTPDialog, + WarpgateDialog, + logActivity, + useAdaptivePolling, + useAreaPreferences, + useConnectionLog, + useTabsSafe, + useUiPreferencesContext, +} from "@termix/plugin-sdk/ui"; +import { DockerApiError, useDockerApi, type ConnectResult } from "./docker-api"; +import { + dockerEnabled, + hostTitle, + toDockerHost, + type DockerContainer, + type DockerHost, + type DockerValidation, +} from "./types"; import { ContainerList } from "./components/ContainerList.tsx"; import { ContainerTable } from "./components/ContainerTable.tsx"; import { ContainerDetail } from "./components/ContainerDetail.tsx"; -import { TOTPDialog } from "@/ssh/dialogs/TOTPDialog.tsx"; -import { SSHAuthDialog } from "@/ssh/dialogs/SSHAuthDialog.tsx"; -import { WarpgateDialog } from "@/ssh/dialogs/WarpgateDialog.tsx"; -import { useTabsSafe } from "@/shell/TabContext.tsx"; -import { - useAreaPreferences, - useUiPreferencesContext, -} from "@/contexts/UiPreferencesContext"; -import { - ConnectionLogProvider, - useConnectionLog, -} from "@/ssh/connection-log/ConnectionLogContext.tsx"; -import { ConnectionScreen } from "@/components/connection/ConnectionScreen.tsx"; -import { useAdaptivePolling } from "@/hooks/use-adaptive-polling.ts"; -import type { LogEntry } from "@/types/connection-log.ts"; -import { Select2 } from "@/components/select2"; interface DockerManagerProps { - hostConfig?: SSHHost; + host?: DockerHost; title?: string; isVisible?: boolean; isTopbarOpen?: boolean; @@ -59,15 +56,21 @@ interface DockerManagerProps { onClose?: () => void; } -type ConnectionLogInput = Omit; +interface PromptState { + kind: "totp" | "warpgate"; + sessionId: string; + prompt?: string; + isPassword?: boolean; + url?: string; + securityKey?: string; +} -interface DockerConnectionError { - message?: string; - connectionLogs?: ConnectionLogInput[]; +function newSessionId(): string { + return `docker-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; } function DockerManagerInner({ - hostConfig, + host, title, isVisible = true, isTopbarOpen = true, @@ -75,11 +78,24 @@ function DockerManagerInner({ onClose, }: DockerManagerProps): React.ReactElement { const { t } = useTranslation(); + const docker = useDockerApi(); const { addLog, setLogs, clearLogs } = useConnectionLog(); const { currentTab, removeTab } = useTabsSafe(); const dockerPrefs = useAreaPreferences("docker"); const uiPrefsCtx = useUiPreferencesContext(); - const [currentHostConfig, setCurrentHostConfig] = React.useState(hostConfig); + + // The shell's host list keeps the host current after an edit; a + // standalone window has no shell, so it keeps the host it was given. + const liveRecord = useHost(host?.id); + const currentHost = React.useMemo( + () => + liveRecord + ? toDockerHost(liveRecord as unknown as Record) + : host, + [liveRecord, host], + ); + const enabled = dockerEnabled(currentHost); + const [sessionId, setSessionId] = React.useState(null); const [containers, setContainers] = React.useState([]); const containersSignatureRef = React.useRef(""); @@ -105,23 +121,14 @@ function DockerManagerInner({ React.useState(null); const [isValidating, setIsValidating] = React.useState(false); // Initial view follows the interface preset; switching it afterwards is a - // per-session choice, same as before. + // per-session choice. const [viewMode, setViewMode] = React.useState<"list" | "detail">( dockerPrefs.viewMode, ); const [isLoadingContainers, setIsLoadingContainers] = React.useState(false); const [hasLoadedContainersOnce, setHasLoadedContainersOnce] = React.useState(false); - const [totpRequired, setTotpRequired] = React.useState(false); - const [totpSessionId, setTotpSessionId] = React.useState(null); - const [totpPrompt, setTotpPrompt] = React.useState(""); - const [warpgateRequired, setWarpgateRequired] = React.useState(false); - const [warpgateSessionId, setWarpgateSessionId] = React.useState< - string | null - >(null); - const [warpgateUrl, setWarpgateUrl] = React.useState(""); - const [warpgateSecurityKey, setWarpgateSecurityKey] = - React.useState(""); + const [prompt, setPrompt] = React.useState(null); const [showAuthDialog, setShowAuthDialog] = React.useState(false); const [authReason, setAuthReason] = React.useState< "no_keyboard" | "auth_failed" | "timeout" @@ -131,214 +138,168 @@ function DockerManagerInner({ const [retryCount, setRetryCount] = React.useState(0); const activityLoggedRef = React.useRef(false); - const activityLoggingRef = React.useRef(false); - const logDockerActivity = async () => { - if ( - !currentHostConfig?.id || - activityLoggedRef.current || - activityLoggingRef.current - ) { - return; - } - - activityLoggingRef.current = true; + const logDockerActivity = () => { + if (!currentHost?.id || activityLoggedRef.current) return; activityLoggedRef.current = true; - - try { - const hostName = - currentHostConfig.name || - `${currentHostConfig.username}@${currentHostConfig.ip}`; - await logActivity("docker", currentHostConfig.id, hostName); - } catch (err) { - console.warn("Failed to log docker activity:", err); + logActivity("docker", currentHost.id, hostTitle(currentHost)).catch(() => { activityLoggedRef.current = false; - } finally { - activityLoggingRef.current = false; - } + }); }; React.useEffect(() => { - if (hostConfig?.id !== currentHostConfig?.id) { - setCurrentHostConfig(hostConfig); - setContainers([]); - setSelectedContainer(null); - setSessionId(null); - setDockerValidation(null); - setViewMode("list"); + setContainers([]); + setSelectedContainer(null); + setSessionId(null); + setDockerValidation(null); + setViewMode("list"); + }, [host?.id]); + + const retryRef = React.useRef | null>( + null, + ); + + const validate = async (sid: string, clearAfter: boolean) => { + setSessionId(sid); + setIsValidating(true); + try { + const validation = await docker.validate(sid); + setDockerValidation(validation); + if (!validation.available) { + addLog({ + type: "error", + stage: "validation", + message: validation.error || t("docker.error"), + details: validation.code + ? `Error code: ${validation.code}` + : undefined, + }); + retryRef.current?.markFailed(); + } else { + logDockerActivity(); + if (clearAfter) setTimeout(() => clearLogs(), 1000); + retryRef.current?.markConnected(); + } + } finally { + setIsValidating(false); } - }, [hostConfig?.id]); + }; - React.useEffect(() => { - const fetchLatestHostConfig = async () => { - if (hostConfig?.id) { - try { - const hosts = await getSSHHosts(); - const updatedHost = hosts.find((h) => h.id === hostConfig.id); - if (updatedHost) { - setCurrentHostConfig(updatedHost); - } - } catch { - // Silently handle error - } + /** Acts on one step of a connect, whichever request it answered. */ + const applyConnectResult = async ( + sid: string, + result: ConnectResult, + clearAfter: boolean, + ) => { + if (result.connectionLogs) setLogs(result.connectionLogs); + + if (result.requires_warpgate) { + setPrompt({ + kind: "warpgate", + sessionId: sid, + url: result.url || "", + securityKey: result.securityKey || "", + }); + return; + } + if (result.requires_totp) { + if (result.retry) { + addLog({ + type: "error", + stage: "auth", + message: t("docker.totpVerificationFailed"), + }); } - }; + setPrompt({ + kind: "totp", + sessionId: sid, + prompt: result.prompt || t("docker.verificationCodePrompt"), + isPassword: result.isPassword, + }); + return; + } + if (result.status === "auth_required") { + setAuthReason( + result.reason === "no_keyboard" ? "no_keyboard" : "auth_failed", + ); + setShowAuthDialog(true); + return; + } + setPrompt(null); + await validate(sid, clearAfter); + }; - fetchLatestHostConfig(); + const reportFailure = (error: unknown, fallback: string) => { + if (error instanceof DockerApiError && error.connectionLogs?.length) { + setLogs(error.connectionLogs); + } else { + addLog({ + type: "error", + stage: "connection", + message: error instanceof Error ? error.message : fallback, + }); + } + retryRef.current?.markFailed(); + }; - const handleHostsChanged = async () => { - if (hostConfig?.id) { - try { - const hosts = await getSSHHosts(); - const updatedHost = hosts.find((h) => h.id === hostConfig.id); - if (updatedHost) { - setCurrentHostConfig(updatedHost); - } - } catch { - // Silently handle error - } - } - }; - - window.addEventListener("ssh-hosts:changed", handleHostsChanged); - return () => - window.removeEventListener("ssh-hosts:changed", handleHostsChanged); - }, [hostConfig?.id]); + const connect = async (credentials?: { + userProvidedPassword?: string; + userProvidedSshKey?: string; + userProvidedKeyPassword?: string; + }) => { + if (!currentHost?.id) return; + const sid = newSessionId(); + setIsConnecting(true); + try { + const result = await docker.connect(sid, currentHost.id, credentials); + await applyConnectResult(sid, result, !credentials); + } catch (error) { + reportFailure(error, t("docker.connectionFailed")); + } finally { + setIsConnecting(false); + } + }; const initializingRef = React.useRef(false); React.useEffect(() => { - const initSession = async () => { - if (!currentHostConfig?.id || !currentHostConfig.enableDocker) { - return; - } - - if (initializingRef.current) return; - initializingRef.current = true; - - if (sessionId) { - initializingRef.current = false; - return; - } - - setIsConnecting(true); - clearLogs(); - const sid = `docker-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - - try { - const result = await connectDockerSession(sid, currentHostConfig.id, { - useSocks5: currentHostConfig.useSocks5, - socks5Host: currentHostConfig.socks5Host, - socks5Port: currentHostConfig.socks5Port, - socks5Username: currentHostConfig.socks5Username, - socks5Password: currentHostConfig.socks5Password, - socks5ProxyChain: currentHostConfig.socks5ProxyChain, - }); - - if (result?.requires_warpgate) { - setWarpgateRequired(true); - setWarpgateSessionId(sid); - setWarpgateUrl(result.url || ""); - setWarpgateSecurityKey(result.securityKey || ""); - setIsConnecting(false); - return; - } - - if (result?.requires_totp) { - setTotpRequired(true); - setTotpSessionId(sid); - setTotpPrompt(result.prompt || t("docker.verificationCodePrompt")); - setIsConnecting(false); - return; - } - - if (result?.status === "auth_required") { - setShowAuthDialog(true); - setAuthReason( - result.reason === "no_keyboard" ? "no_keyboard" : "auth_failed", - ); - setIsConnecting(false); - return; - } - - setSessionId(sid); - - setIsValidating(true); - const validation = await validateDockerAvailability(sid); - setDockerValidation(validation); - setIsValidating(false); - - if (!validation.available) { - addLog({ - type: "error", - stage: "validation", - message: validation.error || t("docker.error"), - details: validation.code - ? `Error code: ${validation.code}` - : undefined, - }); - dockerConnectRetryRef.current.markFailed(); - } else { - logDockerActivity(); - setTimeout(() => clearLogs(), 1000); - dockerConnectRetryRef.current.markConnected(); - } - } catch (error) { - const dockerError = error as DockerConnectionError; - setIsConnecting(false); - setIsValidating(false); - - if (Array.isArray(dockerError.connectionLogs)) { - setLogs(dockerError.connectionLogs); - } else { - addLog({ - type: "error", - stage: "connection", - message: dockerError.message || t("docker.connectionFailed"), - }); - } - dockerConnectRetryRef.current.markFailed(); - } finally { - setIsConnecting(false); - } - }; - - initSession(); + if (!currentHost?.id || !enabled) return; + if (initializingRef.current || sessionId) return; + initializingRef.current = true; + clearLogs(); + void connect(); return () => { initializingRef.current = false; - if (sessionId) { - disconnectDockerSession(sessionId).catch(() => { - // Silently handle disconnect errors - }); - } }; - }, [currentHostConfig?.id, currentHostConfig?.enableDocker, retryCount]); + }, [currentHost?.id, enabled, retryCount]); + + React.useEffect(() => { + if (!sessionId) return; + return () => { + docker.disconnect(sessionId).catch(() => {}); + }; + }, [sessionId]); React.useEffect(() => { if (!sessionId || !isVisible) return; - const keepalive = setInterval( () => { - keepaliveDockerSession(sessionId).catch(() => { - // Silently handle keepalive errors - }); + docker.keepalive(sessionId).catch(() => {}); }, 10 * 60 * 1000, ); - return () => clearInterval(keepalive); }, [sessionId, isVisible]); const refreshContainers = React.useCallback(async () => { if (!sessionId) return; try { - const data = await listDockerContainers(sessionId, true); - setContainers(data); + setContainers(await docker.listContainers(sessionId, true)); } catch { - // Silently handle polling errors + // the next poll tries again } - }, [sessionId]); + }, [sessionId, docker]); React.useEffect(() => { setHasLoadedContainersOnce(false); @@ -350,7 +311,7 @@ function DockerManagerInner({ setIsLoadingContainers((loading) => hasLoadedContainersOnce ? loading : true, ); - const data = await listDockerContainers(sessionId, true); + const data = await docker.listContainers(sessionId, true); const signature = JSON.stringify(data); const changed = signature !== containersSignatureRef.current; containersSignatureRef.current = signature; @@ -378,124 +339,48 @@ function DockerManagerInner({ setSelectedContainer(null); }, []); - const handleTotpSubmit = async (code: string) => { - if (!totpSessionId || !code) return; - - try { - setIsConnecting(true); - const result = await verifyDockerTOTP(totpSessionId, code); - - if (result?.status === "success") { - setTotpRequired(false); - setTotpPrompt(""); - setSessionId(totpSessionId); - setTotpSessionId(null); - - setIsValidating(true); - const validation = await validateDockerAvailability(totpSessionId); - setDockerValidation(validation); - setIsValidating(false); - - if (!validation.available) { - addLog({ - type: "error", - stage: "validation", - message: validation.error || t("docker.error"), - details: validation.code - ? `Error code: ${validation.code}` - : undefined, - }); - dockerConnectRetryRef.current.markFailed(); - } else { - logDockerActivity(); - dockerConnectRetryRef.current.markConnected(); - } - } - } catch (error) { - console.error("TOTP verification failed:", error); - addLog({ - type: "error", - stage: "auth", - message: t("docker.totpVerificationFailed"), - }); - dockerConnectRetryRef.current.markFailed(); - } finally { - setIsConnecting(false); - } + const closeTab = () => { + if (currentTab !== null) removeTab(currentTab); }; - const handleTotpCancel = () => { - setTotpRequired(false); - setTotpSessionId(null); - setTotpPrompt(""); - setIsConnecting(false); - if (currentTab !== null) { - removeTab(currentTab); + const handleTotpSubmit = async (code: string) => { + if (!prompt || prompt.kind !== "totp" || !code) return; + const sid = prompt.sessionId; + setPrompt(null); + setIsConnecting(true); + try { + await applyConnectResult(sid, await docker.answerTotp(sid, code), false); + } catch (error) { + reportFailure(error, t("docker.totpVerificationFailed")); + } finally { + setIsConnecting(false); } }; const handleWarpgateContinue = async () => { - if (!warpgateSessionId) return; - + if (!prompt || prompt.kind !== "warpgate") return; + const sid = prompt.sessionId; + setPrompt(null); + setIsConnecting(true); try { - setIsConnecting(true); - const result = await verifyDockerWarpgate(warpgateSessionId); - - if (result?.status === "success") { - setWarpgateRequired(false); - setWarpgateUrl(""); - setWarpgateSecurityKey(""); - setSessionId(warpgateSessionId); - setWarpgateSessionId(null); - - setIsValidating(true); - const validation = await validateDockerAvailability(warpgateSessionId); - setDockerValidation(validation); - setIsValidating(false); - - if (!validation.available) { - addLog({ - type: "error", - stage: "validation", - message: validation.error || t("docker.error"), - details: validation.code - ? `Error code: ${validation.code}` - : undefined, - }); - dockerConnectRetryRef.current.markFailed(); - } else { - logDockerActivity(); - dockerConnectRetryRef.current.markConnected(); - } - } + await applyConnectResult(sid, await docker.continueWarpgate(sid), false); } catch (error) { - console.error("Warpgate verification failed:", error); - addLog({ - type: "error", - stage: "auth", - message: t("docker.warpgateVerificationFailed"), - }); - dockerConnectRetryRef.current.markFailed(); + reportFailure(error, t("docker.warpgateVerificationFailed")); } finally { setIsConnecting(false); } }; - const handleWarpgateCancel = () => { - setWarpgateRequired(false); - setWarpgateSessionId(null); - setWarpgateUrl(""); - setWarpgateSecurityKey(""); + const handlePromptCancel = () => { + const sid = prompt?.sessionId; + setPrompt(null); setIsConnecting(false); - if (currentTab !== null) { - removeTab(currentTab); - } + if (sid) docker.disconnect(sid).catch(() => {}); + closeTab(); }; const handleWarpgateOpenUrl = () => { - if (warpgateUrl) { - window.open(warpgateUrl, "_blank", "noopener,noreferrer"); - } + if (prompt?.url) window.open(prompt.url, "_blank", "noopener,noreferrer"); }; const handleAuthSubmit = async (credentials: { @@ -503,75 +388,12 @@ function DockerManagerInner({ sshKey?: string; keyPassword?: string; }) => { - if (!currentHostConfig?.id) return; - setShowAuthDialog(false); - setIsConnecting(true); - - const sid = `docker-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - - try { - const result = await connectDockerSession(sid, currentHostConfig.id, { - userProvidedPassword: credentials.password, - userProvidedSshKey: credentials.sshKey, - userProvidedKeyPassword: credentials.keyPassword, - useSocks5: currentHostConfig.useSocks5, - socks5Host: currentHostConfig.socks5Host, - socks5Port: currentHostConfig.socks5Port, - socks5Username: currentHostConfig.socks5Username, - socks5Password: currentHostConfig.socks5Password, - socks5ProxyChain: currentHostConfig.socks5ProxyChain, - }); - - if (result?.requires_warpgate) { - setWarpgateRequired(true); - setWarpgateSessionId(sid); - setWarpgateUrl(result.url || ""); - setWarpgateSecurityKey(result.securityKey || "N/A"); - setIsConnecting(false); - return; - } - - if (result?.requires_totp) { - setTotpRequired(true); - setTotpSessionId(sid); - setTotpPrompt(result.prompt || t("docker.verificationCodePrompt")); - setIsConnecting(false); - return; - } - - if (result?.status === "auth_required") { - setShowAuthDialog(true); - setAuthReason("auth_failed"); - setIsConnecting(false); - return; - } - - setSessionId(sid); - - setIsValidating(true); - const validation = await validateDockerAvailability(sid); - setDockerValidation(validation); - setIsValidating(false); - - if (!validation.available) { - dockerConnectRetryRef.current.markFailed(); - } else { - logDockerActivity(); - dockerConnectRetryRef.current.markConnected(); - } - } catch (error) { - setIsConnecting(false); - setIsValidating(false); - addLog({ - type: "error", - stage: "connection", - message: error?.message || t("docker.connectionFailed"), - }); - dockerConnectRetryRef.current.markFailed(); - } finally { - setIsConnecting(false); - } + await connect({ + userProvidedPassword: credentials.password, + userProvidedSshKey: credentials.sshKey, + userProvidedKeyPassword: credentials.keyPassword, + }); }; const handleAuthCancel = () => { @@ -592,15 +414,10 @@ function DockerManagerInner({ connect: () => { handleRetry(); }, - enabled: - !!currentHostConfig?.enableDocker && - !totpRequired && - !warpgateRequired && - !showAuthDialog, + enabled: enabled && !prompt && !showAuthDialog, autoStart: false, }); - const dockerConnectRetryRef = React.useRef(dockerConnectRetry); - dockerConnectRetryRef.current = dockerConnectRetry; + retryRef.current = dockerConnectRetry; const topMarginPx = isTopbarOpen ? 74 : 16; const leftMarginPx = 8; @@ -619,9 +436,9 @@ function DockerManagerInner({ const containerClass = embedded ? "h-full w-full text-foreground overflow-hidden bg-transparent" - : "bg-canvas text-foreground rounded-lg border-2 border-edge overflow-hidden"; + : "bg-canvas text-foreground border-2 border-edge overflow-hidden"; - if (!currentHostConfig?.enableDocker) { + if (!enabled) { return (
@@ -649,6 +466,40 @@ function DockerManagerInner({ ); } + const dialogs = ( + <> + + + {currentHost && ( + + )} + + ); + if (isConnecting || isValidating) { return (
@@ -662,6 +513,7 @@ function DockerManagerInner({ nextRetryInMs={dockerConnectRetry.nextRetryInMs} onManualRetry={dockerConnectRetry.retryNow} /> + {dialogs}
); } @@ -689,12 +541,12 @@ function DockerManagerInner({ {viewMode === "detail" && sessionId && selectedContainer && - currentHostConfig ? ( + currentHost ? ( @@ -833,34 +685,7 @@ function DockerManagerInner({
)}
- - - {currentHostConfig && ( - - )} + {dialogs} ) { const { t } = useTranslation(); - const [host, setHost] = useState(null); - const [loading, setLoading] = useState(true); + const record = useHost(config.hostId || undefined); - useEffect(() => { - if (!config.hostId) { - setLoading(false); - return; - } - getSSHHosts() - .then((hosts) => { - const found = hosts.find( - (h) => h.id === config.hostId && h.enableDocker, - ); - setHost(found ?? null); - }) - .catch(() => {}) - .finally(() => setLoading(false)); - }, [config.hostId]); - - if (!config.hostId) { + if (!config.hostId || !record || !dockerEnabled(record)) { return (
@@ -47,22 +27,7 @@ function DockerWidget({ ); } - if (loading) { - return ( -
- {t("homepage.loading")} -
- ); - } - - if (!host) { - return ( -
- {t("homepage.widgetNoHostSelected")} -
- ); - } - + const host = toDockerHost(record as unknown as Record); return (
} /> @@ -71,8 +36,8 @@ function DockerWidget({ onMouseDown={(e) => e.stopPropagation()} > = { +export const dockerWidget: WidgetDefinition = { id: "docker_widget", name: "Docker Manager", description: "Embedded Docker container manager for a configured host", diff --git a/plugins/docker/src/frontend/DockerWidgetEditForm.tsx b/plugins/docker/src/frontend/DockerWidgetEditForm.tsx index d8e57086a..6e1166aa8 100644 --- a/plugins/docker/src/frontend/DockerWidgetEditForm.tsx +++ b/plugins/docker/src/frontend/DockerWidgetEditForm.tsx @@ -1,18 +1,37 @@ -import type { - DockerWidgetConfig, - WidgetEditFormProps, -} from "@/types/homepage-types"; -import { SingleHostEditForm } from "@/features/homepage/dialogs/SingleHostEditForm"; +import { useHosts, useTranslation } from "@termix/plugin-sdk/frontend"; +import { Select2 } from "@termix/plugin-sdk/ui"; +import type { DockerWidgetConfig, WidgetEditFormProps } from "./homepage"; +import { dockerEnabled } from "./types"; export function DockerWidgetEditForm({ config, onChange, }: WidgetEditFormProps) { + const { t } = useTranslation(); + const { hosts } = useHosts(); + const options = hosts.filter( + (host) => host.enableSsh !== false && dockerEnabled(host), + ); + return ( - onChange({ ...config, hostId })} - filter={(h) => !!(h.enableSsh && h.enableDocker)} - /> +
+ + + onChange({ ...config, hostId: Number(e.target.value) }) + } + className="h-8 text-xs border border-border bg-background px-2" + > + + {options.map((host) => ( + + ))} + +
); } diff --git a/plugins/docker/src/frontend/HostDockerTab.tsx b/plugins/docker/src/frontend/HostDockerTab.tsx deleted file mode 100644 index e59572637..000000000 --- a/plugins/docker/src/frontend/HostDockerTab.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { Box } from "lucide-react"; -import { Select2 } from "@/components/select2"; -import { SectionCard, SettingRow, FakeSwitch } from "@/components/section-card"; -import type { HostEditorForm } from "@/sidebar/HostEditorData"; - -type SetHostField = ( - key: K, - value: HostEditorForm[K], -) => void; - -export function HostDockerTab({ - form, - setField, -}: { - form: HostEditorForm; - setField: SetHostField; -}) { - const { t } = useTranslation(); - const dockerConfig = form.dockerConfig ?? { runtime: "docker" as const }; - const runtime = - dockerConfig.runtime === "podman" - ? ("podman" as const) - : ("docker" as const); - - return ( - } - > -
- - setField("enableDocker", v)} - /> - - {form.enableDocker && ( - - - setField("dockerConfig", { - ...dockerConfig, - runtime: e.target.value as "docker" | "podman", - }) - } - className="h-7 w-44 text-xs border border-border bg-background px-2 outline-none focus:ring-1 focus:ring-ring" - > - - - - - )} -
-
- ); -} diff --git a/plugins/docker/src/frontend/components/ConsoleTerminal.tsx b/plugins/docker/src/frontend/components/ConsoleTerminal.tsx index bd0112d65..89f6c1755 100644 --- a/plugins/docker/src/frontend/components/ConsoleTerminal.tsx +++ b/plugins/docker/src/frontend/components/ConsoleTerminal.tsx @@ -1,39 +1,42 @@ -import { getErrorMessage } from "@/lib/error-message.js"; +import { getErrorMessage } from "../error-message"; +import { + Button, + Card, + CardContent, + ConnectionLogProvider, + ConnectionScreen, + DEFAULT_TERMINAL_CONFIG, + RobustClipboardProvider, + Select2, + TERMINAL_FONTS, + copyToClipboard, + ensureTerminalFontsLoaded, + isElectron, + pluginWsUrl, + readFromClipboard, + resolveConnectionOrigin, + resolveTermixThemeColors, + useAppTheme as useTheme, + useConnectionLog, +} from "@termix/plugin-sdk/ui"; +import type { DockerHost } from "../types"; import React from "react"; import { useXTerm } from "react-xtermjs"; import { FitAddon } from "@xterm/addon-fit"; import { ClipboardAddon } from "@xterm/addon-clipboard"; -import { RobustClipboardProvider } from "@/lib/clipboard-provider"; -import { copyToClipboard, readFromClipboard } from "@/lib/clipboard"; import { WebLinksAddon } from "@xterm/addon-web-links"; -import { Button } from "@/components/button.tsx"; -import { Select2 } from "@/components/select2"; -import { Card, CardContent } from "@/components/card.tsx"; -import { resolveConnectionOrigin } from "@/lib/connection-origin.ts"; -import { pluginWsUrl } from "@/lib/plugin-transport"; import { Terminal as TerminalIcon, Power, PowerOff } from "lucide-react"; import { toast } from "sonner"; -import type { SSHHost } from "@/types"; -import { isElectron } from "@/main-axios.ts"; import { useTranslation, useConnectionRetry, } from "@termix/plugin-sdk/frontend"; -import { resolveTermixThemeColors } from "@/lib/terminal-look/terminal-theme"; -import { DEFAULT_TERMINAL_CONFIG, TERMINAL_FONTS } from "@/lib/terminal-themes"; -import { ensureTerminalFontsLoaded } from "@/lib/terminal-look/terminal-global-styles"; -import { useTheme } from "@/components/theme-provider"; -import { ConnectionScreen } from "@/components/connection/ConnectionScreen.tsx"; -import { - ConnectionLogProvider, - useConnectionLog, -} from "@/ssh/connection-log/ConnectionLogContext.tsx"; interface ConsoleTerminalProps { containerId: string; containerName: string; containerState: string; - hostConfig: SSHHost; + hostConfig: DockerHost; } export function ConsoleTerminal( @@ -58,7 +61,12 @@ function ConsoleTerminalInner({ const { addLog, clearLogs } = useConnectionLog(); const terminalConfig = React.useMemo( - () => ({ ...DEFAULT_TERMINAL_CONFIG, ...hostConfig.terminalConfig }), + () => ({ + ...DEFAULT_TERMINAL_CONFIG, + ...(hostConfig.terminalConfig as Partial< + typeof DEFAULT_TERMINAL_CONFIG + > | null), + }), [hostConfig.terminalConfig], ); @@ -321,7 +329,11 @@ function ConsoleTerminalInner({ JSON.stringify({ type: "connect", data: { - hostConfig, + hostConfig: { + id: hostConfig.id, + syncId: hostConfig.syncId ?? null, + ip: hostConfig.ip, + }, containerId, shell: selectedShell, cols, diff --git a/plugins/docker/src/frontend/components/ContainerCard.tsx b/plugins/docker/src/frontend/components/ContainerCard.tsx index c5cdc3b08..4be50290b 100644 --- a/plugins/docker/src/frontend/components/ContainerCard.tsx +++ b/plugins/docker/src/frontend/components/ContainerCard.tsx @@ -1,7 +1,7 @@ -import { getErrorMessage } from "@/lib/error-message.js"; +import { getErrorMessage } from "../error-message"; +import { Button, Card, useConfirmation } from "@termix/plugin-sdk/ui"; +import type { DockerContainer } from "../types"; import React from "react"; -import { Card } from "@/components/card.tsx"; -import { Button } from "@/components/button.tsx"; import { Box, Play, @@ -14,16 +14,7 @@ import { } from "lucide-react"; import { toast } from "sonner"; import { useTranslation } from "@termix/plugin-sdk/frontend"; -import type { DockerContainer } from "@/types"; -import { - startDockerContainer, - stopDockerContainer, - restartDockerContainer, - pauseDockerContainer, - unpauseDockerContainer, - removeDockerContainer, -} from "../docker-api"; -import { useConfirmation } from "@/hooks/use-confirmation.ts"; +import { useDockerApi } from "../docker-api"; interface ContainerCardProps { container: DockerContainer; @@ -61,6 +52,7 @@ export function ContainerCard({ }: ContainerCardProps): React.ReactElement { const { t } = useTranslation(); const { confirmWithToast } = useConfirmation(); + const docker = useDockerApi(); const [isStarting, setIsStarting] = React.useState(false); const [isStopping, setIsStopping] = React.useState(false); const [isRestarting, setIsRestarting] = React.useState(false); @@ -82,7 +74,7 @@ export function ContainerCard({ e.stopPropagation(); setIsStarting(true); try { - await startDockerContainer(sessionId, container.id); + await docker.containerAction(sessionId, container.id, "start"); toast.success(t("docker.containerStarted", { name: containerName })); onRefresh?.(); } catch (err) { @@ -100,7 +92,7 @@ export function ContainerCard({ e.stopPropagation(); setIsStopping(true); try { - await stopDockerContainer(sessionId, container.id); + await docker.containerAction(sessionId, container.id, "stop"); toast.success(t("docker.containerStopped", { name: containerName })); onRefresh?.(); } catch (err) { @@ -118,7 +110,7 @@ export function ContainerCard({ e.stopPropagation(); setIsRestarting(true); try { - await restartDockerContainer(sessionId, container.id); + await docker.containerAction(sessionId, container.id, "restart"); toast.success(t("docker.containerRestarted", { name: containerName })); onRefresh?.(); } catch (err) { @@ -137,10 +129,10 @@ export function ContainerCard({ setIsPausing(true); try { if (container.state === "paused") { - await unpauseDockerContainer(sessionId, container.id); + await docker.containerAction(sessionId, container.id, "unpause"); toast.success(t("docker.containerUnpaused", { name: containerName })); } else { - await pauseDockerContainer(sessionId, container.id); + await docker.containerAction(sessionId, container.id, "pause"); toast.success(t("docker.containerPaused", { name: containerName })); } onRefresh?.(); @@ -166,7 +158,7 @@ export function ContainerCard({ async () => { setIsRemoving(true); try { - await removeDockerContainer( + await docker.removeContainer( sessionId, container.id, container.state === "running", diff --git a/plugins/docker/src/frontend/components/ContainerDetail.tsx b/plugins/docker/src/frontend/components/ContainerDetail.tsx index 3d77c8552..d05138178 100644 --- a/plugins/docker/src/frontend/components/ContainerDetail.tsx +++ b/plugins/docker/src/frontend/components/ContainerDetail.tsx @@ -1,7 +1,6 @@ +import { Button, Card, Separator } from "@termix/plugin-sdk/ui"; +import type { DockerContainer, DockerHost } from "../types"; import React from "react"; -import { Button } from "@/components/button.tsx"; -import { Card } from "@/components/card.tsx"; -import { Separator } from "@/components/separator.tsx"; import { Activity, ArrowLeft, @@ -11,7 +10,6 @@ import { Terminal, } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; -import type { DockerContainer, SSHHost } from "@/types"; import { LogViewer } from "./LogViewer.tsx"; import { ContainerStats } from "./ContainerStats.tsx"; import { ConsoleTerminal } from "./ConsoleTerminal.tsx"; @@ -23,7 +21,7 @@ interface ContainerDetailProps { sessionId: string; containerId: string; containers: DockerContainer[]; - hostConfig: SSHHost; + hostConfig: DockerHost; onBack: () => void; initialTab?: DetailTab; } diff --git a/plugins/docker/src/frontend/components/ContainerList.tsx b/plugins/docker/src/frontend/components/ContainerList.tsx index 29a4bed3d..e058de1e1 100644 --- a/plugins/docker/src/frontend/components/ContainerList.tsx +++ b/plugins/docker/src/frontend/components/ContainerList.tsx @@ -1,7 +1,7 @@ +import type { DockerContainer } from "../types"; import React from "react"; import { Box } from "lucide-react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; -import type { DockerContainer } from "@/types"; import { ContainerCard } from "./ContainerCard.tsx"; interface ContainerListProps { diff --git a/plugins/docker/src/frontend/components/ContainerStats.tsx b/plugins/docker/src/frontend/components/ContainerStats.tsx index e3c4297e5..3560961ef 100644 --- a/plugins/docker/src/frontend/components/ContainerStats.tsx +++ b/plugins/docker/src/frontend/components/ContainerStats.tsx @@ -1,4 +1,6 @@ -import { getErrorMessage } from "@/lib/error-message.js"; +import { getErrorMessage } from "../error-message"; +import { SectionCard, useAdaptivePolling } from "@termix/plugin-sdk/ui"; +import type { DockerStats } from "../types"; import React from "react"; import { Activity, @@ -9,12 +11,9 @@ import { Network, RefreshCw, } from "lucide-react"; -import type { DockerStats } from "@/types"; -import { getContainerStats } from "../docker-api"; +import { useDockerApi } from "../docker-api"; import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { SectionCard } from "@/components/section-card"; import { DockerBadge } from "./ContainerCard.tsx"; -import { useAdaptivePolling } from "@/hooks/use-adaptive-polling.ts"; interface ContainerStatsProps { sessionId: string; @@ -30,6 +29,7 @@ export function ContainerStats({ containerState, }: ContainerStatsProps): React.ReactElement { const { t } = useTranslation(); + const docker = useDockerApi(); const [stats, setStats] = React.useState(null); const [isLoading, setIsLoading] = React.useState(false); const [error, setError] = React.useState(null); @@ -41,7 +41,7 @@ export function ContainerStats({ setIsLoading(true); setError(null); try { - const data = await getContainerStats(sessionId, containerId); + const data = await docker.stats(sessionId, containerId); const previous = statsRef.current; const changed = !previous || @@ -58,7 +58,7 @@ export function ContainerStats({ } finally { setIsLoading(false); } - }, [sessionId, containerId, containerState, t]); + }, [sessionId, containerId, containerState, t, docker]); useAdaptivePolling( fetchStats, @@ -246,7 +246,7 @@ export function ContainerStats({
diff --git a/plugins/docker/src/frontend/components/ContainerTable.tsx b/plugins/docker/src/frontend/components/ContainerTable.tsx index 4196e7ec8..d939278ce 100644 --- a/plugins/docker/src/frontend/components/ContainerTable.tsx +++ b/plugins/docker/src/frontend/components/ContainerTable.tsx @@ -1,11 +1,11 @@ -import { getErrorMessage } from "@/lib/error-message.js"; +import { getErrorMessage } from "../error-message"; +import { Button } from "@termix/plugin-sdk/ui"; +import type { DockerContainer } from "../types"; import React from "react"; import { Box, List, Play, RefreshCw, Square, Terminal } from "lucide-react"; import { toast } from "sonner"; import { useTranslation } from "@termix/plugin-sdk/frontend"; -import { Button } from "@/components/button.tsx"; -import type { DockerContainer } from "@/types"; -import { startDockerContainer, stopDockerContainer } from "../docker-api"; +import { useDockerApi } from "../docker-api"; import { DockerBadge } from "./ContainerCard.tsx"; type DetailTab = "logs" | "stats" | "console"; @@ -30,6 +30,7 @@ export function ContainerTable({ statusFilter = "all", }: ContainerTableProps): React.ReactElement { const { t } = useTranslation(); + const docker = useDockerApi(); const [pendingId, setPendingId] = React.useState(null); const filtered = React.useMemo(() => { @@ -56,10 +57,10 @@ export function ContainerTable({ setPendingId(container.id); try { if (container.state === "running") { - await stopDockerContainer(sessionId, container.id); + await docker.containerAction(sessionId, container.id, "stop"); toast.success(t("docker.containerStopped", { name: containerName })); } else { - await startDockerContainer(sessionId, container.id); + await docker.containerAction(sessionId, container.id, "start"); toast.success(t("docker.containerStarted", { name: containerName })); } onRefresh?.(); diff --git a/plugins/docker/src/frontend/components/LogViewer.tsx b/plugins/docker/src/frontend/components/LogViewer.tsx index 1804d4446..adac951f0 100644 --- a/plugins/docker/src/frontend/components/LogViewer.tsx +++ b/plugins/docker/src/frontend/components/LogViewer.tsx @@ -1,15 +1,17 @@ -import { getErrorMessage } from "@/lib/error-message.js"; +import { getErrorMessage } from "../error-message"; +import { + Button, + Input, + Select2, + Separator, + useAdaptivePolling, +} from "@termix/plugin-sdk/ui"; +import type { DockerLogOptions } from "../types"; import React from "react"; -import { Button } from "@/components/button.tsx"; -import { Input } from "@/components/input.tsx"; -import { Separator } from "@/components/separator.tsx"; import { Download, RefreshCw, Search, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { useTranslation } from "@termix/plugin-sdk/frontend"; -import type { DockerLogOptions } from "@/types"; -import { getContainerLogs, downloadContainerLogs } from "../docker-api"; -import { useAdaptivePolling } from "@/hooks/use-adaptive-polling.ts"; -import { Select2 } from "@/components/select2"; +import { useDockerApi } from "../docker-api"; interface LogViewerProps { sessionId: string; @@ -60,6 +62,7 @@ export function LogViewer({ containerName, }: LogViewerProps): React.ReactElement { const { t } = useTranslation(); + const docker = useDockerApi(); const [rawLogs, setRawLogs] = React.useState([]); const [isLoading, setIsLoading] = React.useState(false); const [isDownloading, setIsDownloading] = React.useState(false); @@ -78,7 +81,7 @@ export function LogViewer({ tail: tailLines === "all" ? undefined : parseInt(tailLines, 10), timestamps: showTimestamps, }; - const data = await getContainerLogs(sessionId, containerId, options); + const data = await docker.logs(sessionId, containerId, options); const next = data.logs.split("\n").filter(Boolean); const changed = next.length !== rawLogsRef.current.length || @@ -89,11 +92,13 @@ export function LogViewer({ } return changed; } catch (error) { - toast.error(`Failed to fetch logs: ${getErrorMessage(error)}`); + toast.error( + t("docker.failedToFetchLogs", { error: getErrorMessage(error) }), + ); } finally { setIsLoading(false); } - }, [sessionId, containerId, tailLines, showTimestamps]); + }, [sessionId, containerId, tailLines, showTimestamps, docker, t]); React.useEffect(() => { fetchLogs(); @@ -120,9 +125,10 @@ export function LogViewer({ const handleDownload = async () => { setIsDownloading(true); try { - const blob = await downloadContainerLogs(sessionId, containerId, { + const data = await docker.logs(sessionId, containerId, { timestamps: showTimestamps, }); + const blob = new Blob([data.logs], { type: "text/plain" }); const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; @@ -133,7 +139,9 @@ export function LogViewer({ document.body.removeChild(a); toast.success(t("docker.logsDownloaded")); } catch (error) { - toast.error(`Failed to download logs: ${getErrorMessage(error)}`); + toast.error( + t("docker.failedToDownloadLogs", { error: getErrorMessage(error) }), + ); } finally { setIsDownloading(false); } diff --git a/plugins/docker/src/frontend/docker-api.ts b/plugins/docker/src/frontend/docker-api.ts index afbacd890..14d4e62b7 100644 --- a/plugins/docker/src/frontend/docker-api.ts +++ b/plugins/docker/src/frontend/docker-api.ts @@ -1,345 +1,170 @@ -import axios from "axios"; -import { dockerApi, handleApiError } from "@/main-axios"; +import { useMemo } from "react"; +import { + usePluginApi, + type PluginApiClient, +} from "@termix/plugin-sdk/frontend"; +import type { ConnectionStage } from "@termix/plugin-sdk/ui"; import type { DockerContainer, DockerLogOptions, DockerStats, DockerValidation, -} from "@/types/index"; +} from "./types"; -type ApiConnectionLog = { +export interface ApiConnectionLog { type: "info" | "success" | "warning" | "error"; - stage: string; + stage: ConnectionStage; message: string; - details?: Record; -}; + details?: string; +} -type ConnectErrorResponse = { - error?: string; - message?: string; - connectionLogs?: ApiConnectionLog[]; - requires_totp?: boolean; - requires_warpgate?: boolean; - sessionId?: string; - prompt?: string; - url?: string; - securityKey?: string; +/** One step of a connect: connected, a prompt to answer, or auth needed. */ +export interface ConnectResult { + success?: boolean; status?: string; reason?: string; -}; - -export async function connectDockerSession( - sessionId: string, - hostId: number, - config?: { - userProvidedPassword?: string; - userProvidedSshKey?: string; - userProvidedKeyPassword?: string; - forceKeyboardInteractive?: boolean; - useSocks5?: boolean; - socks5Host?: string; - socks5Port?: number; - socks5Username?: string; - socks5Password?: string; - socks5ProxyChain?: unknown; - }, -): Promise<{ - success?: boolean; message?: string; requires_totp?: boolean; prompt?: string; isPassword?: boolean; - status?: string; - reason?: string; - connectionLogs?: ApiConnectionLog[]; + retry?: boolean; requires_warpgate?: boolean; - url?: string; + url?: string | null; securityKey?: string; -}> { - try { - const response = await dockerApi.post("/ssh/connect", { - sessionId, - hostId, - ...config, - }); - return response.data; - } catch (error: unknown) { - if ( - axios.isAxiosError(error) && - error.response?.data?.status === "auth_required" - ) { - return error.response.data; + connectionLogs?: ApiConnectionLog[]; +} + +/** An API failure carrying the server's message and connection logs. */ +export class DockerApiError extends Error { + constructor( + message: string, + readonly status?: number, + readonly connectionLogs?: ApiConnectionLog[], + ) { + super(message); + this.name = "DockerApiError"; + } +} + +function apiError(error: unknown, action: string): DockerApiError { + const response = ( + error as { + response?: { + status?: number; + data?: { + error?: string; + message?: string; + connectionLogs?: ApiConnectionLog[]; + }; + }; } - if ( - axios.isAxiosError(error) && - error.response?.data?.requires_totp - ) { - return error.response.data; - } - if ( - axios.isAxiosError(error) && - error.response?.data?.requires_warpgate - ) { - return error.response.data; - } - if ( - axios.isAxiosError(error) && - error.response?.data?.connectionLogs - ) { - const data = error.response.data; - const errorWithLogs = new Error( - data.error || data.message || error.message, - ); - Object.assign(errorWithLogs, { - connectionLogs: data.connectionLogs, - }); - throw errorWithLogs; - } - throw handleApiError(error, "connect to Docker SSH session"); - } + )?.response; + const data = response?.data; + const message = + data?.error || + data?.message || + (error instanceof Error ? error.message : `Failed to ${action}`); + return new DockerApiError(message, response?.status, data?.connectionLogs); } -export async function verifyDockerTOTP( - sessionId: string, - totpCode: string, -): Promise<{ status: string; message: string }> { +async function call( + action: string, + request: () => Promise<{ data: T }>, +): Promise { try { - const response = await dockerApi.post("/ssh/connect-totp", { - sessionId, - totpCode, - }); - return response.data; + return (await request()).data; } catch (error) { - throw handleApiError(error, "verify Docker TOTP"); + throw apiError(error, action); } } -export async function verifyDockerWarpgate( - sessionId: string, -): Promise<{ status: string; message: string }> { - try { - const response = await dockerApi.post("/ssh/connect-warpgate", { - sessionId, - }); - return response.data; - } catch (error) { - throw handleApiError(error, "verify Docker Warpgate"); - } -} +export type ContainerAction = + "start" | "stop" | "restart" | "pause" | "unpause"; -export async function disconnectDockerSession( - sessionId: string, -): Promise<{ success: boolean; message: string }> { - try { - const response = await dockerApi.post("/ssh/disconnect", { - sessionId, - }); - return response.data; - } catch (error) { - throw handleApiError(error, "disconnect from Docker SSH session"); - } -} - -export async function keepaliveDockerSession( - sessionId: string, -): Promise<{ success: boolean }> { - try { - const response = await dockerApi.post("/ssh/keepalive", { - sessionId, - }); - return response.data; - } catch (error) { - throw handleApiError(error, "keepalive Docker SSH session"); - } -} - -export async function getDockerSessionStatus( - sessionId: string, -): Promise<{ success: boolean; connected: boolean }> { - try { - const response = await dockerApi.get("/ssh/status", { - params: { sessionId }, - }); - return response.data; - } catch (error) { - throw handleApiError(error, "get Docker session status"); - } -} - -export async function validateDockerAvailability( - sessionId: string, -): Promise { - try { - const response = await dockerApi.get(`/validate/${sessionId}`); - return response.data; - } catch (error) { - throw handleApiError(error, "validate Docker availability"); - } -} - -export async function listDockerContainers( - sessionId: string, - all: boolean = true, -): Promise { - try { - const response = await dockerApi.get(`/containers/${sessionId}`, { - params: { all }, - }); - return response.data; - } catch (error) { - throw handleApiError(error, "list Docker containers"); - } -} - -export async function getDockerContainerDetails( - sessionId: string, - containerId: string, -): Promise { - try { - const response = await dockerApi.get( - `/containers/${sessionId}/${containerId}`, - ); - return response.data; - } catch (error) { - throw handleApiError(error, "get Docker container details"); - } -} - -export async function startDockerContainer( - sessionId: string, - containerId: string, -): Promise<{ success: boolean; message: string }> { - try { - const response = await dockerApi.post( - `/containers/${sessionId}/${containerId}/start`, - ); - return response.data; - } catch (error) { - throw handleApiError(error, "start Docker container"); - } -} - -export async function stopDockerContainer( - sessionId: string, - containerId: string, -): Promise<{ success: boolean; message: string }> { - try { - const response = await dockerApi.post( - `/containers/${sessionId}/${containerId}/stop`, - ); - return response.data; - } catch (error) { - throw handleApiError(error, "stop Docker container"); - } -} - -export async function restartDockerContainer( - sessionId: string, - containerId: string, -): Promise<{ success: boolean; message: string }> { - try { - const response = await dockerApi.post( - `/containers/${sessionId}/${containerId}/restart`, - ); - return response.data; - } catch (error) { - throw handleApiError(error, "restart Docker container"); - } -} - -export async function pauseDockerContainer( - sessionId: string, - containerId: string, -): Promise<{ success: boolean; message: string }> { - try { - const response = await dockerApi.post( - `/containers/${sessionId}/${containerId}/pause`, - ); - return response.data; - } catch (error) { - throw handleApiError(error, "pause Docker container"); - } -} - -export async function unpauseDockerContainer( - sessionId: string, - containerId: string, -): Promise<{ success: boolean; message: string }> { - try { - const response = await dockerApi.post( - `/containers/${sessionId}/${containerId}/unpause`, - ); - return response.data; - } catch (error) { - throw handleApiError(error, "unpause Docker container"); - } -} - -export async function removeDockerContainer( - sessionId: string, - containerId: string, - force: boolean = false, -): Promise<{ success: boolean; message: string }> { - try { - const response = await dockerApi.delete( - `/containers/${sessionId}/${containerId}/remove`, - { - params: { force }, +export function createDockerApi(api: PluginApiClient) { + return { + connect: ( + sessionId: string, + hostId: number, + credentials?: { + userProvidedPassword?: string; + userProvidedSshKey?: string; + userProvidedKeyPassword?: string; }, - ); - return response.data; - } catch (error) { - throw handleApiError(error, "remove Docker container"); - } + ) => + call("connect to Docker", () => + api.post("/ssh/connect", { sessionId, hostId, ...credentials }), + ), + + answerTotp: (sessionId: string, totpCode: string) => + call("verify the code", () => + api.post("/ssh/connect-totp", { sessionId, totpCode }), + ), + + continueWarpgate: (sessionId: string) => + call("complete Warpgate authentication", () => + api.post("/ssh/connect-warpgate", { sessionId }), + ), + + disconnect: (sessionId: string) => + call("disconnect from Docker", () => + api.post("/ssh/disconnect", { sessionId }), + ), + + keepalive: (sessionId: string) => + call("keep the Docker session alive", () => + api.post("/ssh/keepalive", { sessionId }), + ), + + validate: (sessionId: string) => + call("validate Docker", () => + api.get(`/validate/${sessionId}`), + ), + + listContainers: (sessionId: string, all = true) => + call("list containers", () => + api.get(`/containers/${sessionId}`, { params: { all } }), + ), + + containerAction: ( + sessionId: string, + containerId: string, + action: ContainerAction, + ) => + call<{ success: boolean; message: string }>( + `${action} the container`, + () => api.post(`/containers/${sessionId}/${containerId}/${action}`), + ), + + removeContainer: (sessionId: string, containerId: string, force = false) => + call<{ success: boolean; message: string }>("remove the container", () => + api.delete(`/containers/${sessionId}/${containerId}/remove`, { + params: { force }, + }), + ), + + logs: ( + sessionId: string, + containerId: string, + options?: DockerLogOptions, + ) => + call<{ logs: string }>("load container logs", () => + api.get(`/containers/${sessionId}/${containerId}/logs`, { + params: options, + }), + ), + + stats: (sessionId: string, containerId: string) => + call("load container stats", () => + api.get(`/containers/${sessionId}/${containerId}/stats`), + ), + }; } -export async function getContainerLogs( - sessionId: string, - containerId: string, - options?: DockerLogOptions, -): Promise<{ logs: string }> { - try { - const response = await dockerApi.get( - `/containers/${sessionId}/${containerId}/logs`, - { - params: options, - }, - ); - return response.data; - } catch (error) { - throw handleApiError(error, "get container logs"); - } -} +export type DockerApi = ReturnType; -export async function downloadContainerLogs( - sessionId: string, - containerId: string, - options?: DockerLogOptions, -): Promise { - try { - const response = await dockerApi.get( - `/containers/${sessionId}/${containerId}/logs`, - { - params: { ...options, download: true }, - responseType: "blob", - }, - ); - return response.data; - } catch (error) { - throw handleApiError(error, "download container logs"); - } -} - -export async function getContainerStats( - sessionId: string, - containerId: string, -): Promise { - try { - const response = await dockerApi.get( - `/containers/${sessionId}/${containerId}/stats`, - ); - return response.data; - } catch (error) { - throw handleApiError(error, "get container stats"); - } +/** The Docker API on this plugin's own client. */ +export function useDockerApi(): DockerApi { + const api = usePluginApi(); + return useMemo(() => createDockerApi(api), [api]); } diff --git a/plugins/docker/src/frontend/error-message.ts b/plugins/docker/src/frontend/error-message.ts new file mode 100644 index 000000000..6ef95453a --- /dev/null +++ b/plugins/docker/src/frontend/error-message.ts @@ -0,0 +1,6 @@ +export function getErrorMessage( + error: unknown, + fallback = "Unknown error", +): string { + return error instanceof Error ? error.message : fallback; +} diff --git a/plugins/docker/src/frontend/homepage.ts b/plugins/docker/src/frontend/homepage.ts new file mode 100644 index 000000000..e90a3e231 --- /dev/null +++ b/plugins/docker/src/frontend/homepage.ts @@ -0,0 +1,24 @@ +import type { HomepageWidgetContribution } from "@termix/plugin-sdk/frontend"; + +/** The homepage's grid step, in pixels. */ +export const GRID_SIZE = 30; + +export interface DockerWidgetConfig { + hostId: number; +} + +/** What the homepage hands a widget; only the fields this plugin reads. */ +export interface WidgetComponentProps { + widget: { title?: string | null }; + config: C; +} + +export interface WidgetEditFormProps { + config: C; + onChange: (config: C) => void; +} + +export type WidgetDefinition = Omit< + HomepageWidgetContribution, + "defaultConfig" +> & { defaultConfig: C }; diff --git a/plugins/docker/src/frontend/index.tsx b/plugins/docker/src/frontend/index.tsx index 1319fd295..1e192ecc1 100644 --- a/plugins/docker/src/frontend/index.tsx +++ b/plugins/docker/src/frontend/index.tsx @@ -2,21 +2,20 @@ import type { ComponentType } from "react"; import { Box } from "lucide-react"; import type { HomepageWidgetContribution, - HostEditorSectionProps, StandaloneViewProps, TabProps, TermixApp, } from "@termix/plugin-sdk/frontend"; -import type { SSHHost } from "@/types"; import { DockerManager } from "./DockerManager"; import DockerApp from "./DockerApp"; -import { HostDockerTab } from "./HostDockerTab"; import { dockerWidget } from "./DockerWidget"; +import { dockerEnabled, toDockerHost } from "./types"; -function DockerTab({ sshHost, label, isVisible }: TabProps) { +function DockerTab({ host, sshHost, label, isVisible }: TabProps) { + const record = (host ?? sshHost) as Record | undefined; return ( ; } -function DockerHostSection({ form, setField }: HostEditorSectionProps) { - return ( - [0]["setField"]} - /> - ); -} - export function activate(app: TermixApp): void { + // Host actions filter synchronously, so the permission is read once here. + let canUse = true; + void app.hasPermission("use").then((allowed) => { + canUse = allowed; + }); + app.registerTab("docker", DockerTab, { icon: Box, titleKey: "nav.docker", @@ -58,16 +54,7 @@ export function activate(app: TermixApp): void { order: 30, tabType: "docker", copyUrlView: "docker", - when: (host) => !!host.enableSsh && !!host.enableDocker, - }); - - app.registerHostEditorSection({ - id: "docker", - group: "ssh", - titleKey: "hosts.tabDocker", - icon: Box, - order: 30, - component: DockerHostSection, + when: (host) => canUse && !!host.enableSsh && dockerEnabled(host), }); app.registerHomepageWidget( diff --git a/plugins/docker/src/frontend/types.ts b/plugins/docker/src/frontend/types.ts new file mode 100644 index 000000000..fbd39d71b --- /dev/null +++ b/plugins/docker/src/frontend/types.ts @@ -0,0 +1,86 @@ +export interface DockerContainer { + id: string; + name: string; + image: string; + status: string; + state: + | "created" + | "running" + | "paused" + | "restarting" + | "removing" + | "exited" + | "dead"; + ports: string; + created: string; + command?: string; + labels?: Record; + networks?: string[]; + mounts?: string[]; +} + +export interface DockerStats { + cpu: string; + memoryUsed: string; + memoryLimit: string; + memoryPercent: string; + netInput: string; + netOutput: string; + blockRead: string; + blockWrite: string; + pids?: string; +} + +export interface DockerLogOptions { + tail?: number; + timestamps?: boolean; + since?: string; + until?: string; + follow?: boolean; +} + +export interface DockerValidation { + available: boolean; + version?: string; + runtime?: "docker" | "podman"; + error?: string; + code?: string; +} + +/** What the Docker views read from a host. */ +export interface DockerHost { + id: number; + name?: string; + ip: string; + port: number; + username?: string; + syncId?: string | null; + connectionOrigin?: "local" | "remote" | null; + terminalConfig?: Record | null; + pluginSettings?: Record | undefined>; +} + +/** Any host record the shell or an API hands over, as a DockerHost. */ +export function toDockerHost(record: Record): DockerHost { + return { + ...(record as unknown as DockerHost), + id: Number(record.id), + }; +} + +function dockerSettings( + host: object | null | undefined, +): Record { + const all = (host as { pluginSettings?: unknown } | null | undefined) + ?.pluginSettings as + Record | undefined> | undefined; + return all?.docker ?? {}; +} + +export function dockerEnabled(host: object | null | undefined): boolean { + return dockerSettings(host).enableDocker === true; +} + +export function hostTitle(host: DockerHost): string { + return host.name || `${host.username ?? ""}@${host.ip}`; +} diff --git a/plugins/docker/tests/backend/console.test.ts b/plugins/docker/tests/backend/console.test.ts new file mode 100644 index 000000000..cb470aedc --- /dev/null +++ b/plugins/docker/tests/backend/console.test.ts @@ -0,0 +1,170 @@ +import { EventEmitter } from "node:events"; +import express from "express"; +import { afterEach, describe, expect, it } from "vitest"; +import { + createMockCtx, + type MockPluginContext, +} from "@termix/plugin-sdk/testing"; +import type { PluginWebSocketConnection } from "@termix/plugin-sdk/backend"; +import { activate } from "../../src/backend/index.js"; +import { manifest, sshHost } from "./server"; +import { FakeClient, FakeStream } from "./fake-ssh"; + +/** A ws WebSocket as far as the console uses one. */ +class FakeSocket extends EventEmitter { + readyState = 1; + sent: Array> = []; + closedWith: [number, string] | null = null; + + send(data: string) { + this.sent.push(JSON.parse(data)); + } + + ping() {} + + close(code: number, reason: string) { + if (this.readyState !== 1) return; + this.readyState = 3; + this.closedWith = [code, reason]; + this.emit("close"); + } + + message(payload: unknown) { + this.emit("message", Buffer.from(JSON.stringify(payload))); + } +} + +function consoleClient() { + const shells: FakeStream[] = []; + const client = new FakeClient([ + [/which bash/, { stdout: "/bin/bash\n" }], + [/exec -it abc123 \/bin\/bash/, (stream) => shells.push(stream)], + ]); + return { client, shells }; +} + +async function activated(client: FakeClient): Promise { + const mock = createMockCtx({ + pluginId: manifest.id, + manifest, + capabilities: manifest.capabilities, + router: () => express.Router(), + permissions: ["docker.use"], + sshHosts: [sshHost(7)], + sshClient: client, + }); + await mock.ctx.settings.setHost(7, "enableDocker", true); + // Like the runtime, disposing a connection ends its client. + const connect = mock.ctx.ssh.connect; + mock.ctx.ssh.connect = (async (...args: Parameters) => ({ + ...(await connect(...args)), + dispose: () => client.end(), + })) as typeof connect; + await activate(mock.ctx); + return mock; +} + +async function openConsole(mock: MockPluginContext, userId = "user-1") { + const route = mock.wsRoutes.find((r) => r.path === "/console"); + expect(route?.options).toEqual({ public: true, optionalAuth: true }); + const socket = new FakeSocket(); + mock.setActor(userId || undefined); + await route!.handler!({ + userId, + request: {}, + socket, + clientIp: "127.0.0.1", + requestOrigin: "http://localhost", + isDataUnlocked: () => true, + } as PluginWebSocketConnection); + return socket; +} + +async function until(check: () => boolean) { + for (let i = 0; i < 50 && !check(); i++) { + await new Promise((resolve) => setImmediate(resolve)); + } + expect(check()).toBe(true); +} + +let disposals: Array<() => void | Promise> = []; + +afterEach(async () => { + for (const dispose of disposals.reverse()) await dispose(); + disposals = []; +}); + +describe("docker console", () => { + it("refuses a socket without a user", async () => { + const mock = await activated(consoleClient().client); + disposals.push(...mock.disposals); + const socket = await openConsole(mock, ""); + expect(socket.closedWith?.[0]).toBe(1008); + }); + + it("closes an open console on disable, and the next enable works", async () => { + const first = consoleClient(); + const mock = await activated(first.client); + + const socket = await openConsole(mock); + socket.message({ + type: "connect", + data: { + hostConfig: { id: 7, ip: "10.0.0.7" }, + containerId: "abc123", + shell: "bash", + cols: 100, + rows: 30, + }, + }); + await until(() => socket.sent.some((m) => m.type === "connected")); + socket.message({ type: "input", data: "ls\n" }); + await until(() => first.shells[0]?.written.includes("ls\n") ?? false); + + // Disable: the runtime empties the disposable bag. + for (const dispose of [...mock.disposals].reverse()) await dispose(); + expect(first.shells[0].ended).toBe(true); + expect(first.client.ended).toBe(true); + expect(socket.closedWith?.[0]).toBe(1001); + + // Enable again: a fresh activate on the same module serves a new console. + const second = consoleClient(); + const again = await activated(second.client); + disposals.push(...again.disposals); + const next = await openConsole(again); + next.message({ + type: "connect", + data: { hostConfig: { id: 7, ip: "10.0.0.7" }, containerId: "abc123" }, + }); + await until(() => next.sent.some((m) => m.type === "connected")); + second.shells[0].emit("data", Buffer.from("hello")); + await until(() => + next.sent.some((m) => m.type === "output" && m.data === "hello"), + ); + }); + + it("refuses a host id that resolves to another machine", async () => { + const mock = await activated(consoleClient().client); + disposals.push(...mock.disposals); + const socket = await openConsole(mock); + socket.message({ + type: "connect", + data: { hostConfig: { id: 7, ip: "10.9.9.9" }, containerId: "abc123" }, + }); + await until(() => socket.sent.some((m) => m.type === "error")); + expect(String(socket.sent[0].message)).toMatch(/Host mismatch/); + }); + + it("refuses a host with Docker switched off", async () => { + const mock = await activated(consoleClient().client); + disposals.push(...mock.disposals); + await mock.ctx.settings.setHost(7, "enableDocker", false); + const socket = await openConsole(mock); + socket.message({ + type: "connect", + data: { hostConfig: { id: 7, ip: "10.0.0.7" }, containerId: "abc123" }, + }); + await until(() => socket.sent.some((m) => m.type === "error")); + expect(String(socket.sent[0].message)).toMatch(/not enabled/); + }); +}); diff --git a/plugins/docker/tests/backend/fake-ssh.ts b/plugins/docker/tests/backend/fake-ssh.ts new file mode 100644 index 000000000..f4bbc80a6 --- /dev/null +++ b/plugins/docker/tests/backend/fake-ssh.ts @@ -0,0 +1,84 @@ +import { EventEmitter } from "node:events"; + +/** An ssh2 exec channel: stdout on "data", stderr on `stderr`, then "close". */ +export class FakeStream extends EventEmitter { + stderr = new EventEmitter(); + written: string[] = []; + ended = false; + window: [number, number] | null = null; + + write(data: string) { + this.written.push(data); + return true; + } + + end() { + this.ended = true; + } + + setWindow(rows: number, cols: number) { + this.window = [rows, cols]; + } + + /** Emits output and exits, on the next tick like a real channel. */ + finish(stdout: string, code = 0, stderr = "") { + setImmediate(() => { + if (stdout) this.emit("data", Buffer.from(stdout)); + if (stderr) this.stderr.emit("data", Buffer.from(stderr)); + this.emit("close", code); + }); + } +} + +export type ExecReply = + | { stdout?: string; stderr?: string; code?: number } + | ((stream: FakeStream) => void); + +/** + * An ssh2 Client that answers exec by the first matching pattern. A reply + * may be a function for a long-lived channel such as a console. + */ +export class FakeClient extends EventEmitter { + commands: string[] = []; + streams: FakeStream[] = []; + ended = false; + + constructor(private replies: Array<[RegExp, ExecReply]> = []) { + super(); + } + + reply(pattern: RegExp, reply: ExecReply) { + this.replies.unshift([pattern, reply]); + } + + exec( + command: string, + optionsOrCallback: unknown, + maybeCallback?: (err: Error | undefined, stream: FakeStream) => void, + ) { + const callback = ( + typeof optionsOrCallback === "function" + ? optionsOrCallback + : maybeCallback + ) as (err: Error | undefined, stream: FakeStream) => void; + this.commands.push(command); + const stream = new FakeStream(); + this.streams.push(stream); + const match = this.replies.find(([pattern]) => pattern.test(command)); + callback(undefined, stream); + if (!match) { + stream.finish(""); + return this; + } + const reply = match[1]; + if (typeof reply === "function") reply(stream); + else stream.finish(reply.stdout ?? "", reply.code ?? 0, reply.stderr ?? ""); + return this; + } + + end() { + if (this.ended) return; + this.ended = true; + this.emit("close"); + } +} diff --git a/plugins/docker/tests/backend/host-settings.test.ts b/plugins/docker/tests/backend/host-settings.test.ts new file mode 100644 index 000000000..8fc7f4f37 --- /dev/null +++ b/plugins/docker/tests/backend/host-settings.test.ts @@ -0,0 +1,143 @@ +import { afterEach, describe, expect, it } from "vitest"; +import express from "express"; +import { createMockCtx } from "@termix/plugin-sdk/testing"; +import { PluginCapabilityError } from "@termix/plugin-sdk/backend"; +import { activate } from "../../src/backend/index.js"; +import { + normalizeImportedHost, + readDockerHostSettings, +} from "../../src/backend/host-settings.js"; +import { + diffContainerStates, + parseContainerStates, +} from "../../src/backend/container-state.js"; +import { manifest, startServer, type TestServer } from "./server"; + +let server: TestServer | null = null; + +afterEach(async () => { + await server?.close(); + server = null; +}); + +describe("docker activate", () => { + it("fails closed without network:serve", async () => { + const mock = createMockCtx({ + pluginId: manifest.id, + manifest, + capabilities: manifest.capabilities.filter( + (cap) => cap !== "network:serve", + ), + router: () => express.Router(), + }); + await expect(activate(mock.ctx)).rejects.toBeInstanceOf( + PluginCapabilityError, + ); + }); + + it("cannot reach a host without ssh:connect", async () => { + server = await startServer({ + mock: { + capabilities: manifest.capabilities.filter( + (cap) => cap !== "ssh:connect", + ), + }, + }); + const response = await server.request("POST", "/ssh/connect", { + body: { sessionId: "s1", hostId: 7 }, + }); + expect(response.status).toBe(500); + }); + + it("registers its import normalizer", async () => { + server = await startServer(); + expect( + server.mock.ctx.registry.consume("docker.hostImportNormalizer"), + ).toBe(normalizeImportedHost); + }); +}); + +describe("docker host settings", () => { + it("reads the switch and runtime with defaults", async () => { + server = await startServer({ dockerOn: false }); + expect(await readDockerHostSettings(server.mock.ctx, 7)).toEqual({ + enabled: false, + runtime: "docker", + }); + await server.mock.ctx.settings.setHost(7, "enableDocker", true); + await server.mock.ctx.settings.setHost(7, "containerRuntime", "podman"); + expect(await readDockerHostSettings(server.mock.ctx, 7)).toEqual({ + enabled: true, + runtime: "podman", + }); + }); + + it("takes an export's nested settings over the old flat fields", () => { + expect( + normalizeImportedHost({ + enableDocker: false, + pluginSettings: { + docker: { enableDocker: true, containerRuntime: "podman" }, + }, + }), + ).toEqual({ enableDocker: true, containerRuntime: "podman" }); + }); + + it("reads 2.8 flat fields and proxmox-created rows", () => { + expect( + normalizeImportedHost({ + enableDocker: 1, + dockerConfig: '{"runtime":"podman"}', + }), + ).toEqual({ enableDocker: true, containerRuntime: "podman" }); + expect(normalizeImportedHost({ enableDocker: true })).toEqual({ + enableDocker: true, + }); + expect(normalizeImportedHost({ name: "plain" })).toBeNull(); + }); +}); + +/** + * The polling side needs SSH, so what is tested here is the pure part: + * turning `ps` output into states, and two snapshots into events. + */ +describe("container state", () => { + it("reads name, state and health out of the ps output", () => { + const states = parseContainerStates( + [ + '{"name":"web","state":"running","status":"Up 2 hours"}', + "", + "not json at all", + '{"state":"running"}', + '{"name":"api","state":"running","status":"Up 1 hour (unhealthy)"}', + ].join("\n"), + ); + expect([...states.keys()]).toEqual(["web", "api"]); + expect(states.get("api")).toEqual({ state: "running", unhealthy: true }); + }); + + it("reports transitions and skips containers seen for the first time", () => { + const running = { state: "running", unhealthy: false }; + const exited = { state: "exited", unhealthy: false }; + const restarting = { state: "restarting", unhealthy: false }; + expect( + diffContainerStates( + new Map([ + ["web", running], + ["db", exited], + ["cache", running], + ]), + new Map([ + ["web", exited], + ["db", running], + ["cache", restarting], + ["new", running], + ]), + ), + ).toEqual([ + { container: "web", event: "exited" }, + { container: "db", event: "started" }, + { container: "cache", event: "restarting" }, + ]); + }); +}); diff --git a/plugins/docker/tests/backend/routes.test.ts b/plugins/docker/tests/backend/routes.test.ts new file mode 100644 index 000000000..cb0fc6f7b --- /dev/null +++ b/plugins/docker/tests/backend/routes.test.ts @@ -0,0 +1,282 @@ +import { afterEach, describe, expect, it } from "vitest"; +import type { + PluginSshConnectOptions, + PluginSshHost, +} from "@termix/plugin-sdk/backend"; +import { startServer, type TestServer } from "./server"; +import { FakeClient } from "./fake-ssh"; + +let server: TestServer | null = null; + +afterEach(async () => { + await server?.close(); + server = null; +}); + +async function connect(s: TestServer, sessionId = "s1", hostId = 7) { + return s.request("POST", "/ssh/connect", { body: { sessionId, hostId } }); +} + +/** Swaps ctx.ssh.connect for one that runs `flow` against the prompt channel. */ +function promptingConnect( + s: TestServer, + flow: ( + ask: NonNullable["ask"], + ) => Promise, +) { + s.mock.ctx.ssh.connect = (async ( + host: PluginSshHost, + options?: PluginSshConnectOptions, + ) => { + await flow(options!.prompt!.ask); + return { + client: s.client as never, + jumpClient: null, + host, + dispose: () => s.client.end(), + }; + }) as never; +} + +describe("docker routes", () => { + it("refuses every route without docker.use", async () => { + server = await startServer({ permissions: [] }); + const calls: Array<[string, string]> = [ + ["POST", "/ssh/connect"], + ["POST", "/ssh/connect-totp"], + ["POST", "/ssh/connect-warpgate"], + ["POST", "/ssh/disconnect"], + ["POST", "/ssh/keepalive"], + ["GET", "/ssh/status?sessionId=s1"], + ["GET", "/validate/s1"], + ["GET", "/containers/s1"], + ["GET", "/containers/s1/abc"], + ["POST", "/containers/s1/abc/start"], + ["DELETE", "/containers/s1/abc/remove"], + ["GET", "/containers/s1/abc/logs"], + ["GET", "/containers/s1/abc/stats"], + ]; + for (const [method, path] of calls) { + const response = await server.request(method, path, { + body: method === "GET" ? undefined : {}, + }); + expect(response.status, `${method} ${path}`).toBe(403); + } + }); + + it("refuses a host with Docker switched off", async () => { + server = await startServer({ dockerOn: false }); + const response = await connect(server); + expect(response.status).toBe(403); + expect(response.body.code).toBe("DOCKER_DISABLED"); + }); + + it("answers 404 for a host the user cannot reach", async () => { + server = await startServer(); + expect((await connect(server, "s1", 99)).status).toBe(404); + }); + + it("connects, validates and manages containers on the session", async () => { + server = await startServer(); + const connected = await connect(server); + expect(connected.status).toBe(200); + expect(connected.body.success).toBe(true); + expect(server.mock.statusReports).toContainEqual({ hostId: 7, ok: true }); + + const validation = await server.request("GET", "/validate/s1"); + expect(validation.body).toEqual({ + available: true, + version: "27.1.1", + runtime: "docker", + }); + + const list = await server.request("GET", "/containers/s1"); + expect(list.body).toEqual([ + expect.objectContaining({ id: "abc123", name: "web", state: "running" }), + ]); + + server.client.reply(/ start abc123$/, { stdout: "abc123" }); + const started = await server.request("POST", "/containers/s1/abc123/start"); + expect(started.status).toBe(200); + expect( + server.client.commands.some((c) => c.endsWith("docker start abc123")), + ).toBe(true); + + server.client.reply(/ logs abc123/, { stdout: "line one\nline two\n" }); + const logs = await server.request( + "GET", + "/containers/s1/abc123/logs?tail=50&since=2026-01-01T00:00:00Z", + ); + expect(logs.body).toEqual({ success: true, logs: "line one\nline two\n" }); + expect(server.client.commands.at(-1)).toContain( + "logs abc123 --tail 50 --since 2026-01-01T00:00:00Z 2>&1", + ); + + server.client.reply(/ stats abc123/, { + stdout: + '{"cpu":"1.5%","memory":"10MiB / 1GiB","memoryPercent":"1%","netIO":"1kB / 2kB","blockIO":"0B / 0B","pids":"3"}', + }); + const stats = await server.request("GET", "/containers/s1/abc123/stats"); + expect(stats.body).toMatchObject({ + cpu: "1.5%", + memoryUsed: "10MiB", + memoryLimit: "1GiB", + netInput: "1kB", + netOutput: "2kB", + }); + + server.client.reply(/ rm abc123/, { + code: 1, + stderr: "Error: No such container: abc123", + }); + const removed = await server.request( + "DELETE", + "/containers/s1/abc123/remove", + ); + expect(removed.status).toBe(404); + + expect( + (await server.request("POST", "/containers/s1/abc123/explode")).status, + ).toBe(404); + expect( + (await server.request("GET", "/containers/s1/bad;id/logs")).status, + ).toBe(400); + + await server.request("POST", "/ssh/disconnect", { + body: { sessionId: "s1" }, + }); + const status = await server.request("GET", "/ssh/status?sessionId=s1"); + expect(status.body.connected).toBe(false); + }); + + it("uses Podman when the host says so", async () => { + server = await startServer(); + await server.mock.ctx.settings.setHost(7, "containerRuntime", "podman"); + await connect(server); + await server.request("GET", "/containers/s1"); + expect(server.client.commands.at(-1)).toMatch(/ podman ps -a --format/); + }); + + it("keeps another user's session to its owner", async () => { + server = await startServer(); + await connect(server); + const response = await server.request("GET", "/containers/s1", { + user: "user-2", + }); + expect(response.status).toBe(400); + const status = await server.request("GET", "/ssh/status?sessionId=s1", { + user: "user-2", + }); + expect(status.body.connected).toBe(false); + }); + + it("parks a TOTP prompt and re-asks after a wrong code", async () => { + server = await startServer(); + promptingConnect(server, async (ask) => { + let code = await ask({ kind: "totp", prompt: "Code:", retry: false }); + while (code !== "123456") { + if (code === null) throw new Error("Cancelled"); + code = await ask({ kind: "totp", prompt: "Code:", retry: true }); + } + }); + + const first = await connect(server); + expect(first.body).toMatchObject({ requires_totp: true, prompt: "Code:" }); + + const pending = await server.request("GET", "/containers/s1"); + expect(pending.body.code).toBe("AUTH_PENDING"); + + const wrong = await server.request("POST", "/ssh/connect-totp", { + body: { sessionId: "s1", totpCode: "000000" }, + }); + expect(wrong.body).toMatchObject({ requires_totp: true, retry: true }); + + const stranger = await server.request("POST", "/ssh/connect-totp", { + user: "user-2", + body: { sessionId: "s1", totpCode: "123456" }, + }); + expect(stranger.status).toBe(404); + + const right = await server.request("POST", "/ssh/connect-totp", { + body: { sessionId: "s1", totpCode: "123456" }, + }); + expect(right.body).toMatchObject({ success: true, status: "success" }); + expect((await server.request("GET", "/containers/s1")).status).toBe(200); + }); + + it("continues a Warpgate sign-in and refuses a code for it", async () => { + server = await startServer(); + promptingConnect(server, async (ask) => { + await ask({ + kind: "warpgate", + url: "https://warpgate.example/login", + securityKey: "KEY", + instructions: "", + }); + }); + + const first = await connect(server); + expect(first.body).toMatchObject({ + requires_warpgate: true, + url: "https://warpgate.example/login", + securityKey: "KEY", + }); + const wrongKind = await server.request("POST", "/ssh/connect-totp", { + body: { sessionId: "s1", totpCode: "1" }, + }); + expect(wrongKind.status).toBe(400); + + const done = await server.request("POST", "/ssh/connect-warpgate", { + body: { sessionId: "s1" }, + }); + expect(done.body.success).toBe(true); + }); + + it("asks for credentials when a host with no secret gets a password prompt", async () => { + server = await startServer({ + mock: { + sshHosts: [ + { + id: 7, + userId: "user-1", + ip: "10.0.0.7", + port: 22, + username: "root", + authType: "none", + }, + ], + }, + }); + promptingConnect(server, async (ask) => { + const answer = await ask({ + kind: "input", + prompt: "Password:", + echo: false, + isPush: false, + }); + if (answer === null) + throw new Error("All configured authentication methods failed"); + }); + + const response = await connect(server); + expect(response.body).toEqual({ + status: "auth_required", + reason: "no_keyboard", + }); + }); + + it("reports a failed login to core", async () => { + const client = new FakeClient(); + server = await startServer({ client }); + server.mock.ctx.ssh.connect = (async () => { + throw new Error("All configured authentication methods failed"); + }) as never; + const response = await connect(server); + expect(response.status).toBe(500); + expect(server.mock.statusReports).toContainEqual({ + hostId: 7, + ok: false, + hostKeyChanged: false, + }); + }); +}); diff --git a/plugins/docker/tests/backend/server.ts b/plugins/docker/tests/backend/server.ts new file mode 100644 index 000000000..1c9561545 --- /dev/null +++ b/plugins/docker/tests/backend/server.ts @@ -0,0 +1,116 @@ +import http from "node:http"; +import type { AddressInfo } from "node:net"; +import express, { type Router } from "express"; +import { + createMockCtx, + type MockContextOptions, + type MockPluginContext, +} from "@termix/plugin-sdk/testing"; +import type { PluginManifest } from "@termix/plugin-sdk/manifest"; +import manifestJson from "../../manifest.json"; +import { activate } from "../../src/backend/index.js"; +import { FakeClient } from "./fake-ssh"; + +export const manifest = manifestJson as unknown as PluginManifest; + +export const PS_LINE = + '{"id":"abc123","name":"web","image":"nginx","status":"Up 2 hours","state":"running","ports":"80/tcp","created":"2026-01-01"}'; + +/** A host owned by user-1, as ctx.ssh.resolveHost sees it. */ +export function sshHost(id: number, extra: Record = {}) { + return { + id, + userId: "user-1", + name: `host-${id}`, + ip: `10.0.0.${id}`, + port: 22, + username: "root", + authType: "password", + password: "pw", + ...extra, + }; +} + +/** A client that looks like a Linux host with Docker running. */ +export function dockerClient(): FakeClient { + return new FakeClient([ + [/^ver$/, { stdout: "" }], + [/--version/, { stdout: "Docker version 27.1.1, build abc" }], + [/ ps -a --format/, { stdout: `${PS_LINE}\n` }], + [/ ps$/, { stdout: "" }], + ]); +} + +export interface TestServer { + mock: MockPluginContext; + client: FakeClient; + request: ( + method: string, + path: string, + options?: { user?: string; body?: unknown }, + // Route bodies vary per test; asserting on them is the point. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ) => Promise<{ status: number; body: any }>; + close: () => Promise; +} + +/** The plugin activated with its router served the way core mounts it. */ +export async function startServer( + options: { + permissions?: string[]; + dockerOn?: boolean; + client?: FakeClient; + mock?: Partial; + } = {}, +): Promise { + const client = options.client ?? dockerClient(); + let router: Router | null = null; + const mock = createMockCtx({ + pluginId: manifest.id, + manifest, + capabilities: manifest.capabilities, + router: () => (router = express.Router()), + permissions: options.permissions ?? ["docker.use"], + sshHosts: [sshHost(7), sshHost(8)], + sshClient: client, + ...options.mock, + }); + if (options.dockerOn !== false) { + await mock.ctx.settings.setHost(7, "enableDocker", true); + } + + await activate(mock.ctx); + + const app = express(); + app.use(express.json()); + app.use((req, _res, next) => { + mock.setActor(req.header("x-test-user") ?? undefined); + next(); + }); + app.use((req, res, next) => router!(req, res, next)); + + const server = http.createServer(app); + await new Promise((resolve) => server.listen(0, resolve)); + const { port } = server.address() as AddressInfo; + + return { + mock, + client, + async request(method, path, { user = "user-1", body } = {}) { + const response = await fetch(`http://127.0.0.1:${port}${path}`, { + method, + headers: { + "x-test-user": user, + ...(body !== undefined ? { "content-type": "application/json" } : {}), + }, + body: body !== undefined ? JSON.stringify(body) : undefined, + }); + const text = await response.text(); + return { status: response.status, body: text ? JSON.parse(text) : null }; + }, + async close() { + for (const dispose of mock.disposals.reverse()) await dispose(); + await new Promise((resolve) => server.close(() => resolve())); + }, + }; +} diff --git a/plugins/docker/tests/backend/services.test.ts b/plugins/docker/tests/backend/services.test.ts new file mode 100644 index 000000000..7a35203b2 --- /dev/null +++ b/plugins/docker/tests/backend/services.test.ts @@ -0,0 +1,105 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { + DockerEvent, + DockerEventsV1, + DockerServiceV1, +} from "../../src/backend/index.js"; +import { startServer, type TestServer } from "./server"; + +let server: TestServer | null = null; + +afterEach(async () => { + vi.useRealTimers(); + await server?.close(); + server = null; +}); + +const psState = (state: string, status = "Up") => + `{"name":"web","state":"${state}","status":"${status}"}\n`; + +describe("docker.containers service", () => { + it("lists containers and runs actions as the caller", async () => { + server = await startServer(); + const docker = server.mock.services.get( + "docker.containers", + ) as DockerServiceV1; + + const containers = await server.mock.ctx.asUser("user-1", () => + docker.listContainers(7), + ); + expect(containers).toEqual([ + expect.objectContaining({ id: "abc123", name: "web" }), + ]); + + await server.mock.ctx.asUser("user-1", () => + docker.action(7, "web", "restart"), + ); + expect(server.client.commands.at(-1)).toMatch(/ docker restart web$/); + expect(server.mock.sshConnections.at(-1)).toMatchObject({ + host: 7, + pool: "docker", + }); + + await expect( + server.mock.ctx.asUser("user-1", () => + docker.action(7, "web; rm -rf /", "stop"), + ), + ).rejects.toThrow(/Invalid container name/); + }); + + it("needs a calling user", async () => { + server = await startServer(); + const docker = server.mock.services.get( + "docker.containers", + ) as DockerServiceV1; + await expect(docker.listContainers(7)).rejects.toThrow(/calling user/); + }); +}); + +describe("docker.events service", () => { + it("reports state changes between polls to every subscriber", async () => { + vi.useFakeTimers({ toFake: ["Date"] }); + server = await startServer(); + const events = server.mock.services.get("docker.events") as DockerEventsV1; + const seen: DockerEvent[] = []; + const listener = (event: DockerEvent) => seen.push(event); + + const unsubscribe = await server.mock.ctx.asUser("user-1", () => + events.subscribe(7, listener), + ); + // Subscribing the same listener again is a no-op. + await server.mock.ctx.asUser("user-1", () => events.subscribe(7, listener)); + + const poll = async (output: string) => { + server!.client.reply(/ ps -a --format/, { stdout: output }); + vi.setSystemTime(Date.now() + 61_000); + await server!.mock.runScheduled(); + }; + + await poll(psState("running")); + expect(seen).toEqual([]); + + await poll(psState("exited", "Exited (1)")); + expect(seen).toEqual([{ hostId: 7, container: "web", event: "exited" }]); + + await poll(psState("running", "Up 1 second (unhealthy)")); + expect(seen.slice(1)).toEqual([ + { hostId: 7, container: "web", event: "started" }, + { hostId: 7, container: "web", event: "unhealthy" }, + ]); + + unsubscribe(); + const before = server.client.commands.length; + await poll(psState("exited")); + expect(server.client.commands.length).toBe(before); + expect(seen).toHaveLength(3); + }); + + it("stops polling once the plugin is disposed", async () => { + server = await startServer(); + const events = server.mock.services.get("docker.events") as DockerEventsV1; + await server.mock.ctx.asUser("user-1", () => events.subscribe(7, () => {})); + for (const dispose of [...server.mock.disposals].reverse()) await dispose(); + expect(server.mock.scheduled.every((job) => job.stopped)).toBe(true); + }); +}); diff --git a/plugins/docker/tests/frontend/DockerManager.test.tsx b/plugins/docker/tests/frontend/DockerManager.test.tsx new file mode 100644 index 000000000..9818169ec --- /dev/null +++ b/plugins/docker/tests/frontend/DockerManager.test.tsx @@ -0,0 +1,139 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { screen, waitFor } from "@testing-library/react"; +import { + renderWithApp, + type RenderedPluginApp, +} from "@termix/plugin-sdk/testing"; +import type { PluginManifest } from "@termix/plugin-sdk/manifest"; +import type { PluginApiClient } from "@termix/plugin-sdk/frontend"; +import * as plugin from "../../src/frontend/index"; +import manifestJson from "../../manifest.json"; +import locales from "../../locales/en.json"; + +const manifest = manifestJson as unknown as PluginManifest; + +let rendered: RenderedPluginApp | null = null; + +afterEach(async () => { + await rendered?.deactivate(); + rendered = null; +}); + +function host(enableDocker: boolean) { + return { + id: "7", + name: "box", + ip: "10.0.0.7", + port: 22, + username: "root", + enableSsh: true, + pluginSettings: { docker: { enableDocker } }, + }; +} + +function stubApi(connectResult: Record) { + const post = vi.fn(async (url: string) => { + if (url === "/ssh/connect") return { data: connectResult }; + return { data: { success: true } }; + }); + const get = vi.fn(async (url: string) => { + if (url.startsWith("/validate/")) { + return { + data: { available: true, version: "27.1.1", runtime: "docker" }, + }; + } + if (url.startsWith("/containers/")) { + return { + data: [ + { + id: "abc123", + name: "web", + image: "nginx", + status: "Up", + state: "running", + ports: "", + created: "", + }, + ], + }; + } + return { data: {} }; + }); + return { + post, + get, + delete: vi.fn(), + put: vi.fn(), + patch: vi.fn(), + } as unknown as { + post: typeof post; + get: typeof get; + } & PluginApiClient; +} + +describe("DockerManager", () => { + it("says Docker is off for a host without the switch", async () => { + const api = stubApi({ success: true }); + rendered = await renderWithApp(plugin, { + manifest, + locales, + hosts: [host(false)], + api, + }); + rendered.renderTab("docker", { + host: host(false), + label: "box", + isVisible: true, + }); + expect( + await screen.findByText("Docker is not enabled for this host"), + ).toBeTruthy(); + expect(api.post).not.toHaveBeenCalled(); + }); + + it("connects through the plugin api and lists containers", async () => { + const api = stubApi({ success: true, status: "success" }); + rendered = await renderWithApp(plugin, { + manifest, + locales, + hosts: [host(true)], + api, + }); + rendered.renderTab("docker", { + host: host(true), + label: "box", + isVisible: true, + }); + await waitFor(() => + expect(api.post).toHaveBeenCalledWith( + "/ssh/connect", + expect.objectContaining({ hostId: 7 }), + ), + ); + await waitFor(() => + expect(api.get).toHaveBeenCalledWith( + expect.stringMatching(/^\/validate\//), + ), + ); + }); + + it("opens the code prompt when the host asks for one", async () => { + const api = stubApi({ + requires_totp: true, + sessionId: "s1", + prompt: "Verification code:", + }); + rendered = await renderWithApp(plugin, { + manifest, + locales, + hosts: [host(true)], + api, + }); + rendered.renderTab("docker", { + host: host(true), + label: "box", + isVisible: true, + }); + expect(await screen.findByText("Verification code:")).toBeTruthy(); + }); +}); diff --git a/plugins/docker/tests/frontend/activate.test.tsx b/plugins/docker/tests/frontend/activate.test.tsx index 814097280..ce679b717 100644 --- a/plugins/docker/tests/frontend/activate.test.tsx +++ b/plugins/docker/tests/frontend/activate.test.tsx @@ -36,6 +36,11 @@ describe(`${manifest.id} activate`, () => { for (const id of rendered.registered.dashboardCards()) { expect(cards).toContain(id); } + // Host settings render from the manifest; there is no editor tab. + expect(rendered.registered.hostEditorSections()).toEqual([]); + expect(rendered.registered.hostActions()).toEqual([ + expect.objectContaining({ id: "docker", tabType: "docker" }), + ]); }); it("removes everything it registered on deactivate", async () => { diff --git a/plugins/docker/tsconfig.json b/plugins/docker/tsconfig.json index 1d3e42542..7b35314c3 100644 --- a/plugins/docker/tsconfig.json +++ b/plugins/docker/tsconfig.json @@ -1,10 +1,4 @@ { "extends": "@termix/plugin-sdk/tsconfig.plugin.json", - "compilerOptions": { - "paths": { - "@/types/*": ["../../src/types/*"], - "@/*": ["../../src/ui/*"] - } - }, "include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"] } diff --git a/scripts/plugin-boundary-allowlist.json b/scripts/plugin-boundary-allowlist.json index 610d9c4a3..b2e669580 100644 --- a/scripts/plugin-boundary-allowlist.json +++ b/scripts/plugin-boundary-allowlist.json @@ -16,20 +16,6 @@ "plugins/automations/src/frontend/automations/HostSelectorField.tsx", "plugins/automations/src/frontend/automations/StepBlock.tsx", "plugins/automations/src/frontend/automations/TriggerCard.tsx", - "plugins/docker/src/frontend/DockerApp.tsx", - "plugins/docker/src/frontend/DockerManager.tsx", - "plugins/docker/src/frontend/DockerWidget.tsx", - "plugins/docker/src/frontend/DockerWidgetEditForm.tsx", - "plugins/docker/src/frontend/HostDockerTab.tsx", - "plugins/docker/src/frontend/components/ConsoleTerminal.tsx", - "plugins/docker/src/frontend/components/ContainerCard.tsx", - "plugins/docker/src/frontend/components/ContainerDetail.tsx", - "plugins/docker/src/frontend/components/ContainerList.tsx", - "plugins/docker/src/frontend/components/ContainerStats.tsx", - "plugins/docker/src/frontend/components/ContainerTable.tsx", - "plugins/docker/src/frontend/components/LogViewer.tsx", - "plugins/docker/src/frontend/docker-api.ts", - "plugins/docker/src/frontend/index.tsx", "plugins/file-manager/src/frontend/FileManager.tsx", "plugins/file-manager/src/frontend/FileManagerContextMenu.tsx", "plugins/file-manager/src/frontend/FileManagerDialogs.tsx", @@ -107,11 +93,6 @@ "plugins/automations/src/backend/routes.ts", "plugins/automations/src/backend/scheduler.ts", "plugins/automations/src/backend/triggers.ts", - "plugins/docker/src/backend/console.ts", - "plugins/docker/src/backend/container-routes.ts", - "plugins/docker/src/backend/index.ts", - "plugins/docker/src/backend/routes.ts", - "plugins/docker/src/backend/session-manager.ts", "plugins/file-manager/src/backend/index.ts", "plugins/file-manager/src/backend/operation-routes.ts", "plugins/proxmox/src/backend/routes.ts", diff --git a/scripts/shell-plugin-id-allowlist.json b/scripts/shell-plugin-id-allowlist.json index 35c9e6613..e75268a99 100644 --- a/scripts/shell-plugin-id-allowlist.json +++ b/scripts/shell-plugin-id-allowlist.json @@ -1,14 +1,6 @@ { "$comment": "Literals src/ui may still spell that name a plugin id or a plugin-owned view. Each is data, not knowledge of a plugin. Checked by scripts/check-shell-plugin-ids.cjs; D1 and Phase B empty it.", "files": { - "src/ui/sidebar/HostEditorData.ts": { - "literals": ["docker"], - "reason": "Host data: the enableDocker column filter and the container runtime name, until Phase B moves them." - }, - "src/ui/sidebar/HostsPanel.tsx": { - "literals": ["docker"], - "reason": "Host data: the enableDocker column filter and the container runtime name, until Phase B moves them." - }, "src/ui/sidebar/QuickConnectPanel.tsx": { "literals": ["files"], "reason": "The quick-connect-for-an-unsaved-host flow offers a fixed Files button next to the default connect button, not an extensible list a plugin contributes to." diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index f5c76a9cc..d1e43c485 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -798,7 +798,6 @@ app.post("/database/export", authenticateJWT, async (req, res) => { tunnel_connections TEXT, jump_hosts TEXT, enable_file_manager INTEGER NOT NULL DEFAULT 1, - enable_docker INTEGER NOT NULL DEFAULT 0, enable_web_ui INTEGER NOT NULL DEFAULT 0, show_terminal_in_sidebar INTEGER NOT NULL DEFAULT 1, show_file_manager_in_sidebar INTEGER NOT NULL DEFAULT 0, @@ -808,7 +807,6 @@ app.post("/database/export", authenticateJWT, async (req, res) => { default_path TEXT, status_check_enabled INTEGER NOT NULL DEFAULT 1, status_check_interval INTEGER, - docker_config TEXT, web_ui_config TEXT, terminal_config TEXT, quick_actions TEXT, @@ -929,8 +927,8 @@ app.post("/database/export", authenticateJWT, async (req, res) => { const sshHosts = await createCurrentHostRepository().listDecryptedByUserId(userId); const insertHost = exportDb.prepare(` - INSERT INTO ssh_data (id, user_id, connection_type, name, ip, port, username, folder, tags, pin, auth_type, force_keyboard_interactive, password, key, key_password, key_type, sudo_password, autostart_password, autostart_key, autostart_key_password, credential_id, override_credential_username, enable_terminal, enable_tunnel, tunnel_connections, jump_hosts, enable_file_manager, enable_docker, enable_web_ui, show_terminal_in_sidebar, show_file_manager_in_sidebar, show_tunnel_in_sidebar, show_docker_in_sidebar, show_server_stats_in_sidebar, default_path, status_check_enabled, status_check_interval, docker_config, web_ui_config, terminal_config, quick_actions, notes, use_socks5, socks5_host, socks5_port, socks5_username, socks5_password, socks5_proxy_chain, domain, mac_address, port_knock_sequence, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO ssh_data (id, user_id, connection_type, name, ip, port, username, folder, tags, pin, auth_type, force_keyboard_interactive, password, key, key_password, key_type, sudo_password, autostart_password, autostart_key, autostart_key_password, credential_id, override_credential_username, enable_terminal, enable_tunnel, tunnel_connections, jump_hosts, enable_file_manager, enable_web_ui, show_terminal_in_sidebar, show_file_manager_in_sidebar, show_tunnel_in_sidebar, show_docker_in_sidebar, show_server_stats_in_sidebar, default_path, status_check_enabled, status_check_interval, web_ui_config, terminal_config, quick_actions, notes, use_socks5, socks5_host, socks5_port, socks5_username, socks5_password, socks5_proxy_chain, domain, mac_address, port_knock_sequence, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); for (const decrypted of sshHosts) { @@ -962,7 +960,6 @@ app.post("/database/export", authenticateJWT, async (req, res) => { decrypted.tunnelConnections || null, decrypted.jumpHosts || null, decrypted.enableFileManager ? 1 : 0, - decrypted.enableDocker ? 1 : 0, decrypted.enableWebUi ? 1 : 0, decrypted.showTerminalInSidebar ? 1 : 0, decrypted.showFileManagerInSidebar ? 1 : 0, @@ -972,7 +969,6 @@ app.post("/database/export", authenticateJWT, async (req, res) => { decrypted.defaultPath || null, decrypted.statusCheckEnabled === false ? 0 : 1, decrypted.statusCheckInterval ?? null, - decrypted.dockerConfig || null, decrypted.webUiConfig || null, decrypted.terminalConfig || null, decrypted.quickActions || null, @@ -1369,7 +1365,6 @@ app.post( tunnelConnections: host.tunnel_connections, jumpHosts: host.jump_hosts, enableFileManager: Boolean(host.enable_file_manager), - enableDocker: Boolean(host.enable_docker), showTerminalInSidebar: Boolean(host.show_terminal_in_sidebar), showFileManagerInSidebar: Boolean( host.show_file_manager_in_sidebar, diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts index 654d5b68e..34fe732a8 100644 --- a/src/backend/database/db/index.ts +++ b/src/backend/database/db/index.ts @@ -327,7 +327,6 @@ async function initializeCompleteDatabase(): Promise { enable_tunnel INTEGER NOT NULL DEFAULT 1, tunnel_connections TEXT, enable_file_manager INTEGER NOT NULL DEFAULT 1, - enable_docker INTEGER NOT NULL DEFAULT 0, enable_web_ui INTEGER NOT NULL DEFAULT 0, default_path TEXT, autostart_password TEXT, @@ -336,7 +335,6 @@ async function initializeCompleteDatabase(): Promise { force_keyboard_interactive TEXT, status_check_enabled INTEGER NOT NULL DEFAULT 1, status_check_interval INTEGER, - docker_config TEXT, web_ui_config TEXT, terminal_config TEXT, notes TEXT, @@ -1075,12 +1073,6 @@ const migrateSchema = () => { addColumnIfNotExists("ssh_data", "status_check_interval", "INTEGER"); addColumnIfNotExists("ssh_data", "terminal_config", "TEXT"); addColumnIfNotExists("ssh_data", "quick_actions", "TEXT"); - addColumnIfNotExists( - "ssh_data", - "enable_docker", - "INTEGER NOT NULL DEFAULT 0", - ); - addColumnIfNotExists("ssh_data", "docker_config", "TEXT"); addColumnIfNotExists( "ssh_data", "enable_web_ui", diff --git a/src/backend/database/db/schema.mysql.ts b/src/backend/database/db/schema.mysql.ts index 00c114173..fe3535b0b 100644 --- a/src/backend/database/db/schema.mysql.ts +++ b/src/backend/database/db/schema.mysql.ts @@ -264,9 +264,6 @@ export const hosts = mysqlTable( .notNull() .default(true), scpLegacy: boolean("scp_legacy").notNull().default(false), - enableDocker: boolean("enable_docker") - .notNull() - .default(false), enableWebUi: boolean("enable_web_ui") .notNull() .default(false), @@ -299,7 +296,6 @@ export const hosts = mysqlTable( .notNull() .default(true), statusCheckInterval: int("status_check_interval"), - dockerConfig: text("docker_config"), webUiConfig: text("web_ui_config"), terminalConfig: text("terminal_config"), quickActions: text("quick_actions"), diff --git a/src/backend/database/db/schema.pg.ts b/src/backend/database/db/schema.pg.ts index 6547794da..f0e08bcec 100644 --- a/src/backend/database/db/schema.pg.ts +++ b/src/backend/database/db/schema.pg.ts @@ -265,9 +265,6 @@ export const hosts = pgTable( .notNull() .default(true), scpLegacy: boolean("scp_legacy").notNull().default(false), - enableDocker: boolean("enable_docker") - .notNull() - .default(false), enableWebUi: boolean("enable_web_ui") .notNull() .default(false), @@ -300,7 +297,6 @@ export const hosts = pgTable( .notNull() .default(true), statusCheckInterval: integer("status_check_interval"), - dockerConfig: text("docker_config"), webUiConfig: text("web_ui_config"), terminalConfig: text("terminal_config"), quickActions: text("quick_actions"), diff --git a/src/backend/database/db/schema.ts b/src/backend/database/db/schema.ts index 1ee79044a..58ceb5111 100644 --- a/src/backend/database/db/schema.ts +++ b/src/backend/database/db/schema.ts @@ -257,9 +257,6 @@ export const hosts = sqliteTable( .notNull() .default(true), scpLegacy: integer("scp_legacy", { mode: "boolean" }).notNull().default(false), - enableDocker: integer("enable_docker", { mode: "boolean" }) - .notNull() - .default(false), enableWebUi: integer("enable_web_ui", { mode: "boolean" }) .notNull() .default(false), @@ -292,7 +289,6 @@ export const hosts = sqliteTable( .notNull() .default(true), statusCheckInterval: integer("status_check_interval"), - dockerConfig: text("docker_config"), webUiConfig: text("web_ui_config"), terminalConfig: text("terminal_config"), quickActions: text("quick_actions"), diff --git a/src/backend/database/routes/host-bulk-routes.ts b/src/backend/database/routes/host-bulk-routes.ts index c981eaa10..ef7157ba3 100644 --- a/src/backend/database/routes/host-bulk-routes.ts +++ b/src/backend/database/routes/host-bulk-routes.ts @@ -284,8 +284,6 @@ export function registerHostBulkRoutes( simpleUpdates.enableTunnel = updates.enableTunnel; if (typeof updates.enableFileManager === "boolean") simpleUpdates.enableFileManager = updates.enableFileManager; - if (typeof updates.enableDocker === "boolean") - simpleUpdates.enableDocker = updates.enableDocker; if (typeof updates.enableTmuxMonitor === "boolean") simpleUpdates.enableTmuxMonitor = updates.enableTmuxMonitor; if (typeof updates.enableTerminalToolbar === "boolean") @@ -751,7 +749,6 @@ export function registerHostBulkRoutes( enableTerminal: hostData.enableTerminal !== false, enableTunnel: hostData.enableTunnel !== false, enableFileManager: hostData.enableFileManager !== false, - enableDocker: hostData.enableDocker || false, enableTmuxMonitor: hostData.enableTmuxMonitor || false, enableTerminalToolbar: hostData.enableTerminalToolbar !== false, enableAiAssistant: hostData.enableAiAssistant || false, @@ -771,9 +768,6 @@ export function registerHostBulkRoutes( ? JSON.stringify(hostData.quickActions) : null, ...importedStatusCheck(hostData as Record), - dockerConfig: hostData.dockerConfig - ? JSON.stringify(hostData.dockerConfig) - : null, terminalConfig: hostData.terminalConfig ? JSON.stringify(hostData.terminalConfig) : null, @@ -1016,7 +1010,6 @@ export function registerHostBulkRoutes( enableTerminal: true, enableTunnel: true, enableFileManager: true, - enableDocker: false, enableTmuxMonitor: false, enableTerminalToolbar: true, enableAiAssistant: false, @@ -1034,7 +1027,6 @@ export function registerHostBulkRoutes( quickActions: null, statusCheckEnabled: true, statusCheckInterval: null, - dockerConfig: null, terminalConfig: null, forceKeyboardInteractive: "false", notes: null, diff --git a/src/backend/database/routes/host-normalizers.ts b/src/backend/database/routes/host-normalizers.ts index f379b073c..6eb14edbd 100644 --- a/src/backend/database/routes/host-normalizers.ts +++ b/src/backend/database/routes/host-normalizers.ts @@ -174,7 +174,6 @@ export type NormalizedImportedHost = Record & { enableTerminal?: unknown; enableTunnel?: unknown; enableFileManager?: unknown; - enableDocker?: unknown; enableWebUi?: unknown; enableProxmox?: unknown; enableTmuxMonitor?: unknown; @@ -193,7 +192,6 @@ export type NormalizedImportedHost = Record & { quickActions?: unknown; statusCheckEnabled?: unknown; statusCheckInterval?: unknown; - dockerConfig?: unknown; webUiConfig?: unknown; proxmoxConfig?: unknown; enableProxmoxStats?: unknown; @@ -340,7 +338,6 @@ const CONNECT_LEVEL_FIELDS = new Set([ "enableTerminal", "enableTunnel", "enableFileManager", - "enableDocker", "enableTmuxMonitor", "enableTerminalToolbar", "enableAiAssistant", @@ -437,7 +434,6 @@ export function transformHostResponse( enableTerminal: !!host.enableTerminal, enableTunnel: !!host.enableTunnel, enableFileManager: host.enableFileManager !== false, - enableDocker: !!host.enableDocker, enableTmuxMonitor: !!host.enableTmuxMonitor, enableTerminalToolbar: host.enableTerminalToolbar !== false, enableAiAssistant: !!host.enableAiAssistant, @@ -468,9 +464,6 @@ export function transformHostResponse( terminalConfig: host.terminalConfig ? JSON.parse(host.terminalConfig as string) : undefined, - dockerConfig: host.dockerConfig - ? JSON.parse(host.dockerConfig as string) - : undefined, forceKeyboardInteractive: host.forceKeyboardInteractive === "true", useWarpgate: !!host.useWarpgate, socks5ProxyChain: host.socks5ProxyChain diff --git a/src/backend/database/routes/host.ts b/src/backend/database/routes/host.ts index f550138d3..de9281643 100644 --- a/src/backend/database/routes/host.ts +++ b/src/backend/database/routes/host.ts @@ -199,7 +199,6 @@ router.post( enableTunnel, enableFileManager, scpLegacy, - enableDocker, enableProxmox, enableTmuxMonitor, enableTerminalToolbar, @@ -215,7 +214,6 @@ router.post( quickActions, statusCheckEnabled, statusCheckInterval, - dockerConfig, proxmoxConfig, enableProxmoxStats, proxmoxStatsConfig, @@ -332,7 +330,6 @@ router.post( : null, enableFileManager: enableFileManager ? 1 : 0, scpLegacy: scpLegacy ? 1 : 0, - enableDocker: enableDocker ? 1 : 0, enableTmuxMonitor: enableTmuxMonitor ? 1 : 0, enableTerminalToolbar: enableTerminalToolbar === false ? 0 : 1, enableAiAssistant: enableAiAssistant ? 1 : 0, @@ -344,11 +341,6 @@ router.post( defaultPath: defaultPath || null, statusCheckEnabled: statusCheckEnabled === false ? 0 : 1, statusCheckInterval: normalizeStatusInterval(statusCheckInterval), - dockerConfig: dockerConfig - ? typeof dockerConfig === "string" - ? dockerConfig - : JSON.stringify(dockerConfig) - : null, terminalConfig: terminalConfig ? typeof terminalConfig === "string" ? terminalConfig @@ -764,7 +756,6 @@ router.post( enableTerminal: true, enableTunnel: false, enableFileManager: true, - enableDocker: false, enableWebUi: false, enableTmuxMonitor: false, enableTerminalToolbar: true, @@ -895,7 +886,6 @@ router.put( enableTunnel, enableFileManager, scpLegacy, - enableDocker, enableProxmox, enableTmuxMonitor, enableTerminalToolbar, @@ -911,7 +901,6 @@ router.put( quickActions, statusCheckEnabled, statusCheckInterval, - dockerConfig, proxmoxConfig, enableProxmoxStats, proxmoxStatsConfig, @@ -1029,7 +1018,6 @@ router.put( : null, enableFileManager: enableFileManager ? 1 : 0, scpLegacy: scpLegacy ? 1 : 0, - enableDocker: enableDocker ? 1 : 0, enableTmuxMonitor: enableTmuxMonitor ? 1 : 0, enableTerminalToolbar: enableTerminalToolbar === false ? 0 : 1, enableAiAssistant: enableAiAssistant ? 1 : 0, @@ -1041,11 +1029,6 @@ router.put( defaultPath: defaultPath || null, statusCheckEnabled: statusCheckEnabled === false ? 0 : 1, statusCheckInterval: normalizeStatusInterval(statusCheckInterval), - dockerConfig: dockerConfig - ? typeof dockerConfig === "string" - ? dockerConfig - : JSON.stringify(dockerConfig) - : null, terminalConfig: terminalConfig ? typeof terminalConfig === "string" ? terminalConfig @@ -2099,7 +2082,6 @@ router.get( enableTunnel: !!resolvedHost.enableTunnel, enableFileManager: resolvedHost.enableFileManager !== false, scpLegacy: !!resolvedHost.scpLegacy, - enableDocker: !!resolvedHost.enableDocker, enableWebUi: !!webEndpointSettings?.enableWebUi, enableProxmox: !!proxmoxSettings?.enableProxmox, enableProxmoxStats: !!proxmoxSettings?.enableProxmoxStats, @@ -2123,9 +2105,6 @@ router.get( ? JSON.parse(resolvedHost.quickActions as string) : null, webUiConfig: webEndpointSettings?.webUiConfig ?? { endpoints: [] }, - dockerConfig: resolvedHost.dockerConfig - ? JSON.parse(resolvedHost.dockerConfig as string) - : null, proxmoxConfig: proxmoxSettings?.proxmoxConfig ?? null, proxmoxStatsConfig: proxmoxSettings?.proxmoxStatsConfig ?? null, terminalConfig: resolvedHost.terminalConfig @@ -2260,7 +2239,6 @@ router.get( enableCommandHistory: resolvedHost.enableCommandHistory !== false, enableTunnel: !!resolvedHost.enableTunnel, enableFileManager: resolvedHost.enableFileManager !== false, - enableDocker: !!resolvedHost.enableDocker, enableWebUi: !!webEndpointSettings?.enableWebUi, enableProxmox: !!proxmoxSettings?.enableProxmox, enableTmuxMonitor: !!resolvedHost.enableTmuxMonitor, @@ -2288,9 +2266,6 @@ router.get( webUiConfig: webEndpointSettings?.webUiConfig ?? { endpoints: [], }, - dockerConfig: resolvedHost.dockerConfig - ? JSON.parse(resolvedHost.dockerConfig as string) - : null, proxmoxConfig: proxmoxSettings?.proxmoxConfig ?? null, terminalConfig: resolvedHost.terminalConfig ? JSON.parse(resolvedHost.terminalConfig as string) diff --git a/src/backend/plugins/ctx-hosts.ts b/src/backend/plugins/ctx-hosts.ts index c229672f8..597cb226b 100644 --- a/src/backend/plugins/ctx-hosts.ts +++ b/src/backend/plugins/ctx-hosts.ts @@ -85,7 +85,6 @@ function toRecord(host: Record): PluginHostRecord { enableTerminal: (host.enableTerminal as boolean | null) ?? null, enableFileManager: (host.enableFileManager as boolean | null) ?? null, enableTunnel: (host.enableTunnel as boolean | null) ?? null, - enableDocker: (host.enableDocker as boolean | null) ?? null, createdAt: (host.createdAt as string | null) ?? null, updatedAt: (host.updatedAt as string | null) ?? null, ...host, diff --git a/src/backend/starter.ts b/src/backend/starter.ts index 9dc96dfa3..05ef99a9a 100644 --- a/src/backend/starter.ts +++ b/src/backend/starter.ts @@ -393,6 +393,10 @@ async function provisionLocalDesktopUserIfNeeded(): Promise { const { runHostMetricsSettingsMigration } = await import("./utils/crypto-migration/host-metrics-settings-migration.js"); await runHostMetricsSettingsMigration(); + + const { runDockerSettingsMigration } = + await import("./utils/crypto-migration/docker-settings-migration.js"); + await runDockerSettingsMigration(); } catch (error) { systemLogger.warn("Plugin runtime failed to initialize", { operation: "plugin_init", diff --git a/src/backend/tests/database/repositories/host-repository.test.ts b/src/backend/tests/database/repositories/host-repository.test.ts index 828deda7c..c9db68fc7 100644 --- a/src/backend/tests/database/repositories/host-repository.test.ts +++ b/src/backend/tests/database/repositories/host-repository.test.ts @@ -129,7 +129,6 @@ describe("HostRepository Proxmox sync inserts", () => { enableTerminal: true, enableFileManager: true, enableTunnel: true, - enableDocker: false, enableSsh: true, enableRdp: false, rdpUser: null, @@ -148,7 +147,6 @@ describe("HostRepository Proxmox sync inserts", () => { tunnelConnections: "[]", jumpHosts: null, quickActions: null, - dockerConfig: null, terminalConfig: null, forceKeyboardInteractive: "false", useSocks5: 0, diff --git a/src/backend/tests/utils/docker-settings-migration.test.ts b/src/backend/tests/utils/docker-settings-migration.test.ts new file mode 100644 index 000000000..7716199ec --- /dev/null +++ b/src/backend/tests/utils/docker-settings-migration.test.ts @@ -0,0 +1,197 @@ +/** + * The Docker host settings migration. + * + * An upgrade must be lossless: every host with Docker on, and every host on + * Podman, must come through into the docker plugin's host settings, whatever + * shape the engine returned the flag in. Running it twice must not duplicate + * or clobber what it moved. + */ + +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const state = vi.hoisted(() => ({ + pluginRows: [] as Array<{ + pluginId: string; + scope: string; + scopeId: string | null; + key: string; + value: string | null; + }>, + hostRows: [] as Array>, + pluginInstalled: true, + columnsGone: false, +})); + +vi.mock("../../database/repositories/factory.js", () => ({ + createCurrentPluginRepository: () => ({ + findById: async (id: string) => + state.pluginInstalled && id === "docker" ? { id } : null, + }), + createCurrentPluginSettingsRepository: () => ({ + getAll: async (_pluginId: string, scope: string, scopeId: string | null) => + state.pluginRows.filter( + (row) => row.scope === scope && row.scopeId === scopeId, + ), + set: async ( + pluginId: string, + scope: string, + scopeId: string | null, + key: string, + value: string | null, + ) => { + const existing = state.pluginRows.find( + (row) => + row.scope === scope && row.scopeId === scopeId && row.key === key, + ); + if (existing) { + existing.value = value; + return; + } + state.pluginRows.push({ pluginId, scope, scopeId, key, value }); + }, + }), +})); + +vi.mock("../../utils/crypto-migration/raw-rows.js", () => ({ + selectRows: async () => { + if (state.columnsGone) throw new Error("no such column: enable_docker"); + return state.hostRows; + }, +})); + +vi.mock("../../utils/logger.js", () => ({ + databaseLogger: { info: vi.fn(), warn: vi.fn() }, +})); + +import { + dockerSettingsFromRow, + runDockerSettingsMigration, +} from "../../utils/crypto-migration/docker-settings-migration.js"; + +const hostValues = (hostId: number) => + Object.fromEntries( + state.pluginRows + .filter((row) => row.scope === "host" && row.scopeId === String(hostId)) + .map((row) => [row.key, JSON.parse(row.value ?? "null")]), + ); + +beforeEach(() => { + state.pluginRows = []; + state.hostRows = []; + state.pluginInstalled = true; + state.columnsGone = false; +}); + +describe("dockerSettingsFromRow", () => { + it("reads SQLite integers, real booleans and strings", () => { + expect( + dockerSettingsFromRow({ id: 1, enable_docker: 1, docker_config: null }), + ).toEqual({ enableDocker: true }); + expect( + dockerSettingsFromRow({ + id: 1, + enable_docker: true, + docker_config: null, + }), + ).toEqual({ enableDocker: true }); + expect( + dockerSettingsFromRow({ id: 1, enable_docker: 0, docker_config: null }), + ).toEqual({}); + expect( + dockerSettingsFromRow({ + id: 1, + enable_docker: false, + docker_config: null, + }), + ).toEqual({}); + }); + + it("keeps a Podman runtime and ignores the default or broken config", () => { + expect( + dockerSettingsFromRow({ + id: 1, + enable_docker: 1, + docker_config: '{"runtime":"podman"}', + }), + ).toEqual({ enableDocker: true, containerRuntime: "podman" }); + expect( + dockerSettingsFromRow({ + id: 1, + enable_docker: 1, + docker_config: '{"runtime":"docker"}', + }), + ).toEqual({ enableDocker: true }); + expect( + dockerSettingsFromRow({ + id: 1, + enable_docker: 1, + docker_config: "{oops", + }), + ).toEqual({ enableDocker: true }); + }); +}); + +describe("runDockerSettingsMigration", () => { + it("moves every host's docker options and skips hosts with nothing set", async () => { + state.hostRows = [ + { id: 1, enable_docker: 1, docker_config: null }, + { id: 2, enable_docker: 1, docker_config: '{"runtime":"podman"}' }, + { id: 3, enable_docker: 0, docker_config: null }, + ]; + + const result = await runDockerSettingsMigration(); + + expect(result).toEqual({ hostsMoved: 2, hostsSkipped: 0 }); + expect(hostValues(1)).toEqual({ enableDocker: true }); + expect(hostValues(2)).toEqual({ + enableDocker: true, + containerRuntime: "podman", + }); + expect(hostValues(3)).toEqual({}); + }); + + it("changes nothing when run twice", async () => { + state.hostRows = [ + { id: 1, enable_docker: 1, docker_config: '{"runtime":"podman"}' }, + ]; + await runDockerSettingsMigration(); + const before = JSON.stringify(state.pluginRows); + + const second = await runDockerSettingsMigration(); + + expect(second).toEqual({ hostsMoved: 0, hostsSkipped: 1 }); + expect(JSON.stringify(state.pluginRows)).toBe(before); + }); + + it("keeps a value already saved in the plugin", async () => { + state.pluginRows.push({ + pluginId: "docker", + scope: "host", + scopeId: "1", + key: "enableDocker", + value: "false", + }); + state.hostRows = [{ id: 1, enable_docker: 1, docker_config: null }]; + + await runDockerSettingsMigration(); + + expect(hostValues(1)).toEqual({ enableDocker: false }); + }); + + it("does nothing before the plugin row exists or once the columns are gone", async () => { + state.hostRows = [{ id: 1, enable_docker: 1, docker_config: null }]; + state.pluginInstalled = false; + expect(await runDockerSettingsMigration()).toEqual({ + hostsMoved: 0, + hostsSkipped: 0, + }); + + state.pluginInstalled = true; + state.columnsGone = true; + expect(await runDockerSettingsMigration()).toEqual({ + hostsMoved: 0, + hostsSkipped: 0, + }); + expect(state.pluginRows).toEqual([]); + }); +}); diff --git a/src/backend/utils/crypto-migration/docker-settings-migration.ts b/src/backend/utils/crypto-migration/docker-settings-migration.ts new file mode 100644 index 000000000..2522877ef --- /dev/null +++ b/src/backend/utils/crypto-migration/docker-settings-migration.ts @@ -0,0 +1,118 @@ +/** + * Moves each host's Docker options out of ssh_data and into the docker + * plugin's host settings: enable_docker becomes enableDocker and the runtime + * in docker_config becomes containerRuntime. + * + * The columns stay in the database, unused: core's drizzle migrations run + * before this does, so dropping them there would lose the data first. + * + * Idempotent: a host that already has any docker row is skipped, so this is + * safe on every boot. + */ + +import { sql } from "drizzle-orm"; +import { databaseLogger } from "../logger.js"; +import { + createCurrentPluginRepository, + createCurrentPluginSettingsRepository, +} from "../../database/repositories/factory.js"; +import { selectRows } from "./raw-rows.js"; + +const PLUGIN_ID = "docker"; + +interface LegacyHostRow { + id: number; + enable_docker: number | boolean | string | null; + docker_config: string | null; +} + +/** The plugin's host settings for one legacy row, defaults left out. */ +export function dockerSettingsFromRow( + row: LegacyHostRow, +): Record { + const values: Record = {}; + const flag = row.enable_docker; + if (flag === true || flag === 1 || flag === "1" || flag === "true") { + values.enableDocker = true; + } + if (row.docker_config) { + try { + const config = JSON.parse(row.docker_config) as { runtime?: unknown }; + if (config?.runtime === "podman") values.containerRuntime = "podman"; + } catch { + // unreadable config means the default runtime + } + } + return values; +} + +export interface DockerSettingsMigrationResult { + hostsMoved: number; + hostsSkipped: number; +} + +export async function runDockerSettingsMigration(): Promise { + const result: DockerSettingsMigrationResult = { + hostsMoved: 0, + hostsSkipped: 0, + }; + + // plugin_settings has a foreign key to plugins: nothing to migrate into + // until the plugin row exists. + try { + const plugin = await createCurrentPluginRepository().findById(PLUGIN_ID); + if (!plugin) return result; + } catch { + return result; + } + + const pluginSettings = createCurrentPluginSettingsRepository(); + + let rows: LegacyHostRow[]; + try { + // Raw SQL: schema.ts no longer declares these columns. + rows = await selectRows( + sql`SELECT id, enable_docker, docker_config FROM ssh_data`, + ); + } catch { + // A fresh install never had the columns. + return result; + } + + for (const row of rows) { + const values = dockerSettingsFromRow(row); + if (Object.keys(values).length === 0) continue; + const scopeId = String(row.id); + try { + const existing = await pluginSettings.getAll(PLUGIN_ID, "host", scopeId); + if (existing.length > 0) { + result.hostsSkipped++; + continue; + } + for (const [key, value] of Object.entries(values)) { + await pluginSettings.set( + PLUGIN_ID, + "host", + scopeId, + key, + JSON.stringify(value), + ); + } + result.hostsMoved++; + } catch (error) { + databaseLogger.warn("Docker host settings migration failed for a host", { + operation: "docker_settings_migration", + hostId: row.id, + error: error instanceof Error ? error.message : String(error), + }); + } + } + + if (result.hostsMoved > 0) { + databaseLogger.info( + `Moved Docker settings for ${result.hostsMoved} host(s) into plugin settings`, + { operation: "docker_settings_migration" }, + ); + } + return result; +} diff --git a/src/types/homepage-types.ts b/src/types/homepage-types.ts index 32ca2232e..bb53190fa 100644 --- a/src/types/homepage-types.ts +++ b/src/types/homepage-types.ts @@ -22,7 +22,6 @@ export type WidgetTypeId = | "ssh_terminal" | "quick_connect" | "file_manager_widget" - | "docker_widget" | "tunnel_widget" | "calendar" | "countdown" @@ -35,7 +34,6 @@ export type WidgetTypeId = | "dashboard_links" | "search_links" | "link_tree" - | "docker_activity" | "ssh_quick_connect" | (string & {}); @@ -213,10 +211,6 @@ export interface FileManagerWidgetConfig { hostId: number; } -export interface DockerWidgetConfig { - hostId: number; -} - export interface SshTerminalConfig { hostId: number; autoConnect: boolean; diff --git a/src/types/index.ts b/src/types/index.ts index 4d4d0125c..a0d663d92 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -157,7 +157,6 @@ export interface HostFeatureFlags { enableTerminal: boolean; // SSH, Telnet only enableTunnel: boolean; // SSH only enableFileManager: boolean; // SSH only - enableDocker: boolean; // SSH only enableTmuxMonitor: boolean; // SSH only enableTerminalToolbar: boolean; // SSH, RDP, VNC, and Telnet enableAiAssistant: boolean; // SSH only @@ -216,7 +215,6 @@ export type Host = { enableTunnel: boolean; enableFileManager: boolean; scpLegacy?: boolean; - enableDocker: boolean; enableTmuxMonitor: boolean; enableTerminalToolbar: boolean; enableAiAssistant: boolean; @@ -253,7 +251,6 @@ export type Host = { /** "ssh", or the id of the plugin protocol a host without SSH uses. */ connectionType?: string; domain?: string; - dockerConfig?: Record | null; enableWebUi?: boolean; webUiConfig?: WebUiConfig | null; @@ -368,7 +365,6 @@ export interface HostData { enableTunnel?: boolean; enableFileManager?: boolean; scpLegacy?: boolean; - enableDocker?: boolean; enableProxmox?: boolean; enableTmuxMonitor?: boolean; enableTerminalToolbar?: boolean; @@ -410,7 +406,6 @@ export interface HostData { /** "ssh", or the id of the plugin protocol a host without SSH uses. */ connectionType?: string; domain?: string; - dockerConfig?: Record | null; enableWebUi?: boolean; webUiConfig?: WebUiConfig | null; @@ -1005,52 +1000,3 @@ export interface RestoreRequestBody { // ============================================================================ // DOCKER TYPES // ============================================================================ - -export interface DockerContainer { - id: string; - name: string; - image: string; - status: string; - state: - | "created" - | "running" - | "paused" - | "restarting" - | "removing" - | "exited" - | "dead"; - ports: string; - created: string; - command?: string; - labels?: Record; - networks?: string[]; - mounts?: string[]; -} - -export interface DockerStats { - cpu: string; - memoryUsed: string; - memoryLimit: string; - memoryPercent: string; - netInput: string; - netOutput: string; - blockRead: string; - blockWrite: string; - pids?: string; -} - -export interface DockerLogOptions { - tail?: number; - timestamps?: boolean; - since?: string; - until?: string; - follow?: boolean; -} - -export interface DockerValidation { - available: boolean; - version?: string; - runtime?: "docker" | "podman"; - error?: string; - code?: string; -} diff --git a/src/types/ui-types.ts b/src/types/ui-types.ts index b7b96dc3c..2cb148d10 100644 --- a/src/types/ui-types.ts +++ b/src/types/ui-types.ts @@ -100,10 +100,6 @@ export type Host = { scpLegacy?: boolean; defaultPath?: string; - enableDocker: boolean; - dockerConfig?: { - runtime?: "docker" | "podman"; - } | null; enableWebUi?: boolean; enableProxmox: boolean; enableTmuxMonitor: boolean; diff --git a/src/ui/AppShell.tsx b/src/ui/AppShell.tsx index 1d6b7c8cc..1694fb04b 100644 --- a/src/ui/AppShell.tsx +++ b/src/ui/AppShell.tsx @@ -2490,7 +2490,6 @@ export function AppShell({ enableCommandHistory: false, enableTunnel: false, enableFileManager: false, - enableDocker: false, enableProxmox: false, enableProxmoxStats: false, enableTmuxMonitor: false, diff --git a/src/ui/lib/host-to-ssh-host.ts b/src/ui/lib/host-to-ssh-host.ts index ae5edf273..1a7458f3a 100644 --- a/src/ui/lib/host-to-ssh-host.ts +++ b/src/ui/lib/host-to-ssh-host.ts @@ -26,10 +26,8 @@ export function hostToSSHHost(h: Host): SSHHost { enableTerminal: h.enableTerminal ?? false, enableTunnel: h.enableTunnel ?? false, enableFileManager: h.enableFileManager ?? false, - enableDocker: h.enableDocker ?? false, enableTerminalToolbar: h.enableTerminalToolbar ?? true, enableAiAssistant: h.enableAiAssistant ?? false, - dockerConfig: h.dockerConfig ?? null, enableWebUi: h.enableWebUi ?? false, webUiConfig: (h.pluginSettings?.["web-endpoint"]?.webUiConfig as SSHHost["webUiConfig"] | undefined) ?? { endpoints: [] }, diff --git a/src/ui/locales/en.json b/src/ui/locales/en.json index 0b7a70c5c..706ad0dce 100644 --- a/src/ui/locales/en.json +++ b/src/ui/locales/en.json @@ -1241,8 +1241,6 @@ "disableTerminalFeature": "Disable Terminal", "enableFilesFeature": "Enable Files", "disableFilesFeature": "Disable Files", - "enableDockerFeature": "Enable Docker", - "disableDockerFeature": "Disable Docker", "enableProxmoxFeature": "Enable Proxmox", "disableProxmoxFeature": "Disable Proxmox", "addTagsPlaceholder": "Add tags...", @@ -1364,7 +1362,6 @@ "filterFeaturesGroup": "Features", "filterFeatureTerminal": "Terminal", "filterFeatureFileManager": "File Manager", - "filterFeatureDocker": "Docker", "filterTagsGroup": "Tags", "connectRdp": "RDP", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/af_ZA.json b/src/ui/locales/translated/af_ZA.json index e120e9c95..f63d2986d 100644 --- a/src/ui/locales/translated/af_ZA.json +++ b/src/ui/locales/translated/af_ZA.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "Deaktiveer lêers", "enableTunnelsFeature": "Aktiveer tonnels", "disableTunnelsFeature": "Deaktiveer tonnels", - "enableDockerFeature": "Aktiveer Docker", - "disableDockerFeature": "Deaktiveer Docker", "enableProxmoxFeature": "Aktiveer Proxmox", "disableProxmoxFeature": "Deaktiveer Proxmox", "addTagsPlaceholder": "Voeg etikette by...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "Terminaal", "filterFeatureFileManager": "Lêerbestuurder", "filterFeatureTunnel": "Tonnel", - "filterFeatureDocker": "Docker", "filterTagsGroup": "Etikette", "connectRdp": "RDP", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/ar_SA.json b/src/ui/locales/translated/ar_SA.json index ba5ca4260..2c8075401 100644 --- a/src/ui/locales/translated/ar_SA.json +++ b/src/ui/locales/translated/ar_SA.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "تعطيل الملفات", "enableTunnelsFeature": "تفعيل الأنفاق", "disableTunnelsFeature": "تعطيل الأنفاق", - "enableDockerFeature": "تفعيل Docker", - "disableDockerFeature": "تعطيل Docker", "enableProxmoxFeature": "تفعيل Proxmox", "disableProxmoxFeature": "تعطيل Proxmox", "addTagsPlaceholder": "إضافة علامات...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "الطرفية", "filterFeatureFileManager": "مدير الملفات", "filterFeatureTunnel": "نفق", - "filterFeatureDocker": "Docker", "filterTagsGroup": "الوسوم", "connectRdp": "RDP", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/bg_BG.json b/src/ui/locales/translated/bg_BG.json index 5a183b1ff..d20b48e67 100644 --- a/src/ui/locales/translated/bg_BG.json +++ b/src/ui/locales/translated/bg_BG.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "Деактивиране на файлове", "enableTunnelsFeature": "Активиране на тунели", "disableTunnelsFeature": "Деактивиране на тунелите", - "enableDockerFeature": "Активиране на Докер", - "disableDockerFeature": "Деактивиране на Докер", "enableProxmoxFeature": "Активиране на Proxmox", "disableProxmoxFeature": "Деактивиране на Proxmox", "addTagsPlaceholder": "Добавяне на етикети...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "Терминал", "filterFeatureFileManager": "Файлов мениджър", "filterFeatureTunnel": "Тунел", - "filterFeatureDocker": "Докер", "filterTagsGroup": "Етикети", "connectRdp": "ПРСР", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/bn_BD.json b/src/ui/locales/translated/bn_BD.json index 66a6a08b9..a3eb6de8c 100644 --- a/src/ui/locales/translated/bn_BD.json +++ b/src/ui/locales/translated/bn_BD.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "ফাইলগুলি নিষ্ক্রিয় করুন", "enableTunnelsFeature": "টানেলগুলি সক্ষম করুন", "disableTunnelsFeature": "টানেল নিষ্ক্রিয় করুন", - "enableDockerFeature": "ডকার সক্রিয় করুন", - "disableDockerFeature": "ডকার নিষ্ক্রিয় করুন", "enableProxmoxFeature": "প্রক্সমক্স সক্রিয় করুন", "disableProxmoxFeature": "প্রক্সমক্স নিষ্ক্রিয় করুন", "addTagsPlaceholder": "ট্যাগ যোগ করুন...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "টার্মিনাল", "filterFeatureFileManager": "ফাইল ম্যানেজার", "filterFeatureTunnel": "টানেল", - "filterFeatureDocker": "ডকার", "filterTagsGroup": "ট্যাগ", "connectRdp": "আরডিপি", "connectVnc": "ভিএনসি", diff --git a/src/ui/locales/translated/ca_ES.json b/src/ui/locales/translated/ca_ES.json index 9984a5505..55670c7a5 100644 --- a/src/ui/locales/translated/ca_ES.json +++ b/src/ui/locales/translated/ca_ES.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "Desactiva els fitxers", "enableTunnelsFeature": "Habilita els túnels", "disableTunnelsFeature": "Desactiva els túnels", - "enableDockerFeature": "Activa Docker", - "disableDockerFeature": "Desactiva Docker", "enableProxmoxFeature": "Activa Proxmox", "disableProxmoxFeature": "Desactiva Proxmox", "addTagsPlaceholder": "Afegeix etiquetes...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "Terminal", "filterFeatureFileManager": "Gestor de fitxers", "filterFeatureTunnel": "Túnel", - "filterFeatureDocker": "Docker", "filterTagsGroup": "Etiquetes", "connectRdp": "RDP", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/cs_CZ.json b/src/ui/locales/translated/cs_CZ.json index 76ce3ca12..f709ebfbd 100644 --- a/src/ui/locales/translated/cs_CZ.json +++ b/src/ui/locales/translated/cs_CZ.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "Zakázat soubory", "enableTunnelsFeature": "Povolit tunely", "disableTunnelsFeature": "Zakázat tunely", - "enableDockerFeature": "Povolit Docker", - "disableDockerFeature": "Zakázat Docker", "enableProxmoxFeature": "Povolit Proxmox", "disableProxmoxFeature": "Zakázat Proxmox", "addTagsPlaceholder": "Přidat štítky...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "Terminál", "filterFeatureFileManager": "Správce souborů", "filterFeatureTunnel": "Tunel", - "filterFeatureDocker": "Přístavní dělník", "filterTagsGroup": "Štítky", "connectRdp": "PRV", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/da_DK.json b/src/ui/locales/translated/da_DK.json index 5d3a4ac39..4855ecdab 100644 --- a/src/ui/locales/translated/da_DK.json +++ b/src/ui/locales/translated/da_DK.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "Deaktiver filer", "enableTunnelsFeature": "Aktivér tunneler", "disableTunnelsFeature": "Deaktiver tunneler", - "enableDockerFeature": "Aktivér Docker", - "disableDockerFeature": "Deaktiver Docker", "enableProxmoxFeature": "Aktivér Proxmox", "disableProxmoxFeature": "Deaktiver Proxmox", "addTagsPlaceholder": "Tilføj tags...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "Terminal", "filterFeatureFileManager": "Filhåndtering", "filterFeatureTunnel": "Tunnel", - "filterFeatureDocker": "Docker", "filterTagsGroup": "Tags", "connectRdp": "Landdistriktsudviklingsplan", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/de_DE.json b/src/ui/locales/translated/de_DE.json index d3ade0bc0..bc47ca570 100644 --- a/src/ui/locales/translated/de_DE.json +++ b/src/ui/locales/translated/de_DE.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "Dateien deaktivieren", "enableTunnelsFeature": "Tunnels aktivieren", "disableTunnelsFeature": "Tunnels deaktivieren", - "enableDockerFeature": "Docker aktivieren", - "disableDockerFeature": "Docker deaktivieren", "enableProxmoxFeature": "Proxmox aktivieren", "disableProxmoxFeature": "Proxmox deaktivieren", "addTagsPlaceholder": "Tags hinzufügen...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "Terminal", "filterFeatureFileManager": "Dateimanager", "filterFeatureTunnel": "Tunnel", - "filterFeatureDocker": "Docker", "filterTagsGroup": "Tags", "connectRdp": "RDP", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/el_GR.json b/src/ui/locales/translated/el_GR.json index 1cfb65a98..7effaf226 100644 --- a/src/ui/locales/translated/el_GR.json +++ b/src/ui/locales/translated/el_GR.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "Απενεργοποίηση αρχείων", "enableTunnelsFeature": "Ενεργοποίηση σηράγγων", "disableTunnelsFeature": "Απενεργοποίηση σηράγγων", - "enableDockerFeature": "Ενεργοποίηση Docker", - "disableDockerFeature": "Απενεργοποίηση Docker", "enableProxmoxFeature": "Ενεργοποίηση Proxmox", "disableProxmoxFeature": "Απενεργοποίηση Proxmox", "addTagsPlaceholder": "Προσθήκη ετικετών...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "Τερματικό", "filterFeatureFileManager": "Διαχειριστής αρχείων", "filterFeatureTunnel": "Σήραγγα", - "filterFeatureDocker": "Λιμενεργάτης", "filterTagsGroup": "Ετικέτες", "connectRdp": "ΠΑΑ", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/es_ES.json b/src/ui/locales/translated/es_ES.json index 59186c380..bacd9cf95 100644 --- a/src/ui/locales/translated/es_ES.json +++ b/src/ui/locales/translated/es_ES.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "Deshabilitar archivos", "enableTunnelsFeature": "Activar túneles", "disableTunnelsFeature": "Desactivar túneles", - "enableDockerFeature": "Activar Docker", - "disableDockerFeature": "Desactivar Docker", "enableProxmoxFeature": "Activar Proxmox", "disableProxmoxFeature": "Desactivar Proxmox", "addTagsPlaceholder": "Añadir etiquetas...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "Terminal", "filterFeatureFileManager": "Gestor de archivos", "filterFeatureTunnel": "Túnel", - "filterFeatureDocker": "Docker", "filterTagsGroup": "Etiquetas", "connectRdp": "RDP", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/fi_FI.json b/src/ui/locales/translated/fi_FI.json index 2590203ab..00cd1fda6 100644 --- a/src/ui/locales/translated/fi_FI.json +++ b/src/ui/locales/translated/fi_FI.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "Poista tiedostot käytöstä", "enableTunnelsFeature": "Ota tunnelit käyttöön", "disableTunnelsFeature": "Tunnelien poistaminen käytöstä", - "enableDockerFeature": "Ota Docker käyttöön", - "disableDockerFeature": "Poista Docker käytöstä", "enableProxmoxFeature": "Ota Proxmox käyttöön", "disableProxmoxFeature": "Poista Proxmox käytöstä", "addTagsPlaceholder": "Lisää tunnisteita...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "Terminaali", "filterFeatureFileManager": "Tiedostonhallinta", "filterFeatureTunnel": "Tunneli", - "filterFeatureDocker": "Satamatyöläinen", "filterTagsGroup": "Tunnisteet", "connectRdp": "Maaseudun kehittämisohjelma", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/fr_FR.json b/src/ui/locales/translated/fr_FR.json index 64f1c265a..500c582c5 100644 --- a/src/ui/locales/translated/fr_FR.json +++ b/src/ui/locales/translated/fr_FR.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "Désactiver les fichiers", "enableTunnelsFeature": "Activer les tunnels", "disableTunnelsFeature": "Désactiver les tunnels", - "enableDockerFeature": "Activer Docker", - "disableDockerFeature": "Désactiver Docker", "enableProxmoxFeature": "Activer Proxmox", "disableProxmoxFeature": "Désactiver Proxmox", "addTagsPlaceholder": "Ajouter des étiquettes...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "Terminal", "filterFeatureFileManager": "Gestionnaire de fichiers", "filterFeatureTunnel": "Tunnel", - "filterFeatureDocker": "Docker", "filterTagsGroup": "Étiquettes", "connectRdp": "RDP", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/he_IL.json b/src/ui/locales/translated/he_IL.json index 76836321d..39def40f4 100644 --- a/src/ui/locales/translated/he_IL.json +++ b/src/ui/locales/translated/he_IL.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "השבת קבצים", "enableTunnelsFeature": "הפעל מנהרות", "disableTunnelsFeature": "השבת מנהרות", - "enableDockerFeature": "הפעל את Docker", - "disableDockerFeature": "השבת את Docker", "enableProxmoxFeature": "הפעל את Proxmox", "disableProxmoxFeature": "השבת את פרוקסמוקס", "addTagsPlaceholder": "הוסף תגיות...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "מָסוֹף", "filterFeatureFileManager": "מנהל הקבצים", "filterFeatureTunnel": "מִנהָרָה", - "filterFeatureDocker": "דוקר", "filterTagsGroup": "תגיות", "connectRdp": "RDP", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/hi_IN.json b/src/ui/locales/translated/hi_IN.json index 289aa7266..329193a93 100644 --- a/src/ui/locales/translated/hi_IN.json +++ b/src/ui/locales/translated/hi_IN.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "फ़ाइलें अक्षम करें", "enableTunnelsFeature": "टनल सक्षम करें", "disableTunnelsFeature": "टनल अक्षम करें", - "enableDockerFeature": "Docker सक्षम करें", - "disableDockerFeature": "Docker अक्षम करें", "enableProxmoxFeature": "Proxmox सक्षम करें", "disableProxmoxFeature": "Proxmox अक्षम करें", "addTagsPlaceholder": "टैग जोड़ें…", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "टर्मिनल", "filterFeatureFileManager": "फ़ाइल प्रबंधक", "filterFeatureTunnel": "टनल", - "filterFeatureDocker": "Docker", "filterTagsGroup": "टैग्स", "connectRdp": "आरडीपी", "connectVnc": "वीएनसी", diff --git a/src/ui/locales/translated/hu_HU.json b/src/ui/locales/translated/hu_HU.json index 3c03c2d2f..002409180 100644 --- a/src/ui/locales/translated/hu_HU.json +++ b/src/ui/locales/translated/hu_HU.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "Fájlok letiltása", "enableTunnelsFeature": "Alagutak engedélyezése", "disableTunnelsFeature": "Alagutak letiltása", - "enableDockerFeature": "Docker engedélyezése", - "disableDockerFeature": "Docker letiltása", "enableProxmoxFeature": "Proxmox engedélyezése", "disableProxmoxFeature": "Proxmox letiltása", "addTagsPlaceholder": "Címkék hozzáadása...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "Terminál", "filterFeatureFileManager": "Fájlkezelő", "filterFeatureTunnel": "Alagút", - "filterFeatureDocker": "Dokkmunkás", "filterTagsGroup": "Címkék", "connectRdp": "Vidékfejlesztési Program", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/id_ID.json b/src/ui/locales/translated/id_ID.json index 7eabb70e6..27feaedd8 100644 --- a/src/ui/locales/translated/id_ID.json +++ b/src/ui/locales/translated/id_ID.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "Nonaktifkan File", "enableTunnelsFeature": "Aktifkan Terowongan", "disableTunnelsFeature": "Nonaktifkan Terowongan", - "enableDockerFeature": "Aktifkan Docker", - "disableDockerFeature": "Nonaktifkan Docker", "enableProxmoxFeature": "Aktifkan Proxmox", "disableProxmoxFeature": "Nonaktifkan Proxmox", "addTagsPlaceholder": "Tambahkan tag...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "Terminal", "filterFeatureFileManager": "Pengelola File", "filterFeatureTunnel": "Terowongan", - "filterFeatureDocker": "Buruh pelabuhan", "filterTagsGroup": "Tag", "connectRdp": "RDP", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/it_IT.json b/src/ui/locales/translated/it_IT.json index 32b301a52..0ae1d6bc3 100644 --- a/src/ui/locales/translated/it_IT.json +++ b/src/ui/locales/translated/it_IT.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "Disabilita File", "enableTunnelsFeature": "Abilita tunnel", "disableTunnelsFeature": "Disabilita tunnel", - "enableDockerFeature": "Abilita Docker", - "disableDockerFeature": "Disabilita Docker", "enableProxmoxFeature": "Abilita Proxmox", "disableProxmoxFeature": "Disabilita Proxmox", "addTagsPlaceholder": "Aggiungi tag...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "Terminale", "filterFeatureFileManager": "Gestione file", "filterFeatureTunnel": "Tunnel", - "filterFeatureDocker": "Docker", "filterTagsGroup": "Tag", "connectRdp": "RDP", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/ja_JP.json b/src/ui/locales/translated/ja_JP.json index bf9cc2ebc..00730b4bb 100644 --- a/src/ui/locales/translated/ja_JP.json +++ b/src/ui/locales/translated/ja_JP.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "ファイルを無効化", "enableTunnelsFeature": "トンネルを有効化", "disableTunnelsFeature": "トンネルを無効化", - "enableDockerFeature": "Dockerを有効化", - "disableDockerFeature": "Dockerを無効化", "enableProxmoxFeature": "Proxmoxを有効化", "disableProxmoxFeature": "Proxmoxを無効化", "addTagsPlaceholder": "タグを追加...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "ターミナル", "filterFeatureFileManager": "ファイルマネージャー", "filterFeatureTunnel": "トンネル", - "filterFeatureDocker": "Docker", "filterTagsGroup": "タグ", "connectRdp": "RDP", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/ko_KR.json b/src/ui/locales/translated/ko_KR.json index 876626860..04efdcc28 100644 --- a/src/ui/locales/translated/ko_KR.json +++ b/src/ui/locales/translated/ko_KR.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "파일 비활성화", "enableTunnelsFeature": "터널 활성화", "disableTunnelsFeature": "터널 비활성화", - "enableDockerFeature": "Docker 활성화", - "disableDockerFeature": "Docker 비활성화", "enableProxmoxFeature": "Proxmox 활성화", "disableProxmoxFeature": "Proxmox 비활성화", "addTagsPlaceholder": "태그 추가...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "터미널", "filterFeatureFileManager": "파일 관리자", "filterFeatureTunnel": "터널", - "filterFeatureDocker": "Docker", "filterTagsGroup": "태그", "connectRdp": "RDP", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/nl_NL.json b/src/ui/locales/translated/nl_NL.json index 024ab31cc..c105ea247 100644 --- a/src/ui/locales/translated/nl_NL.json +++ b/src/ui/locales/translated/nl_NL.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "Bestanden uitschakelen", "enableTunnelsFeature": "Tunnels inschakelen", "disableTunnelsFeature": "Tunnels uitschakelen", - "enableDockerFeature": "Docker inschakelen", - "disableDockerFeature": "Docker uitschakelen", "enableProxmoxFeature": "Proxmox inschakelen", "disableProxmoxFeature": "Proxmox uitschakelen", "addTagsPlaceholder": "Tags toevoegen...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "Terminal", "filterFeatureFileManager": "Bestandsbeheerder", "filterFeatureTunnel": "Tunnel", - "filterFeatureDocker": "Docker", "filterTagsGroup": "Tags", "connectRdp": "RDP", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/no_NO.json b/src/ui/locales/translated/no_NO.json index 5e162cdd6..6072ee413 100644 --- a/src/ui/locales/translated/no_NO.json +++ b/src/ui/locales/translated/no_NO.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "Deaktiver filer", "enableTunnelsFeature": "Aktiver tunneler", "disableTunnelsFeature": "Deaktiver tunneler", - "enableDockerFeature": "Aktiver Docker", - "disableDockerFeature": "Deaktiver Docker", "enableProxmoxFeature": "Aktiver Proxmox", "disableProxmoxFeature": "Deaktiver Proxmox", "addTagsPlaceholder": "Legg til tagger...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "Terminal", "filterFeatureFileManager": "Filbehandler", "filterFeatureTunnel": "Tunnel", - "filterFeatureDocker": "Dokker", "filterTagsGroup": "Tagger", "connectRdp": "RDP", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/pl_PL.json b/src/ui/locales/translated/pl_PL.json index aa75a045d..1d0d89a74 100644 --- a/src/ui/locales/translated/pl_PL.json +++ b/src/ui/locales/translated/pl_PL.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "Wyłącz pliki", "enableTunnelsFeature": "Włącz tunele", "disableTunnelsFeature": "Wyłącz tunele", - "enableDockerFeature": "Włącz Docker", - "disableDockerFeature": "Wyłącz Docker", "enableProxmoxFeature": "Włącz Proxmox", "disableProxmoxFeature": "Wyłącz Proxmox", "addTagsPlaceholder": "Dodaj tagi...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "Terminal", "filterFeatureFileManager": "Menedżer plików", "filterFeatureTunnel": "Tunel", - "filterFeatureDocker": "Docker", "filterTagsGroup": "Tagi", "connectRdp": "RDP", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/pt_BR.json b/src/ui/locales/translated/pt_BR.json index 959ea6852..5841cf149 100644 --- a/src/ui/locales/translated/pt_BR.json +++ b/src/ui/locales/translated/pt_BR.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "Desativar arquivos", "enableTunnelsFeature": "Ativar túneis", "disableTunnelsFeature": "Desativar túneis", - "enableDockerFeature": "Habilitar Docker", - "disableDockerFeature": "Desativar o Docker", "enableProxmoxFeature": "Ativar Proxmox", "disableProxmoxFeature": "Desativar Proxmox", "addTagsPlaceholder": "Adicionar etiquetas...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "terminal", "filterFeatureFileManager": "Gerenciador de arquivos", "filterFeatureTunnel": "Túnel", - "filterFeatureDocker": "Docker", "filterTagsGroup": "Etiquetas", "connectRdp": "RDP", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/pt_PT.json b/src/ui/locales/translated/pt_PT.json index 1a7fed386..ba440b8e0 100644 --- a/src/ui/locales/translated/pt_PT.json +++ b/src/ui/locales/translated/pt_PT.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "Desativar Ficheiros", "enableTunnelsFeature": "Ativar Tunnels", "disableTunnelsFeature": "Desativar Tunnels", - "enableDockerFeature": "Ativar Docker", - "disableDockerFeature": "Desativar Docker", "enableProxmoxFeature": "Ativar Proxmox", "disableProxmoxFeature": "Desativar Proxmox", "addTagsPlaceholder": "Adicionar tags...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "Terminal", "filterFeatureFileManager": "Gestor de Ficheiros", "filterFeatureTunnel": "Túnel", - "filterFeatureDocker": "Docker", "filterTagsGroup": "Etiquetas", "connectRdp": "RDP", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/ro_RO.json b/src/ui/locales/translated/ro_RO.json index 4fc6908bf..354e0e594 100644 --- a/src/ui/locales/translated/ro_RO.json +++ b/src/ui/locales/translated/ro_RO.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "Dezactivați fișierele", "enableTunnelsFeature": "Activează tunelurile", "disableTunnelsFeature": "Dezactivați tunelurile", - "enableDockerFeature": "Activează Docker", - "disableDockerFeature": "Dezactivați Docker", "enableProxmoxFeature": "Activează Proxmox", "disableProxmoxFeature": "Dezactivați Proxmox", "addTagsPlaceholder": "Adăugați etichete...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "Terminal", "filterFeatureFileManager": "Manager de fișiere", "filterFeatureTunnel": "Tunel", - "filterFeatureDocker": "Docher", "filterTagsGroup": "Etichete", "connectRdp": "RDP", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/ru_RU.json b/src/ui/locales/translated/ru_RU.json index a70c4a60a..2cd9e9616 100644 --- a/src/ui/locales/translated/ru_RU.json +++ b/src/ui/locales/translated/ru_RU.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "Отключить файлы", "enableTunnelsFeature": "Включить туннели", "disableTunnelsFeature": "Отключить туннели", - "enableDockerFeature": "Включить Docker", - "disableDockerFeature": "Отключить Docker", "enableProxmoxFeature": "Включить Proxmox", "disableProxmoxFeature": "Отключить Proxmox", "addTagsPlaceholder": "Добавить теги...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "Терминал", "filterFeatureFileManager": "Файловый менеджер", "filterFeatureTunnel": "Туннель", - "filterFeatureDocker": "Docker", "filterTagsGroup": "Теги", "connectRdp": "РДП", "connectVnc": "ВНК", diff --git a/src/ui/locales/translated/sr_SP.json b/src/ui/locales/translated/sr_SP.json index 3877b457b..b73acaac7 100644 --- a/src/ui/locales/translated/sr_SP.json +++ b/src/ui/locales/translated/sr_SP.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "Онемогући датотеке", "enableTunnelsFeature": "Омогући тунеле", "disableTunnelsFeature": "Онемогући тунеле", - "enableDockerFeature": "Омогући Докер", - "disableDockerFeature": "Онемогући Докер", "enableProxmoxFeature": "Омогући Proxmox", "disableProxmoxFeature": "Онемогући Проксмокс", "addTagsPlaceholder": "Додај ознаке...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "Терминал", "filterFeatureFileManager": "Менаџер датотека", "filterFeatureTunnel": "Тунел", - "filterFeatureDocker": "Докер", "filterTagsGroup": "Ознаке", "connectRdp": "РДП", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/sv_SE.json b/src/ui/locales/translated/sv_SE.json index ef64e5b17..682599ad7 100644 --- a/src/ui/locales/translated/sv_SE.json +++ b/src/ui/locales/translated/sv_SE.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "Inaktivera filer", "enableTunnelsFeature": "Aktivera tunnlar", "disableTunnelsFeature": "Inaktivera tunnlar", - "enableDockerFeature": "Aktivera Docker", - "disableDockerFeature": "Inaktivera Docker", "enableProxmoxFeature": "Aktivera Proxmox", "disableProxmoxFeature": "Inaktivera Proxmox", "addTagsPlaceholder": "Lägg till taggar...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "Terminal", "filterFeatureFileManager": "Filhanteraren", "filterFeatureTunnel": "Tunnel", - "filterFeatureDocker": "Hamnarbetare", "filterTagsGroup": "Taggar", "connectRdp": "Landsbygdsutveckling", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/th_TH.json b/src/ui/locales/translated/th_TH.json index eed50ed94..5f4be09cc 100644 --- a/src/ui/locales/translated/th_TH.json +++ b/src/ui/locales/translated/th_TH.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "ปิดใช้งานไฟล์", "enableTunnelsFeature": "เปิดใช้งานอุโมงค์", "disableTunnelsFeature": "ปิดใช้งานอุโมงค์", - "enableDockerFeature": "เปิดใช้งาน Docker", - "disableDockerFeature": "ปิดใช้งาน Docker", "enableProxmoxFeature": "เปิดใช้งาน Proxmox", "disableProxmoxFeature": "ปิดใช้งาน Proxmox", "addTagsPlaceholder": "เพิ่มแท็ก...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "เทอร์มินัล", "filterFeatureFileManager": "ตัวจัดการไฟล์", "filterFeatureTunnel": "อุโมงค์", - "filterFeatureDocker": "ด็อกเกอร์", "filterTagsGroup": "แท็ก", "connectRdp": "อาร์ดีพี", "connectVnc": "วีเอ็นซี", diff --git a/src/ui/locales/translated/tr_TR.json b/src/ui/locales/translated/tr_TR.json index b6ed8e5d9..d5b145981 100644 --- a/src/ui/locales/translated/tr_TR.json +++ b/src/ui/locales/translated/tr_TR.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "Dosyaları Devre Dışı Bırak", "enableTunnelsFeature": "Tünelleri Etkinleştir", "disableTunnelsFeature": "Tünelleri Devre Dışı Bırak", - "enableDockerFeature": "Docker'ı Etkinleştir", - "disableDockerFeature": "Docker'ı Devre Dışı Bırak", "enableProxmoxFeature": "Proxmox'u Etkinleştir", "disableProxmoxFeature": "Proxmox'u Devre Dışı Bırak", "addTagsPlaceholder": "Etiket ekle...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "Terminal", "filterFeatureFileManager": "Dosya Yöneticisi", "filterFeatureTunnel": "Tünel", - "filterFeatureDocker": "Docker", "filterTagsGroup": "Etiketler", "connectRdp": "RDP", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/uk_UA.json b/src/ui/locales/translated/uk_UA.json index eee03416e..70194bd2d 100644 --- a/src/ui/locales/translated/uk_UA.json +++ b/src/ui/locales/translated/uk_UA.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "Вимкнути файли", "enableTunnelsFeature": "Увімкнути тунелі", "disableTunnelsFeature": "Вимкнути тунелі", - "enableDockerFeature": "Увімкнути Докер", - "disableDockerFeature": "Вимкнути Докер", "enableProxmoxFeature": "Увімкнути Proxmox", "disableProxmoxFeature": "Вимкнути Proxmox", "addTagsPlaceholder": "Додати теги...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "Термінал", "filterFeatureFileManager": "Файловий менеджер", "filterFeatureTunnel": "Тунель", - "filterFeatureDocker": "Докер", "filterTagsGroup": "Теги", "connectRdp": "ПРП", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/vi_VN.json b/src/ui/locales/translated/vi_VN.json index 81a8545b9..f97ecfbec 100644 --- a/src/ui/locales/translated/vi_VN.json +++ b/src/ui/locales/translated/vi_VN.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "Tắt Files", "enableTunnelsFeature": "Bật Tunnels", "disableTunnelsFeature": "Tắt Tunnels", - "enableDockerFeature": "Bật Docker", - "disableDockerFeature": "Tắt Docker", "enableProxmoxFeature": "Bật Proxmox", "disableProxmoxFeature": "Tắt Proxmox", "addTagsPlaceholder": "Thêm thẻ...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "Terminal", "filterFeatureFileManager": "Trình quản lý file", "filterFeatureTunnel": "Tunnel", - "filterFeatureDocker": "Docker", "filterTagsGroup": "Thẻ", "connectRdp": "RDP", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/zh_CN.json b/src/ui/locales/translated/zh_CN.json index c6d74e588..888013434 100644 --- a/src/ui/locales/translated/zh_CN.json +++ b/src/ui/locales/translated/zh_CN.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "禁用文件", "enableTunnelsFeature": "启用隧道", "disableTunnelsFeature": "禁用隧道", - "enableDockerFeature": "启用 Docker", - "disableDockerFeature": "禁用 Docker", "enableProxmoxFeature": "启用 Proxmox", "disableProxmoxFeature": "禁用 Proxmox", "addTagsPlaceholder": "添加标签...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "终端", "filterFeatureFileManager": "文件管理器", "filterFeatureTunnel": "隧道", - "filterFeatureDocker": "Docker", "filterTagsGroup": "标签", "connectRdp": "RDP", "connectVnc": "VNC", diff --git a/src/ui/locales/translated/zh_TW.json b/src/ui/locales/translated/zh_TW.json index 11c535aaa..af4d34389 100644 --- a/src/ui/locales/translated/zh_TW.json +++ b/src/ui/locales/translated/zh_TW.json @@ -1245,8 +1245,6 @@ "disableFilesFeature": "停用檔案", "enableTunnelsFeature": "啟用通道", "disableTunnelsFeature": "停用通道", - "enableDockerFeature": "啟用 Docker", - "disableDockerFeature": "停用 Docker", "enableProxmoxFeature": "啟用 Proxmox", "disableProxmoxFeature": "停用 Proxmox", "addTagsPlaceholder": "新增標籤...", @@ -1369,7 +1367,6 @@ "filterFeatureTerminal": "終端機", "filterFeatureFileManager": "檔案管理員", "filterFeatureTunnel": "通道", - "filterFeatureDocker": "Docker", "filterTagsGroup": "標籤", "connectRdp": "遠端桌面協定", "connectVnc": "VNC", diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index e6f96cebd..64a9fa0e2 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -770,12 +770,6 @@ function initializeApiInstances() { // RBAC API (port 30001) rbacApi = createApiInstance(getApiUrl("", 30001), "RBAC"); - // Docker Management API - the docker plugin, mounted on the main backend. - dockerApi = createApiInstance( - getApiUrl("/plugin-api/docker/docker", 30001), - "DOCKER", - ); - // Tmux Monitor API (port 30010) --- tmux-monitor --- tmuxMonitorApi = createApiInstance( getApiUrl("/tmux_monitor", 30010), @@ -803,9 +797,6 @@ export let dashboardApi: AxiosInstance; // RBAC API (port 30001) export let rbacApi: AxiosInstance; -// Docker Management API (docker plugin) -export let dockerApi: AxiosInstance; - // Tmux Monitor API (port 30010) --- tmux-monitor --- export let tmuxMonitorApi: AxiosInstance; diff --git a/src/ui/plugin-host/sdk-ui.ts b/src/ui/plugin-host/sdk-ui.ts index 6b7e527a8..036369829 100644 --- a/src/ui/plugin-host/sdk-ui.ts +++ b/src/ui/plugin-host/sdk-ui.ts @@ -85,7 +85,10 @@ export * from "@/components/sheet"; export { useConfirmation } from "@/hooks/use-confirmation"; export { useAdaptivePolling } from "@/hooks/use-adaptive-polling"; // Per-area display density and chart options the user picked in Appearance. -export { useAreaPreferences } from "@/contexts/UiPreferencesContext"; +export { + useAreaPreferences, + useUiPreferencesContext, +} from "@/contexts/UiPreferencesContext"; // Homepage widget pieces, for plugins that register a widget. export { WidgetTitle } from "@/features/homepage/widgets/WidgetTitle"; export { runVisibleInterval } from "@/features/homepage/use-visible-interval"; diff --git a/src/ui/sidebar/HostEditorData.ts b/src/ui/sidebar/HostEditorData.ts index 42712c424..079ee4c04 100644 --- a/src/ui/sidebar/HostEditorData.ts +++ b/src/ui/sidebar/HostEditorData.ts @@ -163,23 +163,13 @@ export function createHostEditorForm( host?.enableCommandHistory ?? d?.enableCommandHistory ?? true, enableFileManager: host?.enableFileManager ?? false, scpLegacy: host?.scpLegacy ?? false, - enableDocker: host?.enableDocker ?? false, - dockerConfig: host?.dockerConfig ?? { runtime: "docker" as const }, enableTmuxMonitor: host?.enableTmuxMonitor ?? false, enableTerminalToolbar: host?.enableTerminalToolbar ?? true, enableAiAssistant: host?.enableAiAssistant ?? false, enableProxmox: (proxmoxSettings.enableProxmox as boolean) ?? false, + // The proxmox tab fills in its own defaults when this is unset. proxmoxConfig: - ((proxmoxSettings.proxmoxConfig as Host["proxmoxConfig"]) ?? { - defaultCredentialId: null, - defaultAuthType: "password", - windowsPatterns: "win, windows", - dockerPatterns: "docker", - preferredPrefixes: "10., 192.168.", - autoSyncEnabled: false, - syncIntervalMinutes: 15, - markMissingGuests: true, - }) as Host["proxmoxConfig"], + (proxmoxSettings.proxmoxConfig as Host["proxmoxConfig"]) ?? null, enableProxmoxStats: (proxmoxSettings.enableProxmoxStats as boolean) ?? false, proxmoxStatsConfig: @@ -436,8 +426,6 @@ export function buildHostEditorPayload( enableCommandHistory: form.enableCommandHistory, enableFileManager: form.enableFileManager, scpLegacy: form.scpLegacy, - enableDocker: form.enableDocker, - dockerConfig: form.enableDocker ? form.dockerConfig : null, enableTmuxMonitor: form.enableTmuxMonitor, enableTerminalToolbar: form.enableTerminalToolbar, enableAiAssistant: form.enableAiAssistant, diff --git a/src/ui/sidebar/HostManagerData.ts b/src/ui/sidebar/HostManagerData.ts index 3c2a19c1c..630b8f835 100644 --- a/src/ui/sidebar/HostManagerData.ts +++ b/src/ui/sidebar/HostManagerData.ts @@ -95,8 +95,6 @@ export function sshHostToHost(h: SSHHostWithStatus): Host { enableCommandHistory: h.enableCommandHistory ?? true, enableTunnel: h.enableTunnel ?? false, enableFileManager: h.enableFileManager ?? true, - enableDocker: h.enableDocker ?? false, - dockerConfig: h.dockerConfig ?? null, enableWebUi: h.enableWebUi ?? false, enableProxmox: (proxmoxSettings.enableProxmox as boolean) ?? false, enableProxmoxStats: diff --git a/src/ui/sidebar/HostsPanel.tsx b/src/ui/sidebar/HostsPanel.tsx index 02c2b2a1f..31063fbae 100644 --- a/src/ui/sidebar/HostsPanel.tsx +++ b/src/ui/sidebar/HostsPanel.tsx @@ -152,8 +152,7 @@ function hostPassesFilters(host: Host, filters: FilterState): boolean { } if (filters.features.length > 0) { const ok = - (filters.features.includes("fileManager") && host.enableFileManager) || - (filters.features.includes("docker") && host.enableDocker); + filters.features.includes("fileManager") && host.enableFileManager; if (!ok) return false; } if (filters.tags.length > 0) { @@ -355,7 +354,6 @@ export function HostsPanel({ enableTerminal: true, enableTunnel: false, enableFileManager: true, - enableDocker: false, defaultPath: "/var/www", }, { @@ -373,7 +371,6 @@ export function HostsPanel({ enableTerminal: true, enableTunnel: true, enableFileManager: false, - enableDocker: false, }, ], }, @@ -780,23 +777,20 @@ export function HostsPanel({ {t("hosts.filterFeaturesGroup")} - {( - [ - ["fileManager", "FileManager"], - ["docker", "Docker"], - ] as const - ).map(([val, key]) => ( - - handleFilterToggle("features", val) - } - onSelect={(e) => e.preventDefault()} - > - {t(`hosts.filterFeature${key}`)} - - ))} + {([["fileManager", "FileManager"]] as const).map( + ([val, key]) => ( + + handleFilterToggle("features", val) + } + onSelect={(e) => e.preventDefault()} + > + {t(`hosts.filterFeature${key}`)} + + ), + )} {allTags.length > 0 && ( <> diff --git a/src/ui/sidebar/host-export-payload.ts b/src/ui/sidebar/host-export-payload.ts index fb1fe987b..c52cf7c91 100644 --- a/src/ui/sidebar/host-export-payload.ts +++ b/src/ui/sidebar/host-export-payload.ts @@ -44,7 +44,6 @@ export const FIELD_GROUP_KEYS: Record = { "enableTerminalToolbar", "enableAiAssistant", "enableFileManager", - "enableDocker", "enableWebUi", "enableProxmox", "enableTmuxMonitor", @@ -59,7 +58,6 @@ export const FIELD_GROUP_KEYS: Record = { advanced: [ "statusCheckEnabled", "statusCheckInterval", - "dockerConfig", "webUiConfig", "proxmoxConfig", "terminalConfig", diff --git a/src/ui/sidebar/quick-connect-host.ts b/src/ui/sidebar/quick-connect-host.ts index a52dc26a3..17ea3a50a 100644 --- a/src/ui/sidebar/quick-connect-host.ts +++ b/src/ui/sidebar/quick-connect-host.ts @@ -34,7 +34,6 @@ export function createQuickConnectHost(input: QuickConnectInput): Host { enableCommandHistory: false, enableFileManager: false, enableTunnel: false, - enableDocker: false, enableTerminalToolbar: false, enableAiAssistant: false, enableSsh: false, @@ -71,7 +70,6 @@ export function createQuickConnectHost(input: QuickConnectInput): Host { enableCommandHistory: true, enableFileManager: true, enableTunnel: true, - enableDocker: true, enableProxmox: false, enableProxmoxStats: false, enableTmuxMonitor: false, @@ -102,7 +100,6 @@ export function quickConnectHostToPayload(host: Host): SSHHostData { enableCommandHistory: host.enableCommandHistory, enableFileManager: host.enableFileManager, enableTunnel: host.enableTunnel, - enableDocker: host.enableDocker, enableProxmox: host.enableProxmox, enableTmuxMonitor: host.enableTmuxMonitor, enableTerminalToolbar: host.enableTerminalToolbar, diff --git a/src/ui/sidebar/tree/SidebarTree.tsx b/src/ui/sidebar/tree/SidebarTree.tsx index c77ff4edd..a31622754 100644 --- a/src/ui/sidebar/tree/SidebarTree.tsx +++ b/src/ui/sidebar/tree/SidebarTree.tsx @@ -11,7 +11,6 @@ import { useTranslation } from "react-i18next"; import { useVirtualizer } from "@tanstack/react-virtual"; import { useAreaPreferences } from "@/contexts/UiPreferencesContext"; import { - Box, Boxes, ChevronDown, Download, @@ -612,7 +611,6 @@ export function SidebarTree({ enableTerminal: host.enableTerminal, enableTunnel: host.enableTunnel, enableFileManager: host.enableFileManager, - enableDocker: host.enableDocker, sshPort: host.sshPort, rdpUser: host.rdpUser ?? null, rdpPassword: host.rdpPassword ?? null, @@ -1173,18 +1171,6 @@ export function SidebarTree({ value: false, icon: FolderSearch, }, - { - labelKey: "hosts.enableDockerFeature", - field: "enableDocker", - value: true, - icon: Box, - }, - { - labelKey: "hosts.disableDockerFeature", - field: "enableDocker", - value: false, - icon: Box, - }, { labelKey: "hosts.enableProxmoxFeature", field: "enableProxmox", diff --git a/src/ui/tests/sidebar/HostCredentialList.test.tsx b/src/ui/tests/sidebar/HostCredentialList.test.tsx index 74caa9bca..d7fe7aba5 100644 --- a/src/ui/tests/sidebar/HostCredentialList.test.tsx +++ b/src/ui/tests/sidebar/HostCredentialList.test.tsx @@ -49,7 +49,6 @@ function makeHost(overrides: Partial): Host { enableTunnel: false, serverTunnels: [], enableFileManager: true, - enableDocker: false, enableProxmox: false, enableProxmoxStats: false, enableTmuxMonitor: false, diff --git a/src/ui/tests/sidebar/HostExportDialog.test.tsx b/src/ui/tests/sidebar/HostExportDialog.test.tsx index c701029bb..3cbb77dc6 100644 --- a/src/ui/tests/sidebar/HostExportDialog.test.tsx +++ b/src/ui/tests/sidebar/HostExportDialog.test.tsx @@ -50,7 +50,6 @@ function sshHost( enableCommandHistory: false, enableTunnel: false, enableFileManager: false, - enableDocker: false, enableTmuxMonitor: false, enableTerminalToolbar: true, enableAiAssistant: false, diff --git a/src/ui/tests/sidebar/sidebar-tree-visible-rows.test.ts b/src/ui/tests/sidebar/sidebar-tree-visible-rows.test.ts index b54588d53..928b3b796 100644 --- a/src/ui/tests/sidebar/sidebar-tree-visible-rows.test.ts +++ b/src/ui/tests/sidebar/sidebar-tree-visible-rows.test.ts @@ -28,7 +28,6 @@ function host(id: string, name: string): Host { enableTerminal: true, enableTunnel: false, enableFileManager: true, - enableDocker: false, quickActions: [], } as Host; } diff --git a/src/ui/tests/sidebar/tree/HostItem/HostItem.test.tsx b/src/ui/tests/sidebar/tree/HostItem/HostItem.test.tsx index 0acd8e0a8..a0ae5a8d7 100644 --- a/src/ui/tests/sidebar/tree/HostItem/HostItem.test.tsx +++ b/src/ui/tests/sidebar/tree/HostItem/HostItem.test.tsx @@ -61,7 +61,6 @@ const baseHost: Host = { enableCommandHistory: true, enableTunnel: true, enableFileManager: true, - enableDocker: true, enableRdp: true, enableVnc: true, enableTelnet: true,