diff --git a/drizzle/mysql/0068_plugin_pinned_version.sql b/drizzle/mysql/0068_plugin_pinned_version.sql new file mode 100644 index 000000000..91a5c68cf --- /dev/null +++ b/drizzle/mysql/0068_plugin_pinned_version.sql @@ -0,0 +1 @@ +ALTER TABLE `plugins` ADD `pinned_version` text; \ No newline at end of file diff --git a/drizzle/mysql/meta/0068_snapshot.json b/drizzle/mysql/meta/0068_snapshot.json new file mode 100644 index 000000000..ba7bb0608 --- /dev/null +++ b/drizzle/mysql/meta/0068_snapshot.json @@ -0,0 +1,4745 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "5b26c0bd-97f5-4351-859b-b28056467f20", + "prevId": "b91f5f72-d0db-46df-915c-f8bce033cd9a", + "tables": { + "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": "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": "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": {} + }, + "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": "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": { + "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": "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": { + "credential_sidebar_preferences_user_id": { + "name": "credential_sidebar_preferences_user_id", + "columns": [ + "user_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": "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": { + "folder_access_id": { + "name": "folder_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "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": "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": "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_defaults": { + "name": "host_defaults", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "level": { + "name": "level", + "type": "enum('admin','user','folder')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_key": { + "name": "scope_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder_id": { + "name": "folder_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "namespace": { + "name": "namespace", + "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": false, + "autoincrement": false + }, + "updated_by": { + "name": "updated_by", + "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_host_defaults_scope_key": { + "name": "idx_host_defaults_scope_key", + "columns": [ + "scope_key", + "namespace", + "key" + ], + "isUnique": true + }, + "idx_host_defaults_user": { + "name": "idx_host_defaults_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_defaults_folder": { + "name": "idx_host_defaults_folder", + "columns": [ + "folder_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_defaults_user_id_users_id_fk": { + "name": "host_defaults_user_id_users_id_fk", + "tableFrom": "host_defaults", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_defaults_folder_id_ssh_folders_id_fk": { + "name": "host_defaults_folder_id_ssh_folders_id_fk", + "tableFrom": "host_defaults", + "tableTo": "ssh_folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_defaults_id": { + "name": "host_defaults_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_protocol_auth": { + "name": "host_protocol_auth", + "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 + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('direct')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fields": { + "name": "fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_fields": { + "name": "secret_fields", + "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_host_protocol_auth_host_protocol": { + "name": "idx_host_protocol_auth_host_protocol", + "columns": [ + "host_id", + "protocol" + ], + "isUnique": true + }, + "idx_host_protocol_auth_user": { + "name": "idx_host_protocol_auth_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_protocol_auth_credential": { + "name": "idx_host_protocol_auth_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_protocol_auth_host_id_ssh_data_id_fk": { + "name": "host_protocol_auth_host_id_ssh_data_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_user_id_users_id_fk": { + "name": "host_protocol_auth_user_id_users_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_credential_id_ssh_credentials_id_fk": { + "name": "host_protocol_auth_credential_id_ssh_credentials_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_protocol_auth_id": { + "name": "host_protocol_auth_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": "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": { + "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": "text", + "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": "text", + "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 + }, + "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 + }, + "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 + }, + "jump_hosts": { + "name": "jump_hosts", + "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 + }, + "ssh_options": { + "name": "ssh_options", + "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 + }, + "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 + }, + "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 + }, + "local_only": { + "name": "local_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_overrides": { + "name": "default_overrides", + "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_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" + } + }, + "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": {} + }, + "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": "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": { + "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": "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": "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": "enum('admin','user','host','secret')", + "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": "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": { + "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": "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": { + "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": "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": "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": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "pinned_version": { + "name": "pinned_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": 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": "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": { + "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": "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": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "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 + }, + "external_session_ref": { + "name": "external_session_ref", + "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": "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": "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": { + "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": "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": { + "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 + }, + "encrypted_fields": { + "name": "encrypted_fields", + "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": { + "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": "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": "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 + }, + "shared_source": { + "name": "shared_source", + "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_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": "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 + }, + "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 + }, + "local_only": { + "name": "local_only", + "type": "boolean", + "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_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" + } + }, + "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": {} + }, + "sync_conflicts": { + "name": "sync_conflicts", + "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": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "local_row": { + "name": "local_row", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_revision": { + "name": "server_revision", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sync_conflicts_user": { + "name": "idx_sync_conflicts_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sync_conflicts_user_id_users_id_fk": { + "name": "sync_conflicts_user_id_users_id_fk", + "tableFrom": "sync_conflicts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_conflicts_id": { + "name": "sync_conflicts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_link": { + "name": "sync_link", + "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 + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_name": { + "name": "server_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_version": { + "name": "server_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "basic_auth": { + "name": "basic_auth", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allow_invalid_certificate": { + "name": "allow_invalid_certificate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "remote_user_id": { + "name": "remote_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remote_username": { + "name": "remote_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account": { + "name": "account", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "known_types": { + "name": "known_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cursor": { + "name": "cursor", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('idle')" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "sync_link_user_id_users_id_fk": { + "name": "sync_link_user_id_users_id_fk", + "tableFrom": "sync_link", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_link_id": { + "name": "sync_link_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_records": { + "name": "sync_records", + "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": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision": { + "name": "revision", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "seq": { + "name": "seq", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_hash": { + "name": "error_hash", + "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_sync_records_user_entity_sync": { + "name": "idx_sync_records_user_entity_sync", + "columns": [ + "user_id", + "entity_type", + "sync_id" + ], + "isUnique": true + }, + "idx_sync_records_user_seq": { + "name": "idx_sync_records_user_seq", + "columns": [ + "user_id", + "seq" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sync_records_user_id_users_id_fk": { + "name": "sync_records_user_id_users_id_fk", + "tableFrom": "sync_records", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_records_id": { + "name": "sync_records_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": "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": { + "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": "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": { + "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": "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": { + "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": "text", + "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": "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": { + "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_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 + }, + "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 + }, + "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_keybindings": { + "name": "custom_keybindings", + "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": { + "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 + }, + "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": {} + } + }, + "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 797c7c578..b8770e7a5 100644 --- a/drizzle/mysql/meta/_journal.json +++ b/drizzle/mysql/meta/_journal.json @@ -477,6 +477,13 @@ "when": 1791209311841, "tag": "0067_external_session_ref", "breakpoints": true + }, + { + "idx": 68, + "version": "5", + "when": 1791256341049, + "tag": "0068_plugin_pinned_version", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/postgres/0068_plugin_pinned_version.sql b/drizzle/postgres/0068_plugin_pinned_version.sql new file mode 100644 index 000000000..4acdc2f16 --- /dev/null +++ b/drizzle/postgres/0068_plugin_pinned_version.sql @@ -0,0 +1 @@ +ALTER TABLE "plugins" ADD COLUMN "pinned_version" text; \ No newline at end of file diff --git a/drizzle/postgres/meta/0068_snapshot.json b/drizzle/postgres/meta/0068_snapshot.json new file mode 100644 index 000000000..89d6bf20f --- /dev/null +++ b/drizzle/postgres/meta/0068_snapshot.json @@ -0,0 +1,4770 @@ +{ + "id": "6d2db0b8-e68a-4537-9b6e-1e232c591629", + "prevId": "0e009e23-d22a-4b65-a142-0887e2f243a3", + "version": "7", + "dialect": "postgresql", + "tables": { + "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": "text", + "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": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "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.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": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "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": "text", + "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.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": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "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.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": "text", + "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_defaults": { + "name": "host_defaults", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "namespace": { + "name": "namespace", + "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": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_defaults_scope_key": { + "name": "idx_host_defaults_scope_key", + "columns": [ + { + "expression": "scope_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_defaults_user": { + "name": "idx_host_defaults_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_defaults_folder": { + "name": "idx_host_defaults_folder", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_defaults_user_id_users_id_fk": { + "name": "host_defaults_user_id_users_id_fk", + "tableFrom": "host_defaults", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_defaults_folder_id_ssh_folders_id_fk": { + "name": "host_defaults_folder_id_ssh_folders_id_fk", + "tableFrom": "host_defaults", + "tableTo": "ssh_folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_protocol_auth": { + "name": "host_protocol_auth", + "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 + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'direct'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fields": { + "name": "fields", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_fields": { + "name": "secret_fields", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_host_protocol_auth_host_protocol": { + "name": "idx_host_protocol_auth_host_protocol", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_protocol_auth_user": { + "name": "idx_host_protocol_auth_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_protocol_auth_credential": { + "name": "idx_host_protocol_auth_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_protocol_auth_host_id_ssh_data_id_fk": { + "name": "host_protocol_auth_host_id_ssh_data_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_user_id_users_id_fk": { + "name": "host_protocol_auth_user_id_users_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_credential_id_ssh_credentials_id_fk": { + "name": "host_protocol_auth_credential_id_ssh_credentials_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "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": "text", + "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": "text", + "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": "text", + "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 + }, + "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 + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "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 + }, + "ssh_options": { + "name": "ssh_options", + "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 + }, + "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 + }, + "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 + }, + "local_only": { + "name": "local_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_overrides": { + "name": "default_overrides", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "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": "text", + "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": "text", + "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": "text", + "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": "text", + "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": "text", + "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": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pinned_version": { + "name": "pinned_version", + "type": "text", + "primaryKey": false, + "notNull": 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": "text", + "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": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "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.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 + }, + "external_session_ref": { + "name": "external_session_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "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": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "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": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "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 + }, + "encrypted_fields": { + "name": "encrypted_fields", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "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": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "text", + "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 + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "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": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "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 + }, + "local_only": { + "name": "local_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_conflicts": { + "name": "sync_conflicts", + "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": "text", + "primaryKey": false, + "notNull": true + }, + "local_row": { + "name": "local_row", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_revision": { + "name": "server_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sync_conflicts_user": { + "name": "idx_sync_conflicts_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_conflicts_user_id_users_id_fk": { + "name": "sync_conflicts_user_id_users_id_fk", + "tableFrom": "sync_conflicts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_link": { + "name": "sync_link", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_name": { + "name": "server_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "server_version": { + "name": "server_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "basic_auth": { + "name": "basic_auth", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allow_invalid_certificate": { + "name": "allow_invalid_certificate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "remote_user_id": { + "name": "remote_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_username": { + "name": "remote_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account": { + "name": "account", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "known_types": { + "name": "known_types", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linked_at": { + "name": "linked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "sync_link_user_id_users_id_fk": { + "name": "sync_link_user_id_users_id_fk", + "tableFrom": "sync_link", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_records": { + "name": "sync_records", + "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": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_hash": { + "name": "error_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sync_records_user_entity_sync": { + "name": "idx_sync_records_user_entity_sync", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sync_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sync_records_user_seq": { + "name": "idx_sync_records_user_seq", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_records_user_id_users_id_fk": { + "name": "sync_records_user_id_users_id_fk", + "tableFrom": "sync_records", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "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": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "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": "text", + "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": "text", + "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": "text", + "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": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "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_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 + }, + "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 + }, + "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_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "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 + }, + "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 + } + }, + "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 20d26a13e..189b4ec17 100644 --- a/drizzle/postgres/meta/_journal.json +++ b/drizzle/postgres/meta/_journal.json @@ -477,6 +477,13 @@ "when": 1791209310758, "tag": "0067_external_session_ref", "breakpoints": true + }, + { + "idx": 68, + "version": "7", + "when": 1791256340388, + "tag": "0068_plugin_pinned_version", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/sqlite/0063_plugin_pinned_version.sql b/drizzle/sqlite/0063_plugin_pinned_version.sql new file mode 100644 index 000000000..91a5c68cf --- /dev/null +++ b/drizzle/sqlite/0063_plugin_pinned_version.sql @@ -0,0 +1 @@ +ALTER TABLE `plugins` ADD `pinned_version` text; \ No newline at end of file diff --git a/drizzle/sqlite/meta/0063_snapshot.json b/drizzle/sqlite/meta/0063_snapshot.json new file mode 100644 index 000000000..9a83fbebc --- /dev/null +++ b/drizzle/sqlite/meta/0063_snapshot.json @@ -0,0 +1,4466 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "f30d235a-ff96-490c-a84a-4787012caa8a", + "prevId": "dd7d607e-a0a7-4fa9-833b-d333ceed8cee", + "tables": { + "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": {} + }, + "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": {} + }, + "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": {} + }, + "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_defaults": { + "name": "host_defaults", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "namespace": { + "name": "namespace", + "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": false, + "autoincrement": false + }, + "updated_by": { + "name": "updated_by", + "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_host_defaults_scope_key": { + "name": "idx_host_defaults_scope_key", + "columns": [ + "scope_key", + "namespace", + "key" + ], + "isUnique": true + }, + "idx_host_defaults_user": { + "name": "idx_host_defaults_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_defaults_folder": { + "name": "idx_host_defaults_folder", + "columns": [ + "folder_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_defaults_user_id_users_id_fk": { + "name": "host_defaults_user_id_users_id_fk", + "tableFrom": "host_defaults", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_defaults_folder_id_ssh_folders_id_fk": { + "name": "host_defaults_folder_id_ssh_folders_id_fk", + "tableFrom": "host_defaults", + "tableTo": "ssh_folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_protocol_auth": { + "name": "host_protocol_auth", + "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 + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'direct'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fields": { + "name": "fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "secret_fields": { + "name": "secret_fields", + "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_host_protocol_auth_host_protocol": { + "name": "idx_host_protocol_auth_host_protocol", + "columns": [ + "host_id", + "protocol" + ], + "isUnique": true + }, + "idx_host_protocol_auth_user": { + "name": "idx_host_protocol_auth_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_protocol_auth_credential": { + "name": "idx_host_protocol_auth_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_protocol_auth_host_id_ssh_data_id_fk": { + "name": "host_protocol_auth_host_id_ssh_data_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_user_id_users_id_fk": { + "name": "host_protocol_auth_user_id_users_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_protocol_auth_credential_id_ssh_credentials_id_fk": { + "name": "host_protocol_auth_credential_id_ssh_credentials_id_fk", + "tableFrom": "host_protocol_auth", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "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 + }, + "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 + }, + "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 + }, + "jump_hosts": { + "name": "jump_hosts", + "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 + }, + "ssh_options": { + "name": "ssh_options", + "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 + }, + "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 + }, + "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 + }, + "local_only": { + "name": "local_only", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "shared_source": { + "name": "shared_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_overrides": { + "name": "default_overrides", + "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" + } + }, + "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 + }, + "pinned_version": { + "name": "pinned_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": 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": {} + }, + "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 + }, + "external_session_ref": { + "name": "external_session_ref", + "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_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 + }, + "encrypted_fields": { + "name": "encrypted_fields", + "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 + }, + "shared_source": { + "name": "shared_source", + "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 + }, + "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 + }, + "local_only": { + "name": "local_only", + "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": { + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_conflicts": { + "name": "sync_conflicts", + "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 + }, + "local_row": { + "name": "local_row", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_revision": { + "name": "server_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sync_conflicts_user": { + "name": "idx_sync_conflicts_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sync_conflicts_user_id_users_id_fk": { + "name": "sync_conflicts_user_id_users_id_fk", + "tableFrom": "sync_conflicts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_link": { + "name": "sync_link", + "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 + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_name": { + "name": "server_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_version": { + "name": "server_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_headers": { + "name": "custom_headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "basic_auth": { + "name": "basic_auth", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allow_invalid_certificate": { + "name": "allow_invalid_certificate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "remote_user_id": { + "name": "remote_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remote_username": { + "name": "remote_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account": { + "name": "account", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "known_types": { + "name": "known_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'idle'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_at": { + "name": "linked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "sync_link_user_id_users_id_fk": { + "name": "sync_link_user_id_users_id_fk", + "tableFrom": "sync_link", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_records": { + "name": "sync_records", + "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 + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted": { + "name": "deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_hash": { + "name": "error_hash", + "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_sync_records_user_entity_sync": { + "name": "idx_sync_records_user_entity_sync", + "columns": [ + "user_id", + "entity_type", + "sync_id" + ], + "isUnique": true + }, + "idx_sync_records_user_seq": { + "name": "idx_sync_records_user_seq", + "columns": [ + "user_id", + "seq" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sync_records_user_id_users_id_fk": { + "name": "sync_records_user_id_users_id_fk", + "tableFrom": "sync_records", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "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_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 + }, + "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 + }, + "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_keybindings": { + "name": "custom_keybindings", + "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 + }, + "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": {} + } + }, + "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 e31c0f7f6..7a994784e 100644 --- a/drizzle/sqlite/meta/_journal.json +++ b/drizzle/sqlite/meta/_journal.json @@ -442,6 +442,13 @@ "when": 1791209309626, "tag": "0062_external_session_ref", "breakpoints": true + }, + { + "idx": 63, + "version": "6", + "when": 1791256339684, + "tag": "0063_plugin_pinned_version", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/plugin-sdk/src/vitest-preset.ts b/packages/plugin-sdk/src/vitest-preset.ts index b9e8d48e8..2f3fe8ba6 100644 --- a/packages/plugin-sdk/src/vitest-preset.ts +++ b/packages/plugin-sdk/src/vitest-preset.ts @@ -92,11 +92,16 @@ export function pluginVitestConfig( ...options.alias, }; + // Forks workers on Windows abort now and then with "Worker exited + // unexpectedly". Threads don't. + const pool = "threads" as const; + return { resolve: { alias }, test: { globals: true, setupFiles: [setupFile], + pool, projects: [ { resolve: { alias }, @@ -104,6 +109,7 @@ export function pluginVitestConfig( name: `${pluginId}:backend`, root: pluginDir, environment: "node", + pool, globals: true, setupFiles: [setupFile], include: ["tests/backend/**/*.test.ts"], @@ -115,6 +121,7 @@ export function pluginVitestConfig( name: `${pluginId}:frontend`, root: pluginDir, environment: "jsdom", + pool, globals: true, setupFiles: [setupFile], // Run the SDK through Vite rather than plain Node, so its import diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts index 8da9cb393..ae327d3d0 100644 --- a/src/backend/database/db/index.ts +++ b/src/backend/database/db/index.ts @@ -508,6 +508,7 @@ async function initializeCompleteDatabase(): Promise { installed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, auto_update INTEGER NOT NULL DEFAULT 0, + pinned_version TEXT, manifest_json TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_plugins_registry_id ON plugins (registry_id); @@ -1605,6 +1606,7 @@ const migrateSchema = () => { // Plugin runtime: lastError reports why a plugin is blocked or failed, and // grants record whether a capability came from an admin or from bundling. addColumnIfNotExists("plugins", "last_error", "TEXT"); + addColumnIfNotExists("plugins", "pinned_version", "TEXT"); addColumnIfNotExists( "plugin_permission_grants", "source", diff --git a/src/backend/database/db/schema.mysql.ts b/src/backend/database/db/schema.mysql.ts index 38d6ddcde..24753d1d3 100644 --- a/src/backend/database/db/schema.mysql.ts +++ b/src/backend/database/db/schema.mysql.ts @@ -1005,6 +1005,8 @@ export const plugins = mysqlTable( autoUpdate: boolean("auto_update") .notNull() .default(false), + /** Version an admin pinned. Never auto-updated while set. */ + pinnedVersion: text("pinned_version"), manifestJson: text("manifest_json").notNull(), }, (table) => [index("idx_plugins_registry_id").on(table.registryId)], diff --git a/src/backend/database/db/schema.pg.ts b/src/backend/database/db/schema.pg.ts index 4b5afa23b..58b33c7a7 100644 --- a/src/backend/database/db/schema.pg.ts +++ b/src/backend/database/db/schema.pg.ts @@ -1005,6 +1005,8 @@ export const plugins = pgTable( autoUpdate: boolean("auto_update") .notNull() .default(false), + /** Version an admin pinned. Never auto-updated while set. */ + pinnedVersion: text("pinned_version"), manifestJson: text("manifest_json").notNull(), }, (table) => [index("idx_plugins_registry_id").on(table.registryId)], diff --git a/src/backend/database/db/schema.ts b/src/backend/database/db/schema.ts index 81369756f..ee16a8442 100644 --- a/src/backend/database/db/schema.ts +++ b/src/backend/database/db/schema.ts @@ -1003,6 +1003,8 @@ export const plugins = sqliteTable( autoUpdate: integer("auto_update", { mode: "boolean" }) .notNull() .default(false), + /** Version an admin pinned. Never auto-updated while set. */ + pinnedVersion: text("pinned_version"), manifestJson: text("manifest_json").notNull(), }, (table) => [index("idx_plugins_registry_id").on(table.registryId)], diff --git a/src/backend/database/repositories/plugin-repository.ts b/src/backend/database/repositories/plugin-repository.ts index 7209096c9..46acffc25 100644 --- a/src/backend/database/repositories/plugin-repository.ts +++ b/src/backend/database/repositories/plugin-repository.ts @@ -20,6 +20,7 @@ export interface PluginCreateInput { state?: string; lastError?: string | null; autoUpdate?: boolean; + pinnedVersion?: string | null; manifestJson: string; } @@ -32,6 +33,7 @@ export interface PluginUpdateInput { state?: string; lastError?: string | null; autoUpdate?: boolean; + pinnedVersion?: string | null; manifestJson?: string; } @@ -68,6 +70,7 @@ export class PluginRepository { state: input.state ?? "disabled", lastError: input.lastError ?? null, autoUpdate: input.autoUpdate ?? false, + pinnedVersion: input.pinnedVersion ?? null, manifestJson: input.manifestJson, installedAt: now, updatedAt: now, @@ -101,6 +104,10 @@ export class PluginRepository { lastError: input.lastError === undefined ? existing.lastError : input.lastError, autoUpdate: input.autoUpdate ?? existing.autoUpdate, + pinnedVersion: + input.pinnedVersion === undefined + ? existing.pinnedVersion + : input.pinnedVersion, manifestJson: input.manifestJson ?? existing.manifestJson, updatedAt: now, }, diff --git a/src/backend/database/repositories/plugin-settings-repository.ts b/src/backend/database/repositories/plugin-settings-repository.ts index 7199a5556..71e6ea76d 100644 --- a/src/backend/database/repositories/plugin-settings-repository.ts +++ b/src/backend/database/repositories/plugin-settings-repository.ts @@ -178,6 +178,28 @@ export class PluginSettingsRepository { return affected; } + /** How many stored values the plugin has in each scope. */ + async countByScope( + pluginId: string, + ): Promise> { + const rows = await this.context.drizzle + .select({ scope: pluginSettings.scope }) + .from(pluginSettings) + .where(eq(pluginSettings.pluginId, pluginId)); + + const counts: Record = { + admin: 0, + user: 0, + host: 0, + secret: 0, + }; + for (const row of rows) { + const scope = row.scope as PluginSettingsScope; + if (scope in counts) counts[scope] += 1; + } + return counts; + } + async deleteByPlugin(pluginId: string): Promise { const result = await this.context.drizzle .delete(pluginSettings) diff --git a/src/backend/database/routes/plugins.ts b/src/backend/database/routes/plugins.ts index 675077d38..1bdfeaf1d 100644 --- a/src/backend/database/routes/plugins.ts +++ b/src/backend/database/routes/plugins.ts @@ -17,6 +17,7 @@ import { describePluginFrontend } from "../../plugins/assets.js"; import { invalidatePluginPermissionCache } from "../../plugins/permissions.js"; import { getPluginPublicHttpRoutes } from "../../plugins/http.js"; import { getPluginPublicWsRoutes } from "../../plugins/ws.js"; +import { PluginManageError } from "../../plugins/manage.js"; import { findField, getAllSettings, @@ -247,11 +248,17 @@ router.get("/", authenticateJWT, async (req: Request, res: Response) => { let icon: string | undefined; let dependencies: Record = {}; let optionalDependencies: Record = {}; + let description: string | undefined; + let author: string | undefined; + let repository: string | undefined; try { const manifest = JSON.parse(record.manifestJson) as { contributes?: unknown; capabilities?: unknown; icon?: unknown; + description?: unknown; + author?: unknown; + repository?: unknown; dependencies?: Record; optionalDependencies?: Record; }; @@ -262,6 +269,17 @@ router.get("/", authenticateJWT, async (req: Request, res: Response) => { icon = typeof manifest?.icon === "string" ? manifest.icon : undefined; dependencies = manifest?.dependencies ?? {}; optionalDependencies = manifest?.optionalDependencies ?? {}; + description = + typeof manifest?.description === "string" + ? manifest.description + : undefined; + const rawAuthor = manifest?.author as { name?: unknown } | undefined; + author = + typeof rawAuthor?.name === "string" ? rawAuthor.name : undefined; + repository = + typeof manifest?.repository === "string" + ? manifest.repository + : undefined; } catch { contributes = null; } @@ -285,6 +303,13 @@ router.get("/", authenticateJWT, async (req: Request, res: Response) => { ...summary, tier: record.tier, source: record.source, + registryId: record.registryId ?? null, + autoUpdate: Boolean(record.autoUpdate), + pinnedVersion: record.pinnedVersion ?? null, + description, + author, + repository, + signedBy: loaded?.signedBy ?? null, capabilities, grantedCapabilities: grantsByPlugin.get(record.id) ?? [], lastError: loaded?.lastError ?? record.lastError ?? null, @@ -315,12 +340,70 @@ async function isManagedByLinkedServer(pluginId: string): Promise { return isServerManaged(pluginId); } +async function auditPluginAction( + req: Request, + action: string, + pluginId: string, + details: Record = {}, +): Promise { + const userId = (req as AuthenticatedRequest).userId as string; + const { ipAddress, userAgent } = getRequestMeta(req); + try { + await logAudit({ + userId, + username: await getAuditUsername(userId), + action, + resourceType: "plugin", + resourceId: pluginId, + resourceName: pluginId, + details: JSON.stringify(details), + ipAddress, + userAgent, + success: true, + }); + } catch { + // An audit failure must not undo a change that already happened. + } +} + +function sendManageError( + res: Response, + error: unknown, + fallback: string, + operation: string, +): void { + if (error instanceof PluginManageError) { + res.status(error.status).json({ + error: error.message, + code: error.code, + ...(error.details ?? {}), + }); + return; + } + databaseLogger.error( + fallback, + error instanceof Error ? error : new Error(String(error)), + { operation }, + ); + res.status(500).json({ error: fallback }); +} + +async function refuseOnLinkedDesktop(res: Response): Promise { + const { isLinkedDesktop } = await import("../../plugins/manage.js"); + if (!(await isLinkedDesktop())) return false; + res.status(409).json({ + error: "Plugins on a linked desktop are managed by the server", + code: "MANAGED_BY_SERVER", + }); + return true; +} + /** * @openapi * /plugins/{id}/state: * patch: * summary: Enable or disable a plugin - * description: Persists the new state and starts or stops the plugin immediately. Disabling releases everything the plugin owns, including any port it was listening on. + * description: Persists the new state and starts or stops the plugin immediately. Disabling releases everything the plugin owns, including any port it was listening on. Enabling also starts any installed hard dependency that is off, and disabling also stops the running plugins that depend on it. With dryRun nothing changes and the response lists what would. * tags: * - Plugins * parameters: @@ -329,6 +412,10 @@ async function isManagedByLinkedServer(pluginId: string): Promise { * required: true * schema: * type: string + * - in: query + * name: dryRun + * schema: + * type: boolean * requestBody: * required: true * content: @@ -364,8 +451,7 @@ router.patch( } try { - const repository = createCurrentPluginRepository(); - const record = await repository.findById(pluginId); + const record = await createCurrentPluginRepository().findById(pluginId); if (!record) { res.status(404).json({ error: "Plugin not found" }); return; @@ -380,33 +466,42 @@ router.patch( return; } - // Persist first: if the start or stop below throws, the recorded - // intent still matches what the user asked for, and the next boot - // acts on it rather than silently reverting. - await repository.update(pluginId, { - state: enabled ? "enabled" : "disabled", - lastError: null, - }); + const { planStateChange, setPluginState } = + await import("../../plugins/manage.js"); - const { activatePlugin, deactivatePlugin } = - await import("../../plugins/index.js"); + if (req.query.dryRun === "1" || req.query.dryRun === "true") { + res.json({ + id: pluginId, + enabled, + ...(await planStateChange(pluginId, enabled)), + }); + return; + } - if (enabled) await activatePlugin(pluginId); - else await deactivatePlugin(pluginId); + const result = await setPluginState(pluginId, enabled); + await auditPluginAction( + req, + enabled ? "enable_plugin" : "disable_plugin", + pluginId, + { + enable: result.enable, + disable: result.disable, + }, + ); databaseLogger.info( `Plugin ${pluginId} ${enabled ? "enabled" : "disabled"}`, { operation: "plugin_state_change", pluginId }, ); - res.json({ id: pluginId, enabled }); + res.json({ id: pluginId, enabled, ...result }); } catch (error) { - databaseLogger.error( - `Failed to change state for plugin ${pluginId}`, - error instanceof Error ? error : new Error(String(error)), - { operation: "plugin_state_change" }, + sendManageError( + res, + error, + "Failed to change plugin state", + "plugin_state_change", ); - res.status(500).json({ error: "Failed to change plugin state" }); } }, ); @@ -668,7 +763,7 @@ router.delete( * /plugins/{id}/data: * delete: * summary: Delete everything a plugin stores - * description: Drops every table under the plugin's prefix and clears its key/value state, settings and secrets, capability grants and migration ledger. Role permissions stay. Disabling a plugin never touches its data; this is the explicit uninstall path, and it cannot be undone. The plugin is deactivated first so nothing is writing while its tables go. + * description: Drops every table under the plugin's prefix and clears its key/value state, settings and secrets, migration ledger and files. Role permissions stay, and the capabilities the admin agreed to are granted again. The plugin stays installed but is switched off, so nothing is writing while its tables go. It cannot be undone. * tags: * - Plugins * parameters: @@ -700,19 +795,18 @@ router.delete( return; } - // Stopped first: dropping a table out from under a running plugin turns - // its next query into an error rather than a clean shutdown. - const { loader } = getPluginRuntime(); - if (loader.get(pluginId)?.state === "active") { - await loader.deactivate(pluginId); + if (await isManagedByLinkedServer(pluginId)) { + res.status(409).json({ + error: "Managed by the linked server", + code: "MANAGED_BY_SERVER", + }); + return; } - const { removePluginData } = await import("../../plugins/data.js"); - const removed = await removePluginData(pluginId, { - knownPluginIds: getPluginRuntime() - .loader.list() - .map((plugin) => plugin.id), - }); + // The plugin is stopped first: dropping a table out from under a + // running plugin turns its next query into an error. + const { deletePluginData } = await import("../../plugins/manage.js"); + const removed = await deletePluginData(pluginId, userId ?? null); databaseLogger.warn(`Removed all data for plugin ${pluginId}`, { operation: "plugin_remove_data", @@ -721,6 +815,10 @@ router.delete( tables: removed.tables.length, kvKeys: removed.kvKeys, }); + await auditPluginAction(req, "delete_plugin_data", pluginId, { + tables: removed.tables.length, + kvKeys: removed.kvKeys, + }); res.json({ id: pluginId, removed }); } catch (error) { @@ -1299,4 +1397,414 @@ router.put( }, ); +const ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/; + +function readVersion(body: unknown): string | undefined | null { + const version = (body as { version?: unknown } | undefined)?.version; + if (version === undefined || version === null || version === "") { + return undefined; + } + return typeof version === "string" && version.length <= 64 ? version : null; +} + +/** + * @openapi + * /plugins/registry: + * get: + * summary: List the plugins in the official registry + * description: > + * Fetches the official registry index (cached for 15 minutes unless + * refresh is set) and merges it with what is installed here: the + * installed version, the newest release this build can run, whether an + * update asks for new capabilities, and the pin and auto-update choices. + * tags: + * - Plugins + * parameters: + * - in: query + * name: refresh + * schema: + * type: boolean + * responses: + * 200: + * description: The registry listing. + * 403: + * description: The caller lacks admin.plugins.manage. + * 502: + * description: The registry could not be reached. + */ +router.get( + "/registry", + authenticateJWT, + requireManagePlugins, + async (req: Request, res: Response) => { + try { + const { listRegistry } = await import("../../plugins/manage.js"); + const force = req.query.refresh === "1" || req.query.refresh === "true"; + res.json(await listRegistry({ force })); + } catch (error) { + databaseLogger.warn("Plugin registry fetch failed", { + operation: "plugin_registry", + error: error instanceof Error ? error.message : String(error), + }); + res.status(502).json({ + error: "Could not reach the plugin registry", + code: "REGISTRY_UNREACHABLE", + }); + } + }, +); + +/** + * @openapi + * /plugins/update-all: + * post: + * summary: Update every plugin that has a newer release + * description: Skips pinned plugins and any update that asks for new capabilities, which are returned under needsReview for the admin to look at one by one. + * tags: + * - Plugins + * responses: + * 200: + * description: What was updated, what needs review and what failed. + * 403: + * description: The caller lacks admin.plugins.manage. + * 409: + * description: This desktop is linked to a server, which manages its plugins. + */ +router.post( + "/update-all", + authenticateJWT, + requireManagePlugins, + async (req: Request, res: Response) => { + if (await refuseOnLinkedDesktop(res)) return; + try { + const { updateAllPlugins } = await import("../../plugins/manage.js"); + const userId = (req as AuthenticatedRequest).userId as string; + const result = await updateAllPlugins({ userId }); + for (const done of result.updated) { + await auditPluginAction(req, "update_plugin", done.id, { + version: done.version, + }); + } + res.json(result); + } catch (error) { + sendManageError(res, error, "Failed to update plugins", "plugin_update"); + } + }, +); + +/** + * @openapi + * /plugins/{id}/install: + * post: + * summary: Install a plugin from the official registry + * description: > + * Downloads the release, checks its sha256 and signature against a key + * this build trusts, loads it, grants the capabilities it declares and + * starts it along with any installed dependency that is off. Without a + * version the newest compatible release is used; naming an older one + * pins the plugin there. A bundled plugin at its shipped version needs + * no download. + * tags: + * - Plugins + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * version: + * type: string + * responses: + * 200: + * description: The installed version and the plugin's state. + * 400: + * description: Invalid id or version. + * 403: + * description: The caller lacks admin.plugins.manage. + * 404: + * description: Not in the official registry. + * 409: + * description: Already installed, incompatible, or managed by a linked server. + * 502: + * description: The registry could not be reached. + */ +router.post( + "/:id/install", + authenticateJWT, + requireManagePlugins, + async (req: Request, res: Response) => { + const pluginId = String(req.params.id); + const version = readVersion(req.body); + if (!ID_PATTERN.test(pluginId) || version === null) { + res.status(400).json({ error: "Invalid plugin id or version" }); + return; + } + if (await refuseOnLinkedDesktop(res)) return; + try { + const { installPlugin } = await import("../../plugins/manage.js"); + const userId = (req as AuthenticatedRequest).userId as string; + const result = await installPlugin(pluginId, { version, userId }); + await auditPluginAction(req, "install_plugin", pluginId, { + version: result.version, + }); + res.json(result); + } catch (error) { + sendManageError( + res, + error, + "Failed to install the plugin", + "plugin_install", + ); + } + }, +); + +/** + * @openapi + * /plugins/{id}/update: + * post: + * summary: Update or change the installed version of a plugin + * description: > + * Swaps in another release from the official registry, newer or older. + * If the release declares capabilities the installed one does not, the + * request must set acceptCapabilities, otherwise it answers 409 with + * code CAPABILITIES_ADDED and the list. A running plugin is started + * again on the new version. Naming a version that is not the newest + * pins the plugin to it. + * tags: + * - Plugins + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * version: + * type: string + * acceptCapabilities: + * type: boolean + * responses: + * 200: + * description: The new version and the plugin's state. + * 400: + * description: Invalid id or version. + * 403: + * description: The caller lacks admin.plugins.manage. + * 404: + * description: Not installed, or the version is not in the registry. + * 409: + * description: Same version, new capabilities need consent, or managed by a linked server. + */ +router.post( + "/:id/update", + authenticateJWT, + requireManagePlugins, + async (req: Request, res: Response) => { + const pluginId = String(req.params.id); + const version = readVersion(req.body); + if (!ID_PATTERN.test(pluginId) || version === null) { + res.status(400).json({ error: "Invalid plugin id or version" }); + return; + } + if (await refuseOnLinkedDesktop(res)) return; + try { + const { updatePlugin } = await import("../../plugins/manage.js"); + const userId = (req as AuthenticatedRequest).userId as string; + const result = await updatePlugin(pluginId, { + version, + userId, + acceptCapabilities: req.body?.acceptCapabilities === true, + }); + await auditPluginAction(req, "update_plugin", pluginId, { + version: result.version, + }); + res.json(result); + } catch (error) { + sendManageError( + res, + error, + "Failed to update the plugin", + "plugin_update", + ); + } + }, +); + +/** + * @openapi + * /plugins/{id}: + * delete: + * summary: Uninstall a plugin + * description: > + * Stops the plugin, drops its tables, settings, secrets, grants and + * files, and removes it. A plugin that ships with Termix keeps its + * files in the image but is marked uninstalled and never loads again + * until it is installed from the Plugins tab. Refused while a running + * plugin depends on it. + * tags: + * - Plugins + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: What was removed. + * 403: + * description: The caller lacks admin.plugins.manage. + * 404: + * description: No such plugin. + * 409: + * description: Other running plugins depend on it, or managed by a linked server. + */ +router.delete( + "/:id", + authenticateJWT, + requireManagePlugins, + async (req: Request, res: Response) => { + const pluginId = String(req.params.id); + if (!ID_PATTERN.test(pluginId)) { + res.status(400).json({ error: "Invalid plugin id" }); + return; + } + if (await refuseOnLinkedDesktop(res)) return; + try { + const { uninstallPlugin } = await import("../../plugins/manage.js"); + const result = await uninstallPlugin(pluginId); + await auditPluginAction(req, "uninstall_plugin", pluginId, { + tables: result.removed.tables.length, + }); + res.json(result); + } catch (error) { + sendManageError( + res, + error, + "Failed to uninstall the plugin", + "plugin_uninstall", + ); + } + }, +); + +/** + * @openapi + * /plugins/{id}/options: + * patch: + * summary: Change a plugin's update choices + * description: autoUpdate lets the background check apply new releases that ask for nothing new. pinned holds the plugin at its installed version, which also stops auto-update. + * tags: + * - Plugins + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * autoUpdate: + * type: boolean + * pinned: + * type: boolean + * responses: + * 200: + * description: The saved choices. + * 400: + * description: Invalid request body. + * 403: + * description: The caller lacks admin.plugins.manage. + * 404: + * description: No such plugin. + */ +router.patch( + "/:id/options", + authenticateJWT, + requireManagePlugins, + async (req: Request, res: Response) => { + const pluginId = String(req.params.id); + const { autoUpdate, pinned } = req.body ?? {}; + if ( + (autoUpdate !== undefined && typeof autoUpdate !== "boolean") || + (pinned !== undefined && typeof pinned !== "boolean") + ) { + res.status(400).json({ error: "autoUpdate and pinned must be booleans" }); + return; + } + try { + const { setPluginOptions } = await import("../../plugins/manage.js"); + const result = await setPluginOptions(pluginId, { autoUpdate, pinned }); + await auditPluginAction(req, "update_plugin_options", pluginId, result); + res.json({ id: pluginId, ...result }); + } catch (error) { + sendManageError( + res, + error, + "Failed to save plugin options", + "plugin_options", + ); + } + }, +); + +/** + * @openapi + * /plugins/{id}/data: + * get: + * summary: Summarize what a plugin stores + * description: The plugin's tables with row counts, key/value count, stored settings per scope, applied migrations, capability grants and the size of its files folder. No values are returned. + * tags: + * - Plugins + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The summary. + * 403: + * description: The caller lacks admin.plugins.manage. + * 404: + * description: No such plugin. + */ +router.get( + "/:id/data", + authenticateJWT, + requireManagePlugins, + async (req: Request, res: Response) => { + const pluginId = String(req.params.id); + try { + const { getPluginDataSummary } = await import("../../plugins/manage.js"); + res.json({ id: pluginId, ...(await getPluginDataSummary(pluginId)) }); + } catch (error) { + sendManageError( + res, + error, + "Failed to read the plugin's data", + "plugin_data", + ); + } + }, +); + export default router; diff --git a/src/backend/plugins/auto-update.ts b/src/backend/plugins/auto-update.ts new file mode 100644 index 000000000..f04dfcfb5 --- /dev/null +++ b/src/backend/plugins/auto-update.ts @@ -0,0 +1,64 @@ +/** + * Applies plugin updates on a timer for plugins with auto-update on. An + * update that asks for a new capability is never applied here; admins get + * an alert and review it in the Plugins tab. + */ + +import { pluginLogger } from "../utils/logger.js"; + +const FIRST_CHECK_MS = 2 * 60_000; +const CHECK_EVERY_MS = 6 * 60 * 60_000; + +let timer: NodeJS.Timeout | null = null; + +export async function runPluginAutoUpdate(): Promise { + const { isLinkedDesktop, updateAllPlugins } = await import("./manage.js"); + if (await isLinkedDesktop()) return; + + const result = await updateAllPlugins({ userId: null, onlyAutoUpdate: true }); + + for (const done of result.updated) { + pluginLogger.info(`Auto-updated plugin ${done.id} to ${done.version}`, { + operation: "plugin_auto_update", + }); + } + for (const failed of result.failed) { + pluginLogger.warn(`Auto-update of ${failed.id} failed: ${failed.error}`, { + operation: "plugin_auto_update", + }); + } + if (result.needsReview.length === 0) return; + + const { sendCoreAlert } = await import("../notify/core-notify.js"); + for (const entry of result.needsReview) { + await sendCoreAlert({ + title: `A plugin update needs review`, + body: `The new version of ${entry.id} asks for more access, so it was not installed on its own. Review it in Plugins.`, + severity: "info", + category: "termix.plugin-update", + dedupeKey: `plugin-review:${entry.id}:${entry.capabilities.join(",")}`, + audience: "admins", + }); + } +} + +export function startPluginAutoUpdate(): void { + if (timer) return; + const run = () => + void runPluginAutoUpdate().catch((error) => { + pluginLogger.warn( + `Plugin auto-update check failed: ${ + error instanceof Error ? error.message : String(error) + }`, + { operation: "plugin_auto_update" }, + ); + }); + setTimeout(run, FIRST_CHECK_MS).unref?.(); + timer = setInterval(run, CHECK_EVERY_MS); + timer.unref?.(); +} + +export function stopPluginAutoUpdate(): void { + if (timer) clearInterval(timer); + timer = null; +} diff --git a/src/backend/plugins/data.ts b/src/backend/plugins/data.ts index dc1fc8e66..396f29d21 100644 --- a/src/backend/plugins/data.ts +++ b/src/backend/plugins/data.ts @@ -263,6 +263,34 @@ export async function removePluginData( return { tables: dropped, kvKeys, migrations }; } +/** The plugin's tables in the live database, with how many rows each holds. */ +export async function describePluginTables( + pluginId: string, + options: { knownPluginIds?: string[]; dialect?: DatabaseDialect } = {}, +): Promise> { + const dialect = options.dialect ?? resolveDatabaseDialect(); + const tables = ownedTableNames( + pluginId, + await listDatabaseTables(dialect), + options.knownPluginIds ?? [], + ).sort(); + + const { selectRows } = await import("../utils/crypto-migration/raw-rows.js"); + const result: Array<{ name: string; rows: number | null }> = []; + for (const table of tables) { + const quoted = dialect === "mysql" ? "`" + table + "`" : `"${table}"`; + try { + const [row] = await selectRows>( + sql.raw(`SELECT COUNT(*) AS count FROM ${quoted}`), + ); + result.push({ name: table, rows: Number(row?.count ?? row?.COUNT ?? 0) }); + } catch { + result.push({ name: table, rows: null }); + } + } + return result; +} + /** * The tables in `all` that belong to this plugin: its prefix, minus any that * sit under a longer prefix another plugin owns. diff --git a/src/backend/plugins/fs-retry.ts b/src/backend/plugins/fs-retry.ts new file mode 100644 index 000000000..4608df972 --- /dev/null +++ b/src/backend/plugins/fs-retry.ts @@ -0,0 +1,42 @@ +import fs from "node:fs"; + +// Windows refuses a rename while anything holds a file open, and antivirus +// or the search indexer often does for a moment right after files are +// written. These are worth waiting out. +const TRANSIENT = new Set(["EPERM", "EACCES", "EBUSY"]); +const DELAYS_MS = [50, 100, 200, 400, 800, 1600]; + +function isTransient(error: unknown): boolean { + return TRANSIENT.has((error as NodeJS.ErrnoException)?.code ?? ""); +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +/** + * Renames, retrying while the error looks like a passing lock. A directory + * that still cannot be renamed is copied and the source removed instead. + */ +export async function moveWithRetry( + from: string, + to: string, + delays: readonly number[] = DELAYS_MS, +): Promise { + let lastError: unknown; + for (let attempt = 0; attempt <= delays.length; attempt++) { + try { + await fs.promises.rename(from, to); + return; + } catch (error) { + if (!isTransient(error)) throw error; + lastError = error; + if (attempt < delays.length) await sleep(delays[attempt]); + } + } + + const stat = await fs.promises.stat(from).catch(() => null); + if (!stat) throw lastError; + await fs.promises.cp(from, to, { recursive: true, force: true }); + await fs.promises + .rm(from, { recursive: true, force: true, maxRetries: 5 }) + .catch(() => {}); +} diff --git a/src/backend/plugins/index.ts b/src/backend/plugins/index.ts index 05cee58c0..65ba20bc5 100644 --- a/src/backend/plugins/index.ts +++ b/src/backend/plugins/index.ts @@ -215,6 +215,9 @@ async function syncCapabilityGrants(loaded: LoadedPlugin[]): Promise { export async function initializePlugins(): Promise { const { loader: pluginLoader } = getPluginRuntime(); + const { applyStoredPluginChoices } = await import("./manage.js"); + await applyStoredPluginChoices(pluginLoader); + const loaded = await pluginLoader.loadAll(); if (loaded.length === 0) return []; @@ -567,6 +570,14 @@ export async function installPluginArtifact( return plugin; } +/** Gives freshly loaded plugins a row and their bundled grants. */ +export async function registerLoadedPlugins( + loaded: LoadedPlugin[], +): Promise { + await seedPlugins(loaded); + await syncCapabilityGrants(loaded); +} + /** Stops a plugin and forgets it, before a new version is installed. */ export async function unloadPlugin(pluginId: string): Promise { const { loader: pluginLoader } = getPluginRuntime(); diff --git a/src/backend/plugins/loader.ts b/src/backend/plugins/loader.ts index 97c5d81bb..2f05638ce 100644 --- a/src/backend/plugins/loader.ts +++ b/src/backend/plugins/loader.ts @@ -29,6 +29,7 @@ import { getUnpackedPluginsDir, } from "./paths.js"; import { requireSignedPlugins, verifyPluginArtifact } from "./trust.js"; +import { moveWithRetry } from "./fs-retry.js"; import { createPluginContext, createPluginHandle, @@ -90,6 +91,14 @@ export class PluginLoader { private readonly bundledVersions = new Map(); /** Activation order, so shutdown can run it backwards. */ private activationOrder: string[] = []; + /** + * Bundled plugins an admin uninstalled. The shipped copy is ignored as if + * it were not there, so a reinstall from the registry loads as a normal + * download and a recreated image cannot bring the old copy back. + */ + private uninstalled = new Set(); + /** Versions an admin pinned, which may sit below the bundled copy. */ + private pinned = new Map(); constructor(private readonly options: PluginLoaderOptions = {}) {} @@ -111,6 +120,33 @@ export class PluginLoader { ]); } + setUninstalled(ids: Iterable): void { + this.uninstalled = new Set(ids); + for (const id of this.uninstalled) this.bundledVersions.delete(id); + } + + setPinned(pins: Map): void { + this.pinned = new Map(pins); + } + + /** Version of the copy that ships in the image, loaded or not. */ + bundledVersion(pluginId: string): string | undefined { + return this.bundledVersions.get(pluginId); + } + + /** Loads the shipped copy of a bundled plugin, e.g. after a reinstall. */ + async loadBundled(pluginId: string): Promise { + if (!PLUGIN_ID_PATTERN.test(pluginId)) { + throw new Error(`invalid plugin id "${pluginId}"`); + } + const plugin = await this.load( + path.join(getBundledPluginsDir(), pluginId), + "bundled", + ); + this.bundledVersions.set(plugin.id, plugin.manifest.version); + return plugin; + } + /** Drops a stopped plugin from the list, so a new copy can load in its place. */ forget(pluginId: string): void { const plugin = this.plugins.get(pluginId); @@ -231,7 +267,10 @@ export class PluginLoader { const entries = await fs.promises.readdir(root, { withFileTypes: true }); if (source === "bundled") { for (const entry of entries) { - if (entry.isDirectory()) bundledIds.add(entry.name); + if (!entry.isDirectory() || this.uninstalled.has(entry.name)) { + continue; + } + bundledIds.add(entry.name); } } @@ -244,6 +283,7 @@ export class PluginLoader { if (entry.isDirectory() && entry.name.startsWith(".")) continue; const dir = path.join(root, entry.name); + if (source === "bundled" && this.uninstalled.has(entry.name)) continue; try { if (isArtifact) { const plugin = await this.loadArtifact(dir, bundledIds); @@ -355,6 +395,7 @@ export class PluginLoader { raw?.version, signedBy, this.bundledVersions.get(id) ?? replaced?.manifest.version, + this.pinned.get(id), ); } else if (this.plugins.has(id)) { throw new Error(`another plugin already uses the id "${id}"`); @@ -364,8 +405,12 @@ export class PluginLoader { } const target = path.join(unpackedRoot, id); - await fs.promises.rm(target, { recursive: true, force: true }); - await fs.promises.rename(staging, target); + await fs.promises.rm(target, { + recursive: true, + force: true, + maxRetries: 5, + }); + await moveWithRetry(staging, target); if (replaced) this.plugins.delete(id); try { @@ -547,8 +592,10 @@ export class PluginLoader { // than leave it half-running against a schema that is not there. A // throw here lands in the catch below, which fails this plugin only. const { migratePlugin } = await import("./data.js"); + // A release signed by a trusted key is official whether it shipped in + // the image or was downloaded, so it may adopt its 2.8 tables too. const applied = await migratePlugin(plugin.id, plugin.dir, { - bundled: plugin.source === "bundled", + bundled: plugin.source === "bundled" || Boolean(plugin.signedBy), }); // A new table may be where core data is waiting to move. if (applied.length > 0) { @@ -776,6 +823,7 @@ export function assertBundledUpdate( version: unknown, signedBy: string | undefined, bundledVersion: string | undefined, + pinnedVersion?: string, ): void { if (!signedBy) { throw new Error( @@ -785,6 +833,8 @@ export function assertBundledUpdate( if (typeof version !== "string" || !semver.valid(version)) { throw new Error(`update for "${id}" has no valid version`); } + // An admin pinning an exact older release is the one allowed downgrade. + if (pinnedVersion && pinnedVersion === version) return; if (bundledVersion && !semver.gt(version, bundledVersion)) { throw new Error( `update for "${id}" is ${version}, which is not newer than the bundled ${bundledVersion}`, diff --git a/src/backend/plugins/manage.ts b/src/backend/plugins/manage.ts new file mode 100644 index 000000000..278a5c126 --- /dev/null +++ b/src/backend/plugins/manage.ts @@ -0,0 +1,1005 @@ +/** + * Installing, updating, uninstalling and switching plugins at runtime. + * + * Only the official registry is a source for now. Bundled plugins ship in the + * image and can still be uninstalled: their files are deleted and the id is + * recorded in the settings table, so the shipped copy is ignored from then + * on and a reinstall downloads the plugin like any other. + */ + +import fs from "node:fs"; +import path from "node:path"; +import semver from "semver"; +import { pluginLogger } from "../utils/logger.js"; +import { + activatePlugin, + deactivatePlugin, + getPluginRuntime, + registerLoadedPlugins, + unloadPlugin, +} from "./index.js"; +import type { LoadedPlugin, PluginLoader } from "./loader.js"; +import { + getBundledPluginsDir, + getPluginDataDir, + getPluginsDir, + getUnpackedPluginsDir, +} from "./paths.js"; +import { + OFFICIAL_REGISTRY_ID, + downloadRelease, + fetchRegistryIndex, + findRelease, + getDownloadsDir, + getRegistryStatus, + isApiCompatible, + type RegistryIndex, + type RegistryVersion, +} from "./registry-index.js"; +import { invalidatePluginPermissionCache } from "./permissions.js"; +import { moveWithRetry } from "./fs-retry.js"; + +const UNINSTALLED_KEY = "plugins_uninstalled"; + +export class PluginManageError extends Error { + constructor( + message: string, + readonly status: number, + readonly code: string, + readonly details?: Record, + ) { + super(message); + } +} + +let queue: Promise = Promise.resolve(); + +/** One change at a time: dependencies make concurrent changes unsafe. */ +function serialized(fn: () => Promise): Promise { + const run = queue.then(fn, fn); + queue = run.catch(() => {}); + return run; +} + +async function repos() { + return import("../database/repositories/factory.js"); +} + +export async function readUninstalled(): Promise> { + const { createCurrentSettingsRepository } = await repos(); + const raw = await createCurrentSettingsRepository().get(UNINSTALLED_KEY); + if (!raw) return new Set(); + try { + const parsed = JSON.parse(raw); + return new Set( + Array.isArray(parsed) + ? parsed.filter((id): id is string => typeof id === "string") + : [], + ); + } catch { + return new Set(); + } +} + +async function writeUninstalled(ids: Set): Promise { + const { createCurrentSettingsRepository } = await repos(); + await createCurrentSettingsRepository().set( + UNINSTALLED_KEY, + JSON.stringify([...ids].sort()), + ); + getPluginRuntime().loader.setUninstalled(ids); +} + +async function readPins(): Promise> { + const { createCurrentPluginRepository } = await repos(); + const pins = new Map(); + for (const record of await createCurrentPluginRepository().listAll()) { + if (record.pinnedVersion) pins.set(record.id, record.pinnedVersion); + } + return pins; +} + +/** Called once before the boot scan, so it honours uninstalls and pins. */ +export async function applyStoredPluginChoices( + loader: PluginLoader, +): Promise { + try { + loader.setUninstalled(await readUninstalled()); + loader.setPinned(await readPins()); + } catch (error) { + pluginLogger.warn( + `Could not read stored plugin choices: ${ + error instanceof Error ? error.message : String(error) + }`, + { operation: "plugin_init" }, + ); + } +} + +const ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/; + +function isBundledOnDisk(pluginId: string): boolean { + return fs.existsSync( + path.join(getBundledPluginsDir(), pluginId, "manifest.json"), + ); +} + +/** Whether `target` is a folder strictly inside `root`. */ +function isStrictlyInside(root: string, target: string): boolean { + const relative = path.relative(path.resolve(root), path.resolve(target)); + return ( + relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative) + ); +} + +/** A plugin dropped in as a plain folder under the plugins directory. */ +async function removeUserFolder( + pluginId: string, + live: LoadedPlugin | undefined, +): Promise { + const root = getPluginsDir(); + const candidates = new Set([path.join(root, pluginId)]); + if (live?.source === "user" && !live.artifact) candidates.add(live.dir); + for (const dir of candidates) { + if (!isStrictlyInside(root, dir)) continue; + await fs.promises.rm(dir, { recursive: true, force: true }); + } +} + +/** + * Deletes the copy that shipped in the image. A read-only install keeps the + * files, which is fine: the id is on the ignore list either way. + */ +async function removeShippedCopy(pluginId: string): Promise { + const root = getBundledPluginsDir(); + const dir = path.join(root, pluginId); + if (!isStrictlyInside(root, dir)) return; + try { + await fs.promises.rm(dir, { recursive: true, force: true }); + } catch (error) { + pluginLogger.warn( + `Could not delete the shipped copy of ${pluginId}, it stays ignored: ${ + error instanceof Error ? error.message : String(error) + }`, + { operation: "plugin_uninstall" }, + ); + } +} + +export async function isLinkedDesktop(): Promise { + if (process.env.ELECTRON_EMBEDDED !== "true") return false; + const { getLink } = await import("../sync/client/link-store.js"); + return Boolean(await getLink()); +} + +function latestCompatible(versions: RegistryVersion[]): RegistryVersion | null { + return versions.find((entry) => isApiCompatible(entry.api)) ?? null; +} + +function addedCapabilities( + current: readonly string[], + next: readonly string[], +): string[] { + const have = new Set(current); + return next.filter((capability) => !have.has(capability)); +} + +export interface RegistryListing { + registry: ReturnType & { id: string }; + /** A linked desktop runs what its server runs, so nothing here can change. */ + managedByServer: boolean; + plugins: Array<{ + id: string; + name: string; + description: string; + author: string; + category: string; + repository?: string; + icon?: string; + versions: Array<{ + version: string; + compatible: boolean; + capabilities: string[]; + publishedAt?: string; + releaseNotesUrl?: string; + size: number; + }>; + latestVersion: string | null; + installed: boolean; + installedVersion: string | null; + updateAvailable: boolean; + addedCapabilities: string[]; + pinnedVersion: string | null; + autoUpdate: boolean; + bundled: boolean; + }>; +} + +export async function listRegistry( + options: { force?: boolean } = {}, +): Promise { + const index = await fetchRegistryIndex(options); + const { createCurrentPluginRepository } = await repos(); + const records = new Map( + (await createCurrentPluginRepository().listAll()).map((r) => [r.id, r]), + ); + const { loader } = getPluginRuntime(); + const shipped = loader.bundledIds(); + + return { + registry: { id: OFFICIAL_REGISTRY_ID, ...getRegistryStatus() }, + managedByServer: await isLinkedDesktop(), + plugins: index.plugins.map((plugin) => { + const record = records.get(plugin.id); + const live = loader.get(plugin.id); + const latest = latestCompatible(plugin.versions); + const installedVersion = + live?.manifest.version ?? record?.version ?? null; + const updateAvailable = Boolean( + installedVersion && + latest && + semver.valid(installedVersion) && + semver.gt(latest.version, installedVersion), + ); + return { + id: plugin.id, + name: plugin.name, + description: plugin.description, + author: plugin.author, + category: plugin.category, + repository: plugin.repository, + icon: plugin.icon, + versions: plugin.versions.map((entry) => ({ + version: entry.version, + compatible: isApiCompatible(entry.api), + capabilities: entry.capabilities, + publishedAt: entry.publishedAt, + releaseNotesUrl: entry.releaseNotesUrl, + size: entry.size, + })), + latestVersion: latest?.version ?? null, + installed: Boolean(record), + installedVersion: record ? installedVersion : null, + updateAvailable: Boolean(record) && updateAvailable, + addedCapabilities: + updateAvailable && latest + ? addedCapabilities( + live?.manifest.capabilities ?? [], + latest.capabilities, + ) + : [], + pinnedVersion: record?.pinnedVersion ?? null, + autoUpdate: Boolean(record?.autoUpdate), + bundled: shipped.has(plugin.id), + }; + }), + }; +} + +async function removeArtifactFiles( + pluginId: string, + extra?: string, +): Promise { + const candidates = new Set([ + path.join(getPluginsDir(), `${pluginId}.tmxplug`), + ]); + if (extra) candidates.add(extra); + for (const file of candidates) { + await fs.promises.rm(file, { force: true }); + await fs.promises.rm(`${file}.sig`, { force: true }); + } +} + +/** Grants every capability the manifest declares, after the admin agreed. */ +async function grantDeclared( + plugin: LoadedPlugin, + userId: string | null, +): Promise { + if (plugin.source === "bundled") return; + const { createCurrentPluginPermissionGrantRepository } = await repos(); + const repository = createCurrentPluginPermissionGrantRepository(); + const granted = new Set( + (await repository.listByPlugin(plugin.id)).map((g) => g.capability), + ); + for (const capability of plugin.manifest.capabilities) { + if (granted.has(capability)) continue; + await repository.grant({ + pluginId: plugin.id, + capability, + grantedBy: userId, + source: "admin", + }); + } + invalidatePluginPermissionCache(plugin.id); +} + +/** + * Puts `release` in place of whatever copy of the plugin is loaded. On a + * failed load the previous copy comes back. + */ +async function swapIn( + pluginId: string, + release: RegistryVersion, +): Promise { + const { loader } = getPluginRuntime(); + const previous = loader.get(pluginId); + const bundledVersion = loader.bundledVersion(pluginId); + const useBundled = + bundledVersion !== undefined && + bundledVersion === release.version && + isBundledOnDisk(pluginId); + + // Fetched before anything stops, so a bad download changes nothing. + const staged = useBundled ? null : await downloadRelease(pluginId, release); + + const finalFile = path.join(getPluginsDir(), `${pluginId}.tmxplug`); + const backupDir = path.join(getDownloadsDir(), `${pluginId}.previous`); + const previousArtifact = previous?.artifact; + + await unloadPlugin(pluginId); + + if (previousArtifact && fs.existsSync(previousArtifact)) { + await fs.promises.mkdir(backupDir, { recursive: true }); + await fs.promises.copyFile( + previousArtifact, + path.join(backupDir, "plugin.tmxplug"), + ); + if (fs.existsSync(`${previousArtifact}.sig`)) { + await fs.promises.copyFile( + `${previousArtifact}.sig`, + path.join(backupDir, "plugin.tmxplug.sig"), + ); + } + } + + try { + await removeArtifactFiles(pluginId, previousArtifact); + let plugin: LoadedPlugin; + if (staged) { + await fs.promises.mkdir(getPluginsDir(), { recursive: true }); + await moveWithRetry(staged.file, finalFile); + if (fs.existsSync(staged.signatureFile)) { + await moveWithRetry(staged.signatureFile, `${finalFile}.sig`); + } + plugin = await loader.loadArtifact(finalFile, loader.bundledIds()); + } else { + plugin = await loader.loadBundled(pluginId); + } + await registerLoadedPlugins([plugin]); + return plugin; + } catch (error) { + await removeArtifactFiles(pluginId); + await restorePrevious(pluginId, previousArtifact, backupDir); + throw error; + } finally { + await fs.promises.rm(backupDir, { recursive: true, force: true }); + if (staged) { + await fs.promises.rm(staged.file, { force: true }); + await fs.promises.rm(staged.signatureFile, { force: true }); + } + } +} + +async function restorePrevious( + pluginId: string, + previousArtifact: string | undefined, + backupDir: string, +): Promise { + const { loader } = getPluginRuntime(); + if (loader.get(pluginId)) return; + try { + const backup = path.join(backupDir, "plugin.tmxplug"); + if (previousArtifact && fs.existsSync(backup)) { + await fs.promises.copyFile(backup, previousArtifact); + if (fs.existsSync(`${backup}.sig`)) { + await fs.promises.copyFile(`${backup}.sig`, `${previousArtifact}.sig`); + } + await loader.loadArtifact(previousArtifact, loader.bundledIds()); + } else if ( + loader.bundledVersion(pluginId) !== undefined && + isBundledOnDisk(pluginId) + ) { + await loader.loadBundled(pluginId); + } + } catch (error) { + pluginLogger.error( + `Could not restore the previous copy of ${pluginId}`, + error instanceof Error ? error : new Error(String(error)), + { operation: "plugin_install" }, + ); + } +} + +async function recordState( + pluginId: string, + state: string, + lastError: string | null, +): Promise { + const { createCurrentPluginRepository } = await repos(); + await createCurrentPluginRepository().update(pluginId, { state, lastError }); +} + +/** Starts a plugin and records how it went. Never throws on a failed start. */ +async function startAndRecord(pluginId: string): Promise { + try { + await recordState(pluginId, "enabled", null); + await activatePlugin(pluginId); + return "active"; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + await recordState(pluginId, "failed", message); + return "failed"; + } +} + +async function enabledIds(): Promise> { + const { createCurrentPluginRepository } = await repos(); + return new Set( + (await createCurrentPluginRepository().listAll()) + .filter((record) => record.state === "enabled") + .map((record) => record.id), + ); +} + +/** Hard dependencies to start first, deepest first. */ +function dependencyChain(pluginId: string): { + order: string[]; + missing: string[]; +} { + const { loader } = getPluginRuntime(); + const order: string[] = []; + const missing: string[] = []; + const seen = new Set(); + const visit = (id: string) => { + if (seen.has(id)) return; + seen.add(id); + const plugin = loader.get(id); + if (!plugin) { + missing.push(id); + return; + } + for (const [dependency, range] of Object.entries( + plugin.manifest.dependencies ?? {}, + )) { + const found = loader.get(dependency); + if (found && !semver.satisfies(found.manifest.version, range)) { + missing.push(`${dependency}@${range}`); + continue; + } + visit(dependency); + } + if (id !== pluginId) order.push(id); + }; + visit(pluginId); + return { order, missing }; +} + +/** Running plugins that hard-depend on this one, outermost first. */ +function dependentChain(pluginId: string, enabled: Set): string[] { + const { loader } = getPluginRuntime(); + const result: string[] = []; + const visit = (id: string) => { + for (const plugin of loader.list()) { + if (!enabled.has(plugin.id) || result.includes(plugin.id)) continue; + if (!(id in (plugin.manifest.dependencies ?? {}))) continue; + visit(plugin.id); + result.push(plugin.id); + } + }; + visit(pluginId); + return result; +} + +export interface StateChangePlan { + enable: string[]; + disable: string[]; + missing: string[]; +} + +export async function planStateChange( + pluginId: string, + enabled: boolean, +): Promise { + const current = await enabledIds(); + if (enabled) { + const { order, missing } = dependencyChain(pluginId); + return { + enable: order.filter((id) => !current.has(id)), + disable: [], + missing, + }; + } + return { + enable: [], + disable: dependentChain(pluginId, current), + missing: [], + }; +} + +export function setPluginState( + pluginId: string, + enabled: boolean, +): Promise { + return serialized(async () => { + const { loader } = getPluginRuntime(); + if (!loader.get(pluginId)) { + throw new PluginManageError("Plugin not found", 404, "NOT_FOUND"); + } + const plan = await planStateChange(pluginId, enabled); + + if (enabled) { + if (plan.missing.length > 0) { + throw new PluginManageError( + "Some dependencies are not installed", + 409, + "MISSING_DEPENDENCIES", + { missing: plan.missing }, + ); + } + for (const id of plan.enable) { + if ((await startAndRecord(id)) === "failed") { + await recordState( + pluginId, + "blocked", + `requires plugin "${id}", which could not start`, + ); + return { ...plan, state: "blocked" }; + } + } + return { ...plan, state: await startAndRecord(pluginId) }; + } + + for (const id of [...plan.disable, pluginId]) { + await recordState(id, "disabled", null); + await deactivatePlugin(id); + } + return { ...plan, state: "stopped" }; + }); +} + +export interface InstallOptions { + version?: string; + userId: string | null; + acceptCapabilities?: boolean; +} + +async function requireRelease( + pluginId: string, + version: string | undefined, +): Promise<{ + index: RegistryIndex; + release: RegistryVersion; + latest: RegistryVersion | null; +}> { + let index: RegistryIndex; + try { + index = await fetchRegistryIndex(); + } catch (error) { + throw new PluginManageError( + `Could not reach the plugin registry: ${ + error instanceof Error ? error.message : String(error) + }`, + 502, + "REGISTRY_UNREACHABLE", + ); + } + const found = findRelease(index, pluginId, version); + if (!found) { + throw new PluginManageError( + version + ? `${pluginId} ${version} is not in the official registry` + : `${pluginId} is not in the official registry`, + 404, + "NOT_IN_REGISTRY", + ); + } + if (!isApiCompatible(found.release.api)) { + throw new PluginManageError( + `${pluginId} ${found.release.version} does not run on this version of Termix`, + 409, + "INCOMPATIBLE", + ); + } + return { + index, + release: found.release, + latest: latestCompatible(found.plugin.versions), + }; +} + +/** The version to hold at: an explicit pick that is not the newest. */ +function pinFor( + release: RegistryVersion, + latest: RegistryVersion | null, + requested: string | undefined, +): string | null { + if (!requested) return null; + return latest && latest.version === release.version ? null : release.version; +} + +async function applyPin(pluginId: string, pin: string | null): Promise { + const { loader } = getPluginRuntime(); + const pins = await readPins(); + if (pin) pins.set(pluginId, pin); + else pins.delete(pluginId); + loader.setPinned(pins); +} + +export function installPlugin( + pluginId: string, + options: InstallOptions, +): Promise<{ id: string; version: string; state: string }> { + return serialized(async () => { + const { createCurrentPluginRepository } = await repos(); + const repository = createCurrentPluginRepository(); + if (await repository.findById(pluginId)) { + throw new PluginManageError( + "Plugin is already installed", + 409, + "ALREADY_INSTALLED", + ); + } + + const { release, latest } = await requireRelease(pluginId, options.version); + const pin = pinFor(release, latest, options.version); + await applyPin(pluginId, pin); + + let plugin: LoadedPlugin; + try { + plugin = await swapIn(pluginId, release); + } catch (error) { + await applyPin(pluginId, null); + throw new PluginManageError( + error instanceof Error ? error.message : String(error), + 500, + "INSTALL_FAILED", + ); + } + + await repository.update(pluginId, { + registryId: OFFICIAL_REGISTRY_ID, + tier: plugin.source === "bundled" ? "bundled" : "official", + pinnedVersion: pin, + }); + await grantDeclared(plugin, options.userId); + + const { missing } = dependencyChain(pluginId); + if (missing.length > 0) { + await recordState( + pluginId, + "blocked", + `requires ${missing.map((id) => `"${id}"`).join(", ")}, which is not installed`, + ); + return { id: pluginId, version: release.version, state: "blocked" }; + } + for (const id of (await planStateChange(pluginId, true)).enable) { + await startAndRecord(id); + } + const state = await startAndRecord(pluginId); + pluginLogger.info(`Installed plugin ${pluginId}@${release.version}`, { + operation: "plugin_install", + }); + return { id: pluginId, version: release.version, state }; + }); +} + +export function updatePlugin( + pluginId: string, + options: InstallOptions, +): Promise<{ id: string; version: string; state: string }> { + return serialized(() => updateUnlocked(pluginId, options)); +} + +async function updateUnlocked( + pluginId: string, + options: InstallOptions, +): Promise<{ id: string; version: string; state: string }> { + const { createCurrentPluginRepository } = await repos(); + const repository = createCurrentPluginRepository(); + const record = await repository.findById(pluginId); + const { loader } = getPluginRuntime(); + const live = loader.get(pluginId); + if (!record || !live) { + throw new PluginManageError("Plugin not found", 404, "NOT_FOUND"); + } + + const { release, latest } = await requireRelease(pluginId, options.version); + if (release.version === live.manifest.version) { + throw new PluginManageError( + `${pluginId} ${release.version} is already installed`, + 409, + "SAME_VERSION", + ); + } + + const added = addedCapabilities( + live.manifest.capabilities, + release.capabilities, + ); + if (added.length > 0 && !options.acceptCapabilities) { + throw new PluginManageError( + "This version asks for new capabilities", + 409, + "CAPABILITIES_ADDED", + { capabilities: added }, + ); + } + + const wasEnabled = record.state !== "disabled"; + const pin = pinFor(release, latest, options.version); + await applyPin(pluginId, pin); + + let plugin: LoadedPlugin; + try { + plugin = await swapIn(pluginId, release); + } catch (error) { + await applyPin(pluginId, record.pinnedVersion ?? null); + if (wasEnabled && loader.get(pluginId)) await startAndRecord(pluginId); + throw new PluginManageError( + error instanceof Error ? error.message : String(error), + 500, + "UPDATE_FAILED", + ); + } + + await repository.update(pluginId, { + registryId: OFFICIAL_REGISTRY_ID, + tier: plugin.source === "bundled" ? "bundled" : "official", + pinnedVersion: pin, + }); + await grantDeclared(plugin, options.userId); + + const state = wasEnabled ? await startAndRecord(pluginId) : "stopped"; + pluginLogger.info( + `Updated plugin ${pluginId} from ${live.manifest.version} to ${release.version}`, + { operation: "plugin_update" }, + ); + return { id: pluginId, version: release.version, state }; +} + +export interface UpdateAllResult { + updated: Array<{ id: string; version: string }>; + needsReview: Array<{ id: string; capabilities: string[] }>; + failed: Array<{ id: string; error: string }>; +} + +/** + * Updates every plugin with a newer compatible release. One that asks for + * new capabilities is never updated here: it waits for an admin to review. + */ +export function updateAllPlugins(options: { + userId: string | null; + onlyAutoUpdate?: boolean; +}): Promise { + return serialized(async () => { + const result: UpdateAllResult = { + updated: [], + needsReview: [], + failed: [], + }; + const listing = await listRegistry({ force: true }); + for (const entry of listing.plugins) { + if (!entry.updateAvailable || entry.pinnedVersion) continue; + if (options.onlyAutoUpdate && !entry.autoUpdate) continue; + if (entry.addedCapabilities.length > 0) { + result.needsReview.push({ + id: entry.id, + capabilities: entry.addedCapabilities, + }); + continue; + } + try { + const done = await updateUnlocked(entry.id, { userId: options.userId }); + result.updated.push({ id: done.id, version: done.version }); + } catch (error) { + result.failed.push({ + id: entry.id, + error: error instanceof Error ? error.message : String(error), + }); + } + } + return result; + }); +} + +export function uninstallPlugin( + pluginId: string, +): Promise<{ id: string; removed: { tables: string[]; kvKeys: number } }> { + return serialized(async () => { + if (!ID_PATTERN.test(pluginId)) { + throw new PluginManageError("Invalid plugin id", 400, "INVALID_ID"); + } + const { createCurrentPluginRepository } = await repos(); + const repository = createCurrentPluginRepository(); + const record = await repository.findById(pluginId); + const { loader } = getPluginRuntime(); + const live = loader.get(pluginId); + if (!record && !live) { + throw new PluginManageError("Plugin not found", 404, "NOT_FOUND"); + } + const shipped = loader.bundledIds().has(pluginId); + + const dependents = dependentChain(pluginId, await enabledIds()); + if (dependents.length > 0) { + throw new PluginManageError( + "Other plugins depend on this one", + 409, + "HAS_DEPENDENTS", + { dependents }, + ); + } + + if (live) await deactivatePlugin(pluginId); + + const { removePluginData } = await import("./data.js"); + const removed = await removePluginData(pluginId, { + knownPluginIds: loader.list().map((plugin) => plugin.id), + }); + + if (live) loader.forget(pluginId); + await removeArtifactFiles(pluginId, live?.artifact); + await removeUserFolder(pluginId, live); + await fs.promises.rm(path.join(getUnpackedPluginsDir(), pluginId), { + recursive: true, + force: true, + }); + await fs.promises.rm(getPluginDataDir(pluginId), { + recursive: true, + force: true, + }); + await repository.delete(pluginId); + await applyPin(pluginId, null); + + // Ignored even when the files are gone: a recreated container brings + // the image's copy back, and it must not load in place of a reinstall. + if (shipped) { + const uninstalled = await readUninstalled(); + uninstalled.add(pluginId); + await writeUninstalled(uninstalled); + await removeShippedCopy(pluginId); + } + + pluginLogger.info(`Uninstalled plugin ${pluginId}`, { + operation: "plugin_uninstall", + }); + return { + id: pluginId, + removed: { tables: removed.tables, kvKeys: removed.kvKeys }, + }; + }); +} + +export function setPluginOptions( + pluginId: string, + options: { autoUpdate?: boolean; pinned?: boolean }, +): Promise<{ autoUpdate: boolean; pinnedVersion: string | null }> { + return serialized(async () => { + const { createCurrentPluginRepository } = await repos(); + const repository = createCurrentPluginRepository(); + const record = await repository.findById(pluginId); + if (!record) { + throw new PluginManageError("Plugin not found", 404, "NOT_FOUND"); + } + const live = getPluginRuntime().loader.get(pluginId); + const pinnedVersion = + options.pinned === undefined + ? record.pinnedVersion + : options.pinned + ? (live?.manifest.version ?? record.version) + : null; + const updated = await repository.update(pluginId, { + autoUpdate: options.autoUpdate, + pinnedVersion, + }); + await applyPin(pluginId, pinnedVersion); + return { + autoUpdate: Boolean(updated?.autoUpdate), + pinnedVersion: updated?.pinnedVersion ?? null, + }; + }); +} + +async function directorySize(dir: string): Promise { + let total = 0; + let entries: fs.Dirent[]; + try { + entries = await fs.promises.readdir(dir, { withFileTypes: true }); + } catch { + return 0; + } + for (const entry of entries) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) total += await directorySize(full); + else if (entry.isFile()) { + total += (await fs.promises.stat(full).catch(() => null))?.size ?? 0; + } + } + return total; +} + +export interface PluginDataSummary { + tables: Array<{ name: string; rows: number | null }>; + kvKeys: number; + settings: { admin: number; user: number; host: number; secret: number }; + migrations: Array<{ id: string; appliedAt: string | null }>; + grants: Array<{ + capability: string; + source: string; + grantedAt: string | null; + }>; + filesBytes: number; +} + +export async function getPluginDataSummary( + pluginId: string, +): Promise { + const { + createCurrentPluginRepository, + createCurrentPluginStorageRepository, + createCurrentPluginSettingsRepository, + createCurrentPluginMigrationRepository, + createCurrentPluginPermissionGrantRepository, + } = await repos(); + if (!(await createCurrentPluginRepository().findById(pluginId))) { + throw new PluginManageError("Plugin not found", 404, "NOT_FOUND"); + } + const { describePluginTables } = await import("./data.js"); + const knownPluginIds = getPluginRuntime() + .loader.list() + .map((plugin) => plugin.id); + + const [tables, kvKeys, settings, migrations, grants, filesBytes] = + await Promise.all([ + describePluginTables(pluginId, { knownPluginIds }), + createCurrentPluginStorageRepository().countKeys(pluginId), + createCurrentPluginSettingsRepository().countByScope(pluginId), + createCurrentPluginMigrationRepository().listByPlugin(pluginId), + createCurrentPluginPermissionGrantRepository().listByPlugin(pluginId), + directorySize(getPluginDataDir(pluginId)), + ]); + + return { + tables, + kvKeys, + settings, + migrations: migrations.map((row) => ({ + id: row.migrationId, + appliedAt: row.appliedAt ?? null, + })), + grants: grants.map((grant) => ({ + capability: grant.capability, + source: grant.source, + grantedAt: grant.grantedAt ?? null, + })), + filesBytes, + }; +} + +/** Clears a plugin's data and leaves it installed but switched off. */ +export function deletePluginData( + pluginId: string, + userId: string | null, +): Promise<{ tables: string[]; kvKeys: number; migrations: number }> { + return serialized(async () => { + const { loader } = getPluginRuntime(); + const live = loader.get(pluginId); + if (live) { + await recordState(pluginId, "disabled", null); + await deactivatePlugin(pluginId); + } + const { removePluginData } = await import("./data.js"); + const removed = await removePluginData(pluginId, { + knownPluginIds: loader.list().map((plugin) => plugin.id), + }); + await fs.promises.rm(getPluginDataDir(pluginId), { + recursive: true, + force: true, + }); + // Grants went with the data. A bundled plugin gets them straight back. + if (live) { + await registerLoadedPlugins([live]); + await grantDeclared(live, userId); + } + return removed; + }); +} diff --git a/src/backend/plugins/migrations.ts b/src/backend/plugins/migrations.ts index 984714508..9b41ea69f 100644 --- a/src/backend/plugins/migrations.ts +++ b/src/backend/plugins/migrations.ts @@ -112,8 +112,9 @@ export function readMigrations( * * A plugin owns the p__ namespace and nothing else. Enforced here as well * as in the CLI, because a plugin installed from a tarball never ran the CLI. - * A legacy core table is allowed only for the bundled plugin that adopts it: - * a user plugin with the same id gets no exemption. + * A legacy core table is allowed only for the official plugin that adopts + * it, bundled or signed by a trusted key: an unsigned plugin with the same id + * gets no exemption. */ export function assertOwnedTables( pluginId: string, diff --git a/src/backend/plugins/registry-index.ts b/src/backend/plugins/registry-index.ts new file mode 100644 index 000000000..a708a85f7 --- /dev/null +++ b/src/backend/plugins/registry-index.ts @@ -0,0 +1,314 @@ +/** + * The official plugin registry: fetching its index and downloading a release. + * + * The index is a plain JSON file in Termix-SSH/Termix-Registry. Nothing in it + * is trusted on its own: every download is checked against the sha256 and + * Ed25519 signature listed for it, and the signature must come from a key + * pinned in trust.ts. A tampered index can at most point at a file that then + * fails the check. + */ + +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import semver from "semver"; +import { SUPPORTED_PLUGIN_API_VERSION } from "@termix-ssh/plugin-sdk/manifest"; +import { safeOutboundFetch } from "../utils/safe-outbound-fetch.js"; +import { pluginLogger } from "../utils/logger.js"; +import { verifyPluginArtifact } from "./trust.js"; +import { getPluginsDir } from "./paths.js"; + +export const OFFICIAL_REGISTRY_ID = "official"; +export const OFFICIAL_REGISTRY_URL = + "https://raw.githubusercontent.com/Termix-SSH/Termix-Registry/main/official/index.json"; + +const CACHE_MS = 15 * 60_000; +const MAX_INDEX_BYTES = 5 * 1024 * 1024; +const MAX_ARTIFACT_BYTES = 200 * 1024 * 1024; +const MAX_REDIRECTS = 5; +const ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/; + +export interface RegistryVersion { + version: string; + api: string; + url: string; + sha256: string; + signature: string; + size: number; + capabilities: string[]; + releaseNotesUrl?: string; + publishedAt?: string; +} + +export interface RegistryPlugin { + id: string; + name: string; + description: string; + author: string; + category: string; + repository?: string; + icon?: string; + /** Newest first. */ + versions: RegistryVersion[]; +} + +export interface RegistryIndex { + registry: string; + name: string; + updatedAt?: string; + plugins: RegistryPlugin[]; +} + +export function getRegistryUrl(): string { + return process.env.TERMIX_PLUGIN_REGISTRY_URL || OFFICIAL_REGISTRY_URL; +} + +function asString(value: unknown): string | undefined { + return typeof value === "string" ? value : undefined; +} + +function parseVersion(raw: unknown): RegistryVersion | null { + if (!raw || typeof raw !== "object") return null; + const entry = raw as Record; + const version = asString(entry.version); + const url = asString(entry.url); + const sha256 = asString(entry.sha256); + const signature = asString(entry.signature); + if (!version || !semver.valid(version) || !url || !sha256 || !signature) { + return null; + } + if (!/^[a-f0-9]{64}$/i.test(sha256)) return null; + try { + if (new URL(url).protocol !== "https:") return null; + } catch { + return null; + } + return { + version, + api: asString(entry.api) ?? "", + url, + sha256: sha256.toLowerCase(), + signature, + size: typeof entry.size === "number" ? entry.size : 0, + capabilities: Array.isArray(entry.capabilities) + ? entry.capabilities.filter((c): c is string => typeof c === "string") + : [], + releaseNotesUrl: asString(entry.releaseNotesUrl), + publishedAt: asString(entry.publishedAt), + }; +} + +/** Validates a fetched index. Malformed entries are dropped, not fatal. */ +export function parseRegistryIndex(raw: unknown): RegistryIndex { + if (!raw || typeof raw !== "object") { + throw new Error("registry index is not an object"); + } + const index = raw as Record; + if (!Array.isArray(index.plugins)) { + throw new Error("registry index has no plugins list"); + } + + const plugins: RegistryPlugin[] = []; + for (const item of index.plugins) { + if (!item || typeof item !== "object") continue; + const entry = item as Record; + const id = asString(entry.id); + if (!id || !ID_PATTERN.test(id)) continue; + const versions = (Array.isArray(entry.versions) ? entry.versions : []) + .map(parseVersion) + .filter((v): v is RegistryVersion => v !== null) + .sort((a, b) => semver.rcompare(a.version, b.version)); + if (versions.length === 0) continue; + plugins.push({ + id, + name: asString(entry.name) ?? id, + description: asString(entry.description) ?? "", + author: asString(entry.author) ?? "", + category: asString(entry.category) ?? "", + repository: asString(entry.repository), + icon: asString(entry.icon), + versions, + }); + } + + return { + registry: asString(index.registry) ?? OFFICIAL_REGISTRY_ID, + name: asString(index.name) ?? "", + updatedAt: asString(index.updatedAt), + plugins, + }; +} + +/** Whether this build can run a release built against `api`. */ +export function isApiCompatible(api: string): boolean { + const major = semver.coerce(api)?.major; + return major !== undefined && String(major) === SUPPORTED_PLUGIN_API_VERSION; +} + +/** Follows redirects by hand so each hop still gets the outbound checks. */ +async function fetchFollowing(url: string): Promise { + let current = url; + for (let hop = 0; hop <= MAX_REDIRECTS; hop++) { + const response = await safeOutboundFetch(current, { + method: "GET", + redirect: "manual", + headers: { "User-Agent": "Termix" }, + }); + if (response.status >= 300 && response.status < 400) { + const location = response.headers.get("location"); + if (!location) throw new Error(`redirect without a location: ${current}`); + current = new URL(location, current).toString(); + continue; + } + if (!response.ok) { + throw new Error(`${current} answered ${response.status}`); + } + return response; + } + throw new Error(`too many redirects fetching ${url}`); +} + +async function readLimited(response: Response, limit: number): Promise { + const declared = Number(response.headers.get("content-length") ?? 0); + if (declared > limit) throw new Error("download is larger than allowed"); + if (!response.body) return Buffer.alloc(0); + + const chunks: Buffer[] = []; + let total = 0; + const reader = response.body.getReader(); + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + total += value.byteLength; + if (total > limit) { + await reader.cancel().catch(() => {}); + throw new Error("download is larger than allowed"); + } + chunks.push(Buffer.from(value)); + } + return Buffer.concat(chunks); +} + +let cache: { index: RegistryIndex; at: number; hash: string } | null = null; +let inflight: Promise | null = null; + +export interface RegistryStatus { + url: string; + lastCheckedAt: string | null; + error: string | null; +} + +let lastError: string | null = null; + +export function getRegistryStatus(): RegistryStatus { + return { + url: getRegistryUrl(), + lastCheckedAt: cache ? new Date(cache.at).toISOString() : null, + error: lastError, + }; +} + +export async function fetchRegistryIndex( + options: { force?: boolean } = {}, +): Promise { + if (!options.force && cache && Date.now() - cache.at < CACHE_MS) { + return cache.index; + } + if (inflight) return inflight; + + inflight = (async () => { + try { + const response = await fetchFollowing(getRegistryUrl()); + const body = await readLimited(response, MAX_INDEX_BYTES); + const index = parseRegistryIndex(JSON.parse(body.toString("utf8"))); + cache = { + index, + at: Date.now(), + hash: crypto.createHash("sha256").update(body).digest("hex"), + }; + lastError = null; + return index; + } catch (error) { + lastError = error instanceof Error ? error.message : String(error); + // A stale index beats none when the registry is briefly unreachable. + if (cache) { + pluginLogger.warn(`Plugin registry check failed: ${lastError}`, { + operation: "plugin_registry", + }); + return cache.index; + } + throw error; + } finally { + inflight = null; + } + })(); + return inflight; +} + +/** Test seam. */ +export function resetRegistryCache(): void { + cache = null; + inflight = null; + lastError = null; +} + +export function findRelease( + index: RegistryIndex, + pluginId: string, + version?: string, +): { plugin: RegistryPlugin; release: RegistryVersion } | null { + const plugin = index.plugins.find((entry) => entry.id === pluginId); + if (!plugin) return null; + const release = version + ? plugin.versions.find((entry) => entry.version === version) + : plugin.versions.find((entry) => isApiCompatible(entry.api)); + if (!release) return null; + return { plugin, release }; +} + +/** + * Downloads a release, verifies it, and leaves it in a staging folder the + * loader never scans. The caller moves it into place. + */ +export async function downloadRelease( + pluginId: string, + release: RegistryVersion, +): Promise<{ file: string; signatureFile: string }> { + if (!isApiCompatible(release.api)) { + throw new Error( + `${pluginId} ${release.version} needs plugin API ${release.api}, this build runs ${SUPPORTED_PLUGIN_API_VERSION}`, + ); + } + + const limit = + release.size > 0 ? release.size + 1024 * 1024 : MAX_ARTIFACT_BYTES; + const response = await fetchFollowing(release.url); + const buffer = await readLimited( + response, + Math.min(limit, MAX_ARTIFACT_BYTES), + ); + + const verified = verifyPluginArtifact( + buffer, + release.sha256, + release.signature, + ); + if (verified.ok === false) { + throw new Error(`${pluginId} ${release.version}: ${verified.reason}`); + } + + const staging = getDownloadsDir(); + await fs.promises.mkdir(staging, { recursive: true }); + const base = path.join( + staging, + `${pluginId}-${release.version}-${crypto.randomBytes(4).toString("hex")}.tmxplug`, + ); + await fs.promises.writeFile(base, buffer); + await fs.promises.writeFile(`${base}.sig`, release.signature.trim()); + return { file: base, signatureFile: `${base}.sig` }; +} + +/** A dot folder inside the plugins dir, which the loader's scan skips. */ +export function getDownloadsDir(): string { + return path.join(getPluginsDir(), ".downloads"); +} diff --git a/src/backend/starter.ts b/src/backend/starter.ts index cf654bb19..b04f0fabe 100644 --- a/src/backend/starter.ts +++ b/src/backend/starter.ts @@ -248,6 +248,10 @@ async function provisionLocalDesktopUserIfNeeded(): Promise { const { runPluginDataMigrations } = await import("./upgrade/boot-migrations.js"); await runPluginDataMigrations(); + + const { startPluginAutoUpdate } = + await import("./plugins/auto-update.js"); + startPluginAutoUpdate(); } catch (error) { systemLogger.warn("Plugin runtime failed to initialize", { operation: "plugin_init", diff --git a/src/backend/tests/database/routes/plugins.test.ts b/src/backend/tests/database/routes/plugins.test.ts index d2a73c11f..4bebd101f 100644 --- a/src/backend/tests/database/routes/plugins.test.ts +++ b/src/backend/tests/database/routes/plugins.test.ts @@ -6,6 +6,7 @@ */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { PluginManageError } from "../../../plugins/manage.js"; import express from "express"; import type { AddressInfo } from "node:net"; import type { Server } from "node:http"; @@ -161,6 +162,32 @@ vi.mock("../../../database/repositories/factory.js", () => ({ }), })); +const manageMock = vi.hoisted(() => ({ + listRegistry: vi.fn(), + installPlugin: vi.fn(), + updatePlugin: vi.fn(), + updateAllPlugins: vi.fn(), + uninstallPlugin: vi.fn(), + setPluginOptions: vi.fn(), + getPluginDataSummary: vi.fn(), + deletePluginData: vi.fn(), + planStateChange: vi.fn(), + setPluginState: vi.fn(), + isLinkedDesktop: vi.fn(async () => false), +})); +const auditMock = vi.hoisted(() => ({ logAudit: vi.fn(async () => {}) })); + +vi.mock("../../../plugins/manage.js", async (importOriginal) => ({ + ...(await importOriginal()), + ...manageMock, +})); + +vi.mock("../../../utils/audit-logger.js", () => ({ + logAudit: auditMock.logAudit, + getAuditUsername: async () => "admin", + getRequestMeta: () => ({ ipAddress: "127.0.0.1", userAgent: "test" }), +})); + const pluginRoutes = (await import("../../../database/routes/plugins.js")) .default; @@ -583,4 +610,139 @@ describe("plugins route", () => { expect(state.grants).toHaveLength(1); }); }); + describe("install, update, uninstall", () => { + const post = (path: string, body: unknown = {}, user = "admin-1") => + fetch(`${baseUrl}${path}`, { + method: "POST", + headers: { "content-type": "application/json", "x-test-user-id": user }, + body: JSON.stringify(body), + }); + + beforeEach(() => { + for (const fn of Object.values(manageMock)) fn.mockReset(); + manageMock.isLinkedDesktop.mockResolvedValue(false); + auditMock.logAudit.mockClear(); + }); + + it("installs a plugin as the caller and audits it", async () => { + manageMock.installPlugin.mockResolvedValue({ + id: "docker", + version: "1.0.0", + state: "active", + }); + const res = await post("/plugins/docker/install", { version: "1.0.0" }); + expect(res.status).toBe(200); + expect(manageMock.installPlugin).toHaveBeenCalledWith("docker", { + version: "1.0.0", + userId: "admin-1", + }); + expect(auditMock.logAudit).toHaveBeenCalledWith( + expect.objectContaining({ + action: "install_plugin", + resourceId: "docker", + }), + ); + }); + + it("rejects a bad id or version before touching anything", async () => { + expect((await post("/plugins/..%2Fx/install")).status).toBe(400); + expect( + (await post("/plugins/docker/install", { version: 7 })).status, + ).toBe(400); + expect(manageMock.installPlugin).not.toHaveBeenCalled(); + }); + + it("passes a refusal through with its code and details", async () => { + manageMock.updatePlugin.mockRejectedValue( + new PluginManageError("new caps", 409, "CAPABILITIES_ADDED", { + capabilities: ["notify:send"], + }), + ); + const res = await post("/plugins/docker/update", {}); + expect(res.status).toBe(409); + expect(await res.json()).toMatchObject({ + code: "CAPABILITIES_ADDED", + capabilities: ["notify:send"], + }); + expect(manageMock.updatePlugin).toHaveBeenCalledWith("docker", { + version: undefined, + userId: "admin-1", + acceptCapabilities: false, + }); + }); + + it("uninstalls", async () => { + manageMock.uninstallPlugin.mockResolvedValue({ + id: "docker", + removed: { tables: ["p_docker_x"], kvKeys: 0 }, + }); + const res = await fetch(`${baseUrl}/plugins/docker`, { + method: "DELETE", + }); + expect(res.status).toBe(200); + expect(manageMock.uninstallPlugin).toHaveBeenCalledWith("docker"); + }); + + it("refuses changes on a linked desktop", async () => { + manageMock.isLinkedDesktop.mockResolvedValue(true); + const res = await post("/plugins/docker/install"); + expect(res.status).toBe(409); + expect((await res.json()).code).toBe("MANAGED_BY_SERVER"); + expect(manageMock.installPlugin).not.toHaveBeenCalled(); + }); + + it("previews a state change without making it", async () => { + state.plugins.set("sample-plugin", makePlugin()); + manageMock.planStateChange.mockResolvedValue({ + enable: [], + disable: ["addon"], + missing: [], + }); + const res = await fetch( + `${baseUrl}/plugins/sample-plugin/state?dryRun=1`, + { + method: "PATCH", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ enabled: false }), + }, + ); + expect(await res.json()).toMatchObject({ disable: ["addon"] }); + expect(manageMock.setPluginState).not.toHaveBeenCalled(); + }); + + it("answers 502 when the registry is unreachable", async () => { + manageMock.listRegistry.mockRejectedValue(new Error("offline")); + const res = await fetch(`${baseUrl}/plugins/registry`); + expect(res.status).toBe(502); + }); + + it("403s a non-admin on every management route", async () => { + const user = "regular-user"; + const headers = { + "content-type": "application/json", + "x-test-user-id": user, + }; + const calls = [ + fetch(`${baseUrl}/plugins/registry`, { headers }), + post("/plugins/docker/install", {}, user), + post("/plugins/docker/update", {}, user), + post("/plugins/update-all", {}, user), + fetch(`${baseUrl}/plugins/docker`, { method: "DELETE", headers }), + fetch(`${baseUrl}/plugins/docker/options`, { + method: "PATCH", + headers, + body: JSON.stringify({ autoUpdate: true }), + }), + fetch(`${baseUrl}/plugins/docker/data`, { headers }), + ]; + for (const res of await Promise.all(calls)) { + expect(res.status).toBe(403); + } + for (const fn of Object.values(manageMock)) { + if (fn !== manageMock.isLinkedDesktop) { + expect(fn).not.toHaveBeenCalled(); + } + } + }); + }); }); diff --git a/src/backend/tests/plugins/auto-update.test.ts b/src/backend/tests/plugins/auto-update.test.ts new file mode 100644 index 000000000..548a43c9f --- /dev/null +++ b/src/backend/tests/plugins/auto-update.test.ts @@ -0,0 +1,55 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const manage = vi.hoisted(() => ({ + isLinkedDesktop: vi.fn(async () => false), + updateAllPlugins: vi.fn(), +})); +const notify = vi.hoisted(() => ({ sendCoreAlert: vi.fn(async () => {}) })); + +vi.mock("../../plugins/manage.js", () => manage); +vi.mock("../../notify/core-notify.js", () => notify); + +import { runPluginAutoUpdate } from "../../plugins/auto-update.js"; + +beforeEach(() => { + manage.isLinkedDesktop.mockResolvedValue(false); + manage.updateAllPlugins.mockReset(); + notify.sendCoreAlert.mockClear(); +}); + +describe("runPluginAutoUpdate", () => { + it("only touches plugins with auto-update on", async () => { + manage.updateAllPlugins.mockResolvedValue({ + updated: [{ id: "docker", version: "1.1.0" }], + needsReview: [], + failed: [], + }); + await runPluginAutoUpdate(); + expect(manage.updateAllPlugins).toHaveBeenCalledWith({ + userId: null, + onlyAutoUpdate: true, + }); + expect(notify.sendCoreAlert).not.toHaveBeenCalled(); + }); + + it("alerts admins about an update that asks for more", async () => { + manage.updateAllPlugins.mockResolvedValue({ + updated: [], + needsReview: [{ id: "docker", capabilities: ["notify:send"] }], + failed: [], + }); + await runPluginAutoUpdate(); + expect(notify.sendCoreAlert).toHaveBeenCalledWith( + expect.objectContaining({ + audience: "admins", + dedupeKey: "plugin-review:docker:notify:send", + }), + ); + }); + + it("does nothing on a linked desktop", async () => { + manage.isLinkedDesktop.mockResolvedValue(true); + await runPluginAutoUpdate(); + expect(manage.updateAllPlugins).not.toHaveBeenCalled(); + }); +}); diff --git a/src/backend/tests/plugins/fs-retry.test.ts b/src/backend/tests/plugins/fs-retry.test.ts new file mode 100644 index 000000000..b239036bf --- /dev/null +++ b/src/backend/tests/plugins/fs-retry.test.ts @@ -0,0 +1,62 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { moveWithRetry } from "../../plugins/fs-retry.js"; + +const roots: string[] = []; + +function tempDir(): string { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "termix-move-")); + roots.push(root); + return root; +} + +function locked(): NodeJS.ErrnoException { + return Object.assign(new Error("locked"), { code: "EPERM" }); +} + +afterEach(() => { + vi.restoreAllMocks(); + while (roots.length) { + fs.rmSync(roots.pop()!, { recursive: true, force: true }); + } +}); + +describe("moveWithRetry", () => { + it("waits out a passing lock", async () => { + const root = tempDir(); + fs.mkdirSync(path.join(root, "from")); + const real = fs.promises.rename; + const rename = vi + .spyOn(fs.promises, "rename") + .mockRejectedValueOnce(locked()) + .mockImplementation(real); + + await moveWithRetry(path.join(root, "from"), path.join(root, "to"), [1, 1]); + + expect(rename).toHaveBeenCalledTimes(2); + expect(fs.existsSync(path.join(root, "to"))).toBe(true); + }); + + it("copies a folder that never unlocks", async () => { + const root = tempDir(); + fs.mkdirSync(path.join(root, "from", "nested"), { recursive: true }); + fs.writeFileSync(path.join(root, "from", "nested", "a.js"), "x"); + vi.spyOn(fs.promises, "rename").mockRejectedValue(locked()); + + await moveWithRetry(path.join(root, "from"), path.join(root, "to"), [1]); + + expect( + fs.readFileSync(path.join(root, "to", "nested", "a.js"), "utf8"), + ).toBe("x"); + expect(fs.existsSync(path.join(root, "from"))).toBe(false); + }); + + it("does not retry a real error", async () => { + const root = tempDir(); + await expect( + moveWithRetry(path.join(root, "missing"), path.join(root, "to"), [1]), + ).rejects.toMatchObject({ code: "ENOENT" }); + }); +}); diff --git a/src/backend/tests/plugins/loader.test.ts b/src/backend/tests/plugins/loader.test.ts index f4ace73df..09244d528 100644 --- a/src/backend/tests/plugins/loader.test.ts +++ b/src/backend/tests/plugins/loader.test.ts @@ -11,7 +11,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { PluginLoader } from "../../plugins/loader.js"; +import { PluginLoader, assertBundledUpdate } from "../../plugins/loader.js"; import { createFixturePlugin } from "./fixture-plugin.js"; vi.mock("../../database/repositories/factory.js", () => ({ @@ -484,3 +484,68 @@ describe("PluginLoader error budget", () => { expect(loader.get("sample-plugin")?.lastError).toBeNull(); }); }); + +describe("PluginLoader uninstall and pins", () => { + it("ignores the shipped copy of a bundled plugin an admin uninstalled", async () => { + const bundled = tempRoot("termix-bundled-"); + const user = tempRoot("termix-data-"); + createFixturePlugin({ id: "ssh-terminal", root: bundled }); + createFixturePlugin({ id: "docker", root: bundled }); + setRoots(bundled, user); + + const loader = new PluginLoader(); + loader.setUninstalled(["docker"]); + const loaded = await loader.loadAll(); + + expect(loaded.map((plugin) => plugin.id)).toEqual(["ssh-terminal"]); + expect(loader.bundledVersion("docker")).toBeUndefined(); + expect(loader.bundledIds().has("docker")).toBe(false); + }); + + it("loads a downloaded copy of an uninstalled bundled id as a normal plugin", async () => { + const bundled = tempRoot("termix-bundled-"); + const user = tempRoot("termix-data-"); + const userPlugins = path.join(user, "plugins"); + fs.mkdirSync(userPlugins, { recursive: true }); + createFixturePlugin({ id: "docker", root: bundled }); + createFixturePlugin({ id: "docker", root: userPlugins }); + setRoots(bundled, user); + + const loader = new PluginLoader(); + loader.setUninstalled(["docker"]); + const loaded = await loader.loadAll(); + + expect(loaded).toHaveLength(1); + expect(loaded[0].source).toBe("user"); + }); + + it("loads the shipped copy again on reinstall", async () => { + const bundled = tempRoot("termix-bundled-"); + const user = tempRoot("termix-data-"); + createFixturePlugin({ id: "docker", root: bundled }); + setRoots(bundled, user); + + const loader = new PluginLoader(); + loader.setUninstalled(["docker"]); + await loader.loadAll(); + const plugin = await loader.loadBundled("docker"); + + expect(plugin.source).toBe("bundled"); + expect(loader.get("docker")).toBe(plugin); + }); + + it("refuses an older signed update unless that exact version is pinned", () => { + expect(() => + assertBundledUpdate("docker", "1.0.0", "key", "1.1.0"), + ).toThrow(/not newer/); + expect(() => + assertBundledUpdate("docker", "1.0.0", "key", "1.1.0", "0.9.0"), + ).toThrow(/not newer/); + expect(() => + assertBundledUpdate("docker", "1.0.0", "key", "1.1.0", "1.0.0"), + ).not.toThrow(); + expect(() => + assertBundledUpdate("docker", "1.0.0", undefined, "1.1.0", "1.0.0"), + ).toThrow(/signed/); + }); +}); diff --git a/src/backend/tests/plugins/manage.test.ts b/src/backend/tests/plugins/manage.test.ts new file mode 100644 index 000000000..b2bb9f5c7 --- /dev/null +++ b/src/backend/tests/plugins/manage.test.ts @@ -0,0 +1,431 @@ +/** + * Install, update, uninstall and switching, against a real PluginLoader and + * fixture plugins on disk. The database and the registry are faked. + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import * as tar from "tar"; +import { createFixturePlugin } from "./fixture-plugin.js"; +import type { LoadedPlugin } from "../../plugins/loader.js"; + +interface Row { + id: string; + name: string; + version: string; + tier: string; + source: string; + state: string; + lastError: string | null; + registryId: string | null; + autoUpdate: boolean; + pinnedVersion: string | null; + manifestJson: string; +} + +const db = vi.hoisted(() => ({ + rows: new Map>(), + settings: new Map(), + grants: [] as Array<{ pluginId: string; capability: string }>, +})); + +const registry = vi.hoisted(() => ({ + index: { registry: "official", name: "", plugins: [] as unknown[] }, + download: vi.fn(), +})); + +vi.mock("../../database/repositories/factory.js", () => ({ + createCurrentPluginRepository: () => ({ + listAll: async () => [...db.rows.values()], + findById: async (id: string) => db.rows.get(id) ?? null, + create: async (input: Record) => { + const row = { + registryId: null, + lastError: null, + autoUpdate: false, + pinnedVersion: null, + ...input, + }; + db.rows.set(input.id as string, row); + return row; + }, + update: async (id: string, input: Record) => { + const row = db.rows.get(id); + if (!row) return null; + for (const [key, value] of Object.entries(input)) { + if (value !== undefined) row[key] = value; + } + return row; + }, + delete: async (id: string) => db.rows.delete(id), + }), + createCurrentSettingsRepository: () => ({ + get: async (key: string) => db.settings.get(key) ?? null, + set: async (key: string, value: string) => void db.settings.set(key, value), + }), + createCurrentPluginPermissionGrantRepository: () => ({ + listByPlugin: async (id: string) => + db.grants.filter((g) => g.pluginId === id), + grant: async (input: { pluginId: string; capability: string }) => + void db.grants.push(input), + }), + createCurrentPluginStorageRepository: () => ({ + get: async () => null, + set: async () => {}, + delete: async () => false, + listKeys: async () => [], + countKeys: async () => 0, + }), +})); + +vi.mock("../../plugins/data.js", () => ({ + migratePlugin: async () => [], + removePluginData: async () => ({ tables: [], kvKeys: 0, migrations: 0 }), + describePluginTables: async () => [], + registerTable: () => ({}), + forgetTables: () => {}, +})); + +vi.mock("../../plugins/permissions.js", () => ({ + invalidatePluginPermissionCache: () => {}, +})); + +vi.mock("../../plugins/registry-index.js", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + fetchRegistryIndex: async () => actual.parseRegistryIndex(registry.index), + downloadRelease: registry.download, + }; +}); + +vi.mock("../../plugins/index.js", async () => { + const { PluginLoader } = await import("../../plugins/loader.js"); + const loader = new PluginLoader(); + const seed = async (plugins: LoadedPlugin[]) => { + for (const plugin of plugins) { + if (db.rows.has(plugin.id)) { + Object.assign(db.rows.get(plugin.id)!, { + version: plugin.manifest.version, + manifestJson: JSON.stringify(plugin.manifest), + }); + continue; + } + db.rows.set(plugin.id, { + id: plugin.id, + name: plugin.manifest.name, + version: plugin.manifest.version, + tier: plugin.source === "bundled" ? "bundled" : "community", + source: plugin.source, + state: plugin.source === "bundled" ? "enabled" : "disabled", + lastError: null, + registryId: null, + autoUpdate: false, + pinnedVersion: null, + manifestJson: JSON.stringify(plugin.manifest), + } satisfies Row); + } + }; + return { + getPluginRuntime: () => ({ loader }), + activatePlugin: (id: string) => loader.activate(id), + deactivatePlugin: (id: string) => loader.deactivate(id), + registerLoadedPlugins: seed, + unloadPlugin: async (id: string) => { + if (!loader.get(id)) return; + await loader.deactivate(id); + loader.forget(id); + }, + }; +}); + +const { getPluginRuntime } = await import("../../plugins/index.js"); +const manage = await import("../../plugins/manage.js"); + +const roots: string[] = []; +let bundled: string; +let data: string; + +function tempRoot(prefix: string): string { + const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + roots.push(root); + return root; +} + +function registryEntry( + id: string, + versions: Array<{ version: string; capabilities?: string[] }>, +) { + return { + id, + name: id, + versions: versions.map((v) => ({ + version: v.version, + api: "1", + url: `https://example.com/${id}-${v.version}.tmxplug`, + sha256: "a".repeat(64), + signature: "sig", + size: 1, + capabilities: v.capabilities ?? ["hosts:read", "kv:own"], + })), + }; +} + +/** A .tmxplug of a fixture plugin, standing in for a verified download. */ +async function stageArtifact( + id: string, + manifestOverrides: Record = {}, +) { + const build = tempRoot("termix-build-"); + createFixturePlugin({ id, root: build, manifestOverrides }); + const staging = path.join(data, "plugins", ".downloads"); + fs.mkdirSync(staging, { recursive: true }); + const file = path.join(staging, `${id}-staged.tmxplug`); + await tar.c({ gzip: true, file, cwd: path.join(build, id) }, ["."]); + return { file, signatureFile: `${file}.sig` }; +} + +async function boot() { + const { loader } = getPluginRuntime(); + await manage.applyStoredPluginChoices(loader); + const loaded = await loader.loadAll(); + const { registerLoadedPlugins } = await import("../../plugins/index.js"); + await registerLoadedPlugins(loaded); + for (const plugin of loaded) { + if (db.rows.get(plugin.id)?.state === "enabled") { + await loader.activate(plugin.id); + } + } + return loader; +} + +beforeEach(() => { + bundled = tempRoot("termix-bundled-"); + data = tempRoot("termix-data-"); + process.env.TERMIX_BUNDLED_PLUGINS_DIR = bundled; + process.env.DATA_DIR = data; + db.rows.clear(); + db.settings.clear(); + db.grants.length = 0; + registry.index.plugins = []; + registry.download.mockReset(); +}); + +afterEach(async () => { + const { loader } = getPluginRuntime(); + await loader.shutdown(); + for (const plugin of loader.list()) loader.forget(plugin.id); + delete process.env.TERMIX_BUNDLED_PLUGINS_DIR; + delete process.env.DATA_DIR; + while (roots.length) { + fs.rmSync(roots.pop()!, { recursive: true, force: true }); + } +}); + +describe("uninstall and reinstall of a bundled plugin", () => { + it("deletes it, keeps it gone after a restart, and downloads it again", async () => { + createFixturePlugin({ id: "docker", root: bundled }); + registry.index.plugins = [registryEntry("docker", [{ version: "1.0.0" }])]; + let loader = await boot(); + expect(loader.get("docker")?.state).toBe("active"); + + await manage.uninstallPlugin("docker"); + expect(loader.get("docker")).toBeUndefined(); + expect(db.rows.has("docker")).toBe(false); + expect(fs.existsSync(path.join(bundled, "docker"))).toBe(false); + expect(JSON.parse(db.settings.get("plugins_uninstalled")!)).toEqual([ + "docker", + ]); + + await loader.shutdown(); + for (const plugin of loader.list()) loader.forget(plugin.id); + // A recreated container brings the image's copy back; it stays ignored. + createFixturePlugin({ id: "docker", root: bundled }); + loader = await boot(); + expect(loader.get("docker")).toBeUndefined(); + + registry.download.mockImplementation(() => stageArtifact("docker")); + const result = await manage.installPlugin("docker", { userId: "admin" }); + expect(result.state).toBe("active"); + expect(registry.download).toHaveBeenCalledOnce(); + expect(loader.get("docker")?.source).toBe("user"); + expect(fs.existsSync(path.join(data, "plugins", "docker.tmxplug"))).toBe( + true, + ); + expect(db.rows.get("docker")).toMatchObject({ + registryId: "official", + tier: "official", + }); + expect(db.grants.map((g) => g.capability)).toEqual([ + "hosts:read", + "kv:own", + ]); + + await loader.shutdown(); + for (const plugin of loader.list()) loader.forget(plugin.id); + loader = await boot(); + expect(loader.get("docker")?.source).toBe("user"); + }); + + it("deletes a plugin dropped in as a plain folder", async () => { + createFixturePlugin({ id: "notes", root: path.join(data, "plugins") }); + const loader = await boot(); + expect(loader.get("notes")?.source).toBe("user"); + + await manage.uninstallPlugin("notes"); + expect(fs.existsSync(path.join(data, "plugins", "notes"))).toBe(false); + expect(fs.existsSync(path.join(data, "plugins"))).toBe(true); + expect(db.settings.get("plugins_uninstalled")).toBeUndefined(); + }); + + it("refuses to install something already installed", async () => { + createFixturePlugin({ id: "docker", root: bundled }); + registry.index.plugins = [registryEntry("docker", [{ version: "1.0.0" }])]; + await boot(); + await expect( + manage.installPlugin("docker", { userId: "admin" }), + ).rejects.toMatchObject({ code: "ALREADY_INSTALLED" }); + }); +}); + +describe("installing from the registry", () => { + it("puts the artifact in place, grants what it declares and starts it", async () => { + registry.index.plugins = [registryEntry("notes", [{ version: "1.0.0" }])]; + registry.download.mockImplementation(() => + stageArtifact("notes", { capabilities: ["kv:own"] }), + ); + const loader = await boot(); + + const result = await manage.installPlugin("notes", { userId: "admin" }); + + expect(result).toMatchObject({ id: "notes", state: "active" }); + expect(fs.existsSync(path.join(data, "plugins", "notes.tmxplug"))).toBe( + true, + ); + expect(loader.get("notes")?.source).toBe("user"); + expect(db.rows.get("notes")?.tier).toBe("official"); + expect(db.grants).toEqual([ + expect.objectContaining({ pluginId: "notes", capability: "kv:own" }), + ]); + }); + + it("refuses a plugin that is not in the registry", async () => { + await boot(); + await expect( + manage.installPlugin("nope", { userId: "admin" }), + ).rejects.toMatchObject({ code: "NOT_IN_REGISTRY" }); + }); + + it("pins an explicitly picked older version", async () => { + registry.index.plugins = [ + registryEntry("notes", [{ version: "1.1.0" }, { version: "1.0.0" }]), + ]; + registry.download.mockImplementation(() => stageArtifact("notes")); + await boot(); + + await manage.installPlugin("notes", { userId: "admin", version: "1.0.0" }); + expect(db.rows.get("notes")?.pinnedVersion).toBe("1.0.0"); + }); + + it("uninstalls a registry plugin and its files", async () => { + registry.index.plugins = [registryEntry("notes", [{ version: "1.0.0" }])]; + registry.download.mockImplementation(() => stageArtifact("notes")); + await boot(); + await manage.installPlugin("notes", { userId: "admin" }); + + await manage.uninstallPlugin("notes"); + expect(fs.existsSync(path.join(data, "plugins", "notes.tmxplug"))).toBe( + false, + ); + expect(db.rows.has("notes")).toBe(false); + expect(db.settings.get("plugins_uninstalled")).toBeUndefined(); + }); +}); + +describe("updates", () => { + it("waits for consent when a release asks for new capabilities", async () => { + createFixturePlugin({ id: "docker", root: bundled }); + registry.index.plugins = [ + registryEntry("docker", [ + { + version: "1.1.0", + capabilities: ["hosts:read", "kv:own", "notify:send"], + }, + { version: "1.0.0" }, + ]), + ]; + await boot(); + + await expect( + manage.updatePlugin("docker", { userId: "admin" }), + ).rejects.toMatchObject({ + code: "CAPABILITIES_ADDED", + details: { capabilities: ["notify:send"] }, + }); + expect(registry.download).not.toHaveBeenCalled(); + }); + + it("lists an update and skips it in update-all when pinned", async () => { + createFixturePlugin({ id: "docker", root: bundled }); + registry.index.plugins = [ + registryEntry("docker", [{ version: "1.1.0" }, { version: "1.0.0" }]), + ]; + await boot(); + + const listing = await manage.listRegistry(); + expect(listing.plugins[0]).toMatchObject({ + installed: true, + installedVersion: "1.0.0", + latestVersion: "1.1.0", + updateAvailable: true, + addedCapabilities: [], + bundled: true, + }); + + await manage.setPluginOptions("docker", { pinned: true }); + const result = await manage.updateAllPlugins({ userId: "admin" }); + expect(result.updated).toEqual([]); + expect(registry.download).not.toHaveBeenCalled(); + }); +}); + +describe("dependencies", () => { + it("starts a dependency first and stops dependents first", async () => { + createFixturePlugin({ id: "base", root: bundled }); + createFixturePlugin({ + id: "addon", + root: bundled, + manifestOverrides: { dependencies: { base: "^1.0.0" } }, + }); + const loader = await boot(); + + const off = await manage.setPluginState("base", false); + expect(off.disable).toEqual(["addon"]); + expect(loader.get("addon")?.state).toBe("stopped"); + expect(loader.get("base")?.state).toBe("stopped"); + + const plan = await manage.planStateChange("addon", true); + expect(plan.enable).toEqual(["base"]); + const on = await manage.setPluginState("addon", true); + expect(on.state).toBe("active"); + expect(loader.get("base")?.state).toBe("active"); + }); + + it("refuses to uninstall a plugin a running one depends on", async () => { + createFixturePlugin({ id: "base", root: bundled }); + createFixturePlugin({ + id: "addon", + root: bundled, + manifestOverrides: { dependencies: { base: "^1.0.0" } }, + }); + await boot(); + + await expect(manage.uninstallPlugin("base")).rejects.toMatchObject({ + code: "HAS_DEPENDENTS", + details: { dependents: ["addon"] }, + }); + }); +}); diff --git a/src/backend/tests/plugins/registry-index.test.ts b/src/backend/tests/plugins/registry-index.test.ts new file mode 100644 index 000000000..74cde0db0 --- /dev/null +++ b/src/backend/tests/plugins/registry-index.test.ts @@ -0,0 +1,231 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const fetchMock = vi.hoisted(() => vi.fn()); +const testKey = vi.hoisted(() => ({ value: null as null | object })); + +vi.mock("../../utils/safe-outbound-fetch.js", () => ({ + safeOutboundFetch: fetchMock, +})); + +vi.mock("../../plugins/trust.js", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + verifyPluginArtifact: (buffer: Buffer, sha256: string, signature: string) => + actual.verifyPluginArtifact(buffer, sha256, signature, [ + testKey.value as never, + ]), + }; +}); + +import { + downloadRelease, + fetchRegistryIndex, + findRelease, + isApiCompatible, + parseRegistryIndex, + resetRegistryCache, + type RegistryVersion, +} from "../../plugins/registry-index.js"; + +const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519"); +testKey.value = { + id: "test", + publicKey: publicKey + .export({ format: "der", type: "spki" }) + .subarray(12) + .toString("base64"), + addedIn: "26.10.0", +}; + +function sign(buffer: Buffer) { + const digest = crypto.createHash("sha256").update(buffer).digest(); + return { + sha256: digest.toString("hex"), + signature: crypto.sign(null, digest, privateKey).toString("base64"), + }; +} + +function response(body: string | Buffer, status = 200, headers = {}) { + return new Response(status >= 300 && status < 400 ? null : body, { + status, + headers, + }); +} + +function version(overrides: Partial = {}) { + return { + version: "1.0.0", + api: "1", + url: "https://example.com/p-1.0.0.tmxplug", + sha256: "a".repeat(64), + signature: "sig", + size: 10, + capabilities: ["kv:own"], + publishedAt: "2026-10-01T00:00:00Z", + ...overrides, + }; +} + +let dataDir: string; + +beforeEach(() => { + dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "termix-registry-")); + process.env.DATA_DIR = dataDir; + fetchMock.mockReset(); + resetRegistryCache(); +}); + +afterEach(() => { + delete process.env.DATA_DIR; + fs.rmSync(dataDir, { recursive: true, force: true }); +}); + +describe("parseRegistryIndex", () => { + it("keeps valid entries and sorts versions newest first", () => { + const index = parseRegistryIndex({ + registry: "official", + plugins: [ + { + id: "docker", + name: "Docker", + versions: [version(), version({ version: "1.2.0" })], + }, + ], + }); + expect(index.plugins).toHaveLength(1); + expect(index.plugins[0].versions.map((v) => v.version)).toEqual([ + "1.2.0", + "1.0.0", + ]); + }); + + it("drops entries with a bad id, no https url or a malformed sha256", () => { + const index = parseRegistryIndex({ + plugins: [ + { id: "../evil", versions: [version()] }, + { id: "plain", versions: [version({ url: "http://x/y.tmxplug" })] }, + { id: "short", versions: [version({ sha256: "abc" })] }, + { id: "ok", versions: [version()] }, + ], + }); + expect(index.plugins.map((p) => p.id)).toEqual(["ok"]); + }); + + it("refuses something that is not an index", () => { + expect(() => parseRegistryIndex({ nope: true })).toThrow(/no plugins/); + }); +}); + +describe("findRelease", () => { + const index = parseRegistryIndex({ + plugins: [ + { + id: "docker", + versions: [ + version({ version: "2.0.0", api: "2" }), + version({ version: "1.1.0" }), + version(), + ], + }, + ], + }); + + it("picks the newest release this build can run", () => { + expect(isApiCompatible("1")).toBe(true); + expect(isApiCompatible("2")).toBe(false); + expect(findRelease(index, "docker")?.release.version).toBe("1.1.0"); + }); + + it("finds an exact version", () => { + expect(findRelease(index, "docker", "1.0.0")?.release.version).toBe( + "1.0.0", + ); + expect(findRelease(index, "docker", "9.9.9")).toBeNull(); + expect(findRelease(index, "missing")).toBeNull(); + }); +}); + +describe("fetchRegistryIndex", () => { + const body = JSON.stringify({ + plugins: [{ id: "docker", versions: [version()] }], + }); + + it("follows redirects and caches the result", async () => { + fetchMock + .mockResolvedValueOnce( + response("", 302, { location: "https://cdn.example.com/index.json" }), + ) + .mockResolvedValueOnce(response(body)); + + const first = await fetchRegistryIndex(); + const second = await fetchRegistryIndex(); + expect(first.plugins[0].id).toBe("docker"); + expect(second).toBe(first); + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(fetchMock.mock.calls[1][0]).toBe( + "https://cdn.example.com/index.json", + ); + }); + + it("serves the stale copy when a refresh fails", async () => { + fetchMock.mockResolvedValueOnce(response(body)); + const first = await fetchRegistryIndex(); + fetchMock.mockResolvedValueOnce(response("down", 503)); + expect(await fetchRegistryIndex({ force: true })).toBe(first); + }); + + it("throws when there is nothing cached", async () => { + fetchMock.mockResolvedValueOnce(response("down", 503)); + await expect(fetchRegistryIndex()).rejects.toThrow(/503/); + }); +}); + +describe("downloadRelease", () => { + const artifact = Buffer.from("plugin archive bytes"); + + it("writes a verified artifact and its signature to staging", async () => { + const signed = sign(artifact); + fetchMock.mockResolvedValueOnce(response(artifact)); + + const result = await downloadRelease( + "docker", + version({ ...signed, size: artifact.length }), + ); + expect(fs.readFileSync(result.file)).toEqual(artifact); + expect(fs.readFileSync(result.signatureFile, "utf8")).toBe( + signed.signature, + ); + expect( + result.file.startsWith(path.join(dataDir, "plugins", ".downloads")), + ).toBe(true); + }); + + it("refuses a file that does not match its signature", async () => { + const signed = sign(artifact); + fetchMock.mockResolvedValueOnce(response(Buffer.from("something else!!!"))); + await expect( + downloadRelease("docker", version({ ...signed, size: 0 })), + ).rejects.toThrow(/sha256/); + }); + + it("refuses a release built for another plugin API", async () => { + await expect( + downloadRelease("docker", version({ api: "2" })), + ).rejects.toThrow(/plugin API/); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it("refuses a download larger than the index says", async () => { + const signed = sign(artifact); + fetchMock.mockResolvedValueOnce(response(Buffer.alloc(3 * 1024 * 1024))); + await expect( + downloadRelease("docker", version({ ...signed, size: 10 })), + ).rejects.toThrow(/larger than allowed/); + }); +}); diff --git a/src/types/ui-types.ts b/src/types/ui-types.ts index 1eae6c98d..78a7b09ca 100644 --- a/src/types/ui-types.ts +++ b/src/types/ui-types.ts @@ -149,7 +149,8 @@ type KnownTabType = | "settings" | "user-profile" | "admin-settings" - | "split-screen"; + | "split-screen" + | "plugins"; /** * TabType covers every built-in tab plus any plugin-contributed tab id. diff --git a/src/ui/AppShell.tsx b/src/ui/AppShell.tsx index ce5b43cae..3bfd93595 100644 --- a/src/ui/AppShell.tsx +++ b/src/ui/AppShell.tsx @@ -238,6 +238,7 @@ import { setShellLayoutProvider, } from "@/plugin-host/shell-bridge"; import { PluginViewPlaceholder } from "@/plugin-host/PluginViewPlaceholder"; +import { OPEN_PLUGINS_EVENT } from "@/plugins/open-plugins"; export { buildHostTree } from "@/sidebar/build-host-tree"; export { tabIcon, renderTabContent } from "@/shell/tabUtils"; @@ -1723,6 +1724,7 @@ export function AppShell({ const singletonLabels: Partial> = { "host-manager": t("nav.manage"), settings: t("nav.settings"), + plugins: t("nav.plugins"), }; // A plugin tab names itself; promoted rail panels reuse the rail's own // label so the two stay in sync. @@ -1776,6 +1778,12 @@ export function AppShell({ const openSingletonTabRef = useRef(openSingletonTab); openSingletonTabRef.current = openSingletonTab; + useEffect(() => { + const handler = () => openSingletonTabRef.current("plugins"); + window.addEventListener(OPEN_PLUGINS_EVENT, handler); + return () => window.removeEventListener(OPEN_PLUGINS_EVENT, handler); + }, []); + const getTabCloseLabel = useCallback((tab: Tab) => { return tab.customLabel || tab.label || tab.host?.name || String(tab.id); }, []); @@ -3161,6 +3169,7 @@ export function AppShell({ "host-manager", "user-profile", "admin-settings", + "plugins", ].includes(type) ) { openSingletonTab(type, pendingEvent); diff --git a/src/ui/api/plugins-api.ts b/src/ui/api/plugins-api.ts index f8ee09854..9531c1a48 100644 --- a/src/ui/api/plugins-api.ts +++ b/src/ui/api/plugins-api.ts @@ -237,9 +237,209 @@ export interface PluginSummary { lastError?: string | null; /** Paths a running plugin serves without login. Admins only. */ publicRoutes?: { http: string[]; ws: string[] }; + registryId?: string | null; + autoUpdate?: boolean; + pinnedVersion?: string | null; + description?: string; + author?: string; + repository?: string; + signedBy?: string | null; } export async function getPlugins(): Promise { const response = await rbacApi.get("/plugins"); return Array.isArray(response.data) ? response.data : []; } + +/** Fired after any change to installed plugins; the loader resyncs on it. */ +export const PLUGINS_CHANGED_EVENT = "termix:plugins-changed"; + +function announcePluginsChanged(): void { + window.dispatchEvent(new CustomEvent(PLUGINS_CHANGED_EVENT)); +} + +export interface RegistryPluginVersion { + version: string; + compatible: boolean; + capabilities: string[]; + publishedAt?: string; + releaseNotesUrl?: string; + size: number; +} + +export interface RegistryPluginEntry { + id: string; + name: string; + description: string; + author: string; + category: string; + repository?: string; + icon?: string; + versions: RegistryPluginVersion[]; + latestVersion: string | null; + installed: boolean; + installedVersion: string | null; + updateAvailable: boolean; + addedCapabilities: string[]; + pinnedVersion: string | null; + autoUpdate: boolean; + bundled: boolean; +} + +export interface RegistryListing { + registry: { + id: string; + url: string; + lastCheckedAt: string | null; + error: string | null; + }; + managedByServer: boolean; + plugins: RegistryPluginEntry[]; +} + +export interface PluginStateChange { + id: string; + enabled: boolean; + enable: string[]; + disable: string[]; + missing: string[]; + state?: string; +} + +export interface PluginDataSummary { + id: string; + tables: Array<{ name: string; rows: number | null }>; + kvKeys: number; + settings: { admin: number; user: number; host: number; secret: number }; + migrations: Array<{ id: string; appliedAt: string | null }>; + grants: Array<{ + capability: string; + source: string; + grantedAt: string | null; + }>; + filesBytes: number; +} + +export interface UpdateAllResult { + updated: Array<{ id: string; version: string }>; + needsReview: Array<{ id: string; capabilities: string[] }>; + failed: Array<{ id: string; error: string }>; +} + +const pluginPath = (pluginId: string) => + `/plugins/${encodeURIComponent(pluginId)}`; + +export async function getPluginRegistry( + refresh = false, +): Promise { + const response = await rbacApi.get("/plugins/registry", { + params: refresh ? { refresh: 1 } : undefined, + }); + return response.data; +} + +export async function installPlugin( + pluginId: string, + version?: string, +): Promise<{ id: string; version: string; state: string }> { + try { + const response = await rbacApi.post(`${pluginPath(pluginId)}/install`, { + version, + }); + return response.data; + } finally { + announcePluginsChanged(); + } +} + +export async function updatePlugin( + pluginId: string, + options: { version?: string; acceptCapabilities?: boolean } = {}, +): Promise<{ id: string; version: string; state: string }> { + try { + const response = await rbacApi.post( + `${pluginPath(pluginId)}/update`, + options, + ); + return response.data; + } finally { + announcePluginsChanged(); + } +} + +export async function updateAllPlugins(): Promise { + try { + const response = await rbacApi.post("/plugins/update-all"); + return response.data; + } finally { + announcePluginsChanged(); + } +} + +export async function uninstallPlugin(pluginId: string): Promise { + try { + await rbacApi.delete(pluginPath(pluginId)); + } finally { + announcePluginsChanged(); + } +} + +export async function previewPluginState( + pluginId: string, + enabled: boolean, +): Promise { + const response = await rbacApi.patch( + `${pluginPath(pluginId)}/state`, + { enabled }, + { params: { dryRun: 1 } }, + ); + return response.data; +} + +export async function setPluginState( + pluginId: string, + enabled: boolean, +): Promise { + try { + const response = await rbacApi.patch(`${pluginPath(pluginId)}/state`, { + enabled, + }); + return response.data; + } finally { + announcePluginsChanged(); + } +} + +export async function retryPlugin(pluginId: string): Promise { + try { + await rbacApi.post(`${pluginPath(pluginId)}/retry`); + } finally { + announcePluginsChanged(); + } +} + +export async function setPluginOptions( + pluginId: string, + options: { autoUpdate?: boolean; pinned?: boolean }, +): Promise<{ autoUpdate: boolean; pinnedVersion: string | null }> { + const response = await rbacApi.patch( + `${pluginPath(pluginId)}/options`, + options, + ); + return response.data; +} + +export async function getPluginData( + pluginId: string, +): Promise { + const response = await rbacApi.get(`${pluginPath(pluginId)}/data`); + return response.data; +} + +export async function deletePluginData(pluginId: string): Promise { + try { + await rbacApi.delete(`${pluginPath(pluginId)}/data`); + } finally { + announcePluginsChanged(); + } +} diff --git a/src/ui/locales/en.json b/src/ui/locales/en.json index 3ad69516c..19781776d 100644 --- a/src/ui/locales/en.json +++ b/src/ui/locales/en.json @@ -331,7 +331,8 @@ "openSettings": "Open settings", "settings": "Settings", "manage": "Manage", - "mobileNavigation": "Navigation" + "mobileNavigation": "Navigation", + "plugins": "Plugins" }, "hosts": { "hosts": "Hosts", @@ -1952,6 +1953,172 @@ "loading": "Loading {{name}}", "crashed": "Something went wrong in {{name}}", "retry": "Try again" + }, + "manager": { + "tabs": { + "installed": "Installed", + "browse": "Browse", + "updates": "Updates" + }, + "status": { + "summary": "{{installed}} installed, {{running}} running", + "running": "Running", + "disabled": "Disabled", + "failed": "Stopped unexpectedly", + "blocked": "Waiting on another plugin" + }, + "groups": { + "running": "Running", + "notRunning": "Not running" + }, + "sort": { + "name": "Name", + "updated": "Recently updated", + "category": "Category" + }, + "risk": { + "critical": "Critical", + "high": "High risk", + "medium": "Medium risk", + "low": "Low risk" + }, + "contributes": { + "tabs_one": "{{count}} tab", + "tabs_other": "{{count}} tabs", + "panels_one": "{{count}} panel", + "panels_other": "{{count}} panels", + "dashboardCards_one": "{{count}} dashboard card", + "dashboardCards_other": "{{count}} dashboard cards", + "actions_one": "{{count}} action", + "actions_other": "{{count}} actions", + "protocols_one": "{{count}} connection type", + "protocols_other": "{{count}} connection types", + "auth_one": "{{count}} sign in method", + "auth_other": "{{count}} sign in methods", + "settings_one": "Settings", + "settings_other": "Settings", + "hostCapability_one": "Host settings", + "hostCapability_other": "Host settings", + "permissions_one": "{{count}} role permission", + "permissions_other": "{{count}} role permissions", + "keybindingActions_one": "{{count}} shortcut", + "keybindingActions_other": "{{count}} shortcuts", + "syncEntities_one": "Desktop sync", + "syncEntities_other": "Desktop sync" + }, + "consent": { + "installTitle": "Install {{name}}", + "updateTitle": "Update {{name}}", + "agreeUpdate": "Agree and update", + "updateExplain": "This version asks for something the one you have does not, so it will not install on its own.", + "newInVersion": "New in this version", + "minor": "It also adds its own screens and stores its own data, kept separate from other plugins.", + "none": "It asks for no special access." + }, + "confirm": { + "uninstallTitle": "Uninstall {{name}}?", + "uninstallBody": "This stops it and deletes everything it stores, including its settings and secrets. You can install it again later, but the data will not come back.", + "deleteDataTitle": "Delete all data for {{name}}?", + "deleteDataBody": "Its tables, settings, secrets and files are deleted and it is turned off. This cannot be undone.", + "enableAlsoTitle": "Turn on {{name}}?", + "enableAlsoBody": "It needs these to run, so they will be turned on too: {{list}}.", + "disableAlsoTitle": "Turn off {{name}}?", + "disableAlsoBody": "These need it to run, so they will be turned off too: {{list}}.", + "downgradeTitle": "Go back to {{name}} {{version}}?", + "downgradeBody": "Data written by the newer version stays as it is, and an older version may not understand it. The plugin will stay on this version until you unpin it.", + "downgradeConfirm": "Switch version" + }, + "toast": { + "installed": "{{name}} installed", + "installedNotRunning": "{{name}} installed but could not start. Open it to see why.", + "updated": "{{name}} updated to {{version}}", + "updatedMany_one": "{{count}} plugin updated", + "updatedMany_other": "{{count}} plugins updated", + "uninstalled": "{{name}} uninstalled", + "dataDeleted": "Data for {{name}} deleted", + "failedToStart": "{{name}} could not start. Open it to see why." + }, + "errors": { + "generic": "Something went wrong", + "install": "Could not install {{name}}: {{error}}", + "update": "Could not update {{name}}: {{error}}", + "updateAll": "Could not update plugins: {{error}}", + "uninstall": "Could not uninstall {{name}}: {{error}}", + "state": "Could not change {{name}}: {{error}}", + "retry": "Could not restart {{name}}: {{error}}", + "options": "Could not save options for {{name}}: {{error}}", + "deleteData": "Could not delete data for {{name}}: {{error}}", + "managedByServer": "This desktop is linked to a server, which decides its plugins.", + "hasDependents": "{{name}} is needed by {{list}}. Turn those off or uninstall them first.", + "missingDependencies": "{{name}} needs {{list}}, which is not installed." + }, + "data": { + "title": "Stored data", + "delete": "Delete data", + "loadFailed": "Could not read what this plugin stores.", + "tables": "Tables", + "kv": "Keys", + "settings": "Settings", + "files": "Files", + "rows_one": "{{count}} row", + "rows_other": "{{count}} rows", + "secrets_one": "{{count}} secret", + "secrets_other": "{{count}} secrets", + "migrations_one": "{{count}} migration applied", + "migrations_other": "{{count}} migrations applied", + "grants_one": "{{count}} capability granted", + "grants_other": "{{count}} capabilities granted" + }, + "install": "Install", + "uninstall": "Uninstall", + "enable": "Enable", + "disable": "Disable", + "restart": "Restart", + "update": "Update", + "updateAll": "Update all", + "updateTo": "Update to {{version}}", + "review": "Review", + "reviewUpdate": "Review update", + "updates": "Updates", + "auto": "Auto", + "autoUpdate": "Update on its own", + "autoUpdateHint": "Never when the update asks for something new.", + "pin": "Stay on this version", + "pinHint": "Keeps {{version}} and skips every update until you turn this off.", + "pinnedTo": "Pinned to {{version}}", + "version": "Version", + "versionPickHint": "Install an older release instead of the newest. It stays on that version until you unpin it.", + "versionPickHintInstalled": "Switch to another release. Picking one that is not the newest pins it.", + "switchVersion": "Switch", + "latestVersion": "{{version}} (latest)", + "versionChange": "{{from}} to {{to}}", + "versionHistory": "Version history", + "releaseNotes": "Release notes", + "installedBadge": "Installed", + "incompatible": "Needs a newer Termix", + "whatItCanDo": "What it can do", + "addsToTermix": "Adds to Termix", + "addsToTermixHint": "Uninstall and every one of these goes away.", + "noServerAccess": "No access to your servers", + "searchInstalled": "Search installed", + "searchPlugins": "Search plugins", + "allCategories": "All", + "nothingMatches": "Nothing matches that.", + "noneInstalled": "No plugins installed yet. Browse to add some.", + "nothingHere": "Nothing here.", + "nothingHereHint": "Try another category or search.", + "upToDate": "Everything is up to date.", + "lastChecked": "Last checked {{time}}", + "updatesReady_one": "{{count}} update ready.", + "updatesReady_other": "{{count}} updates ready.", + "updatesNeedReview_one": "{{count}} more needs your review first.", + "updatesNeedReview_other": "{{count}} more need your review first.", + "updateAsksMore": "This update asks for more access, so it waits for you.", + "checkRegistry": "Check for plugins and updates", + "registryUnreachable": "Could not reach the plugin registry ({{error}}). Installed plugins still work.", + "managedByServer": "This desktop is linked to a server, which decides its plugins. Changes are made on the server.", + "browsePlugins": "Browse plugins", + "loading": "Loading" } }, "settings": { diff --git a/src/ui/plugin-host/PluginViewPlaceholder.tsx b/src/ui/plugin-host/PluginViewPlaceholder.tsx index 54082d494..b8d411b06 100644 --- a/src/ui/plugin-host/PluginViewPlaceholder.tsx +++ b/src/ui/plugin-host/PluginViewPlaceholder.tsx @@ -1,5 +1,8 @@ import { CircleSlash } from "lucide-react"; import { useTranslation } from "react-i18next"; +import { Button } from "@/components/button"; +import { usePermissions } from "@/hooks/use-permissions"; +import { OPEN_PLUGINS_EVENT } from "@/plugins/open-plugins"; import { usePluginStore } from "./plugin-store"; import { unregisteredViewStatus, type ViewKind } from "./view-ownership"; @@ -22,6 +25,7 @@ export function PluginViewPlaceholder({ const { t } = useTranslation(); // Subscribing re-renders this when plugin state changes. usePluginStore(); + const { has } = usePermissions(); const { status, owner } = unregisteredViewStatus(kind, viewId); const name = owner?.summary.name ?? viewId; @@ -60,6 +64,18 @@ export function PluginViewPlaceholder({ {title} {hint} + {!compact && status !== "failed" && has("admin.plugins.manage") && ( + + )} ); } diff --git a/src/ui/plugins/PluginConsentPrompt.tsx b/src/ui/plugins/PluginConsentPrompt.tsx new file mode 100644 index 000000000..3b5a7b865 --- /dev/null +++ b/src/ui/plugins/PluginConsentPrompt.tsx @@ -0,0 +1,110 @@ +import { useTranslation } from "react-i18next"; +import { Button } from "@/components/button"; +import { Facts } from "@/components/panel-layout"; +import { PanePrompt } from "@/components/surface/surface-scope"; +import { CapabilityRow, PluginIconBox } from "./plugin-bits"; +import { capabilityRisk, orderByRisk, type PluginEntry } from "./plugin-model"; + +export interface ConsentRequest { + plugin: PluginEntry; + mode: "install" | "update"; + version: string; + /** What the prompt lists: all of them to install, only the new ones to update. */ + capabilities: string[]; +} + +/** + * Install and update consent. Capabilities read as consequences, worst first; + * the low risk ones fold into one line so the list stays short enough to read. + */ +export function PluginConsentPrompt({ + request, + busy, + onCancel, + onConfirm, +}: { + request: ConsentRequest | null; + busy?: boolean; + onCancel: () => void; + onConfirm: () => void; +}) { + const { t } = useTranslation(); + if (!request) return null; + const { plugin, mode, version } = request; + const ordered = orderByRisk(request.capabilities); + const notable = ordered.filter((c) => capabilityRisk(c) !== "low"); + const minor = ordered.filter((c) => capabilityRisk(c) === "low"); + + return ( + } + title={ + mode === "update" + ? t("plugins.manager.consent.updateTitle", { name: plugin.name }) + : t("plugins.manager.consent.installTitle", { name: plugin.name }) + } + description={ + + {plugin.author && {plugin.author}} + + {mode === "update" && plugin.version + ? t("plugins.manager.versionChange", { + from: plugin.version, + to: version, + }) + : version} + + + } + actions={ + <> + + + + } + > +
+ {mode === "update" && ( +

+ {t("plugins.manager.consent.updateExplain")} +

+ )} +
+ + {mode === "update" + ? t("plugins.manager.consent.newInVersion") + : t("plugins.manager.whatItCanDo")} + +
+ {notable.map((capability) => ( + + ))} + {minor.length > 0 && ( +
+ {t("plugins.manager.consent.minor")} +
+ )} + {ordered.length === 0 && ( +
+ {t("plugins.manager.consent.none")} +
+ )} +
+
+
+
+ ); +} diff --git a/src/ui/plugins/PluginDetail.tsx b/src/ui/plugins/PluginDetail.tsx new file mode 100644 index 000000000..ee0566134 --- /dev/null +++ b/src/ui/plugins/PluginDetail.tsx @@ -0,0 +1,527 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + Database, + Download, + ExternalLink, + Pin, + RotateCcw, + Trash2, + TriangleAlert, +} from "lucide-react"; +import { Button } from "@/components/button"; +import { Card } from "@/components/card"; +import { Facts, PANEL } from "@/components/panel-layout"; +import { FakeSwitch, SectionCard } from "@/components/section-card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/select"; +import { getPluginData, type PluginDataSummary } from "@/api/plugins-api"; +import { CapabilityRow, PluginIconBox } from "./plugin-bits"; +import { + describeContributions, + formatBytes, + orderByRisk, + type PluginEntry, +} from "./plugin-model"; +import type { PluginsManager } from "./use-plugins-manager"; + +function formatDate(value: string | null | undefined): string { + if (!value) return ""; + const date = new Date(value); + return Number.isNaN(date.getTime()) ? value : date.toLocaleDateString(); +} + +export function PluginDetail({ + plugin, + manager, + onUninstalled, +}: { + plugin: PluginEntry; + manager: PluginsManager; + onUninstalled: () => void; +}) { + const { t } = useTranslation(); + const busy = manager.busy.has(plugin.id) || manager.busy.has("*"); + const locked = manager.managedByServer; + const running = plugin.status === "running"; + const failed = plugin.status === "failed" || plugin.status === "blocked"; + const compatible = plugin.versions.filter((v) => v.compatible); + const [picked, setPicked] = useState( + plugin.version ?? plugin.latestVersion ?? "", + ); + + useEffect(() => { + setPicked(plugin.version ?? plugin.latestVersion ?? ""); + }, [plugin.version, plugin.latestVersion]); + + const contributions = describeContributions(plugin.contributes); + + return ( +
+ + +
+

{plugin.name}

+ + {plugin.description} + + + {plugin.author && {plugin.author}} + {plugin.version ? ( + v{plugin.version} + ) : ( + plugin.latestVersion && v{plugin.latestVersion} + )} + {plugin.category && {plugin.category}} + {plugin.pinnedVersion && ( + + {t("plugins.manager.pinnedTo", { + version: plugin.pinnedVersion, + })} + + )} + +
+
+ {plugin.installed && + plugin.updateAvailable && + !plugin.pinnedVersion && ( + + )} + {plugin.installed ? ( + <> + {failed ? ( + + ) : null} + + + + ) : ( + + )} +
+
+ + {plugin.installed && failed && plugin.lastError && ( +
+ +
+ + {plugin.status === "blocked" + ? t("plugins.manager.status.blocked") + : t("plugins.manager.status.failed")} + + + {plugin.lastError} + +
+
+ )} + +
+
+ +
+ {orderByRisk(plugin.capabilities).map((capability) => ( + + ))} + {plugin.capabilities.length === 0 && ( +
+ {t("plugins.manager.consent.none")} +
+ )} +
+
+ + {plugin.versions.length > 0 && ( + +
+ {plugin.versions.map((v) => ( +
+
+ {v.version} + {v.version === plugin.version && ( + + {t("plugins.manager.installedBadge")} + + )} + {!v.compatible && ( + + {t("plugins.manager.incompatible")} + + )} + + {formatDate(v.publishedAt)} + +
+ {v.releaseNotesUrl && ( + + {t("plugins.manager.releaseNotes")} + + )} +
+ ))} +
+
+ )} + + {plugin.installed && ( + manager.removeData(plugin)} + /> + )} +
+ +
+ {contributions.length > 0 && ( + +
+
+ {contributions.map((c) => ( + + {t(`plugins.manager.contributes.${c.kind}`, { + count: c.count, + })} + + ))} +
+ + {t("plugins.manager.addsToTermixHint")} + +
+
+ )} + + {plugin.inRegistry && compatible.length > 0 && ( + +
+ + {plugin.installed + ? t("plugins.manager.versionPickHintInstalled") + : t("plugins.manager.versionPickHint")} + +
+ + +
+
+
+ )} + + {plugin.installed && ( + +
+
+
+ + {t("plugins.manager.autoUpdate")} + + + {t("plugins.manager.autoUpdateHint")} + +
+ + void manager.setOptions(plugin, { autoUpdate: v }) + } + /> +
+
+
+ + + {t("plugins.manager.pin")} + + + {t("plugins.manager.pinHint", { + version: plugin.version, + })} + +
+ + void manager.setOptions(plugin, { pinned: v }) + } + /> +
+
+
+ )} + + {plugin.repository && ( + + + + {plugin.repository.replace(/^https?:\/\//, "")} + + + )} +
+
+
+ ); +} + +function DataCard({ + plugin, + disabled, + onDelete, +}: { + plugin: PluginEntry; + disabled: boolean; + onDelete: () => Promise; +}) { + const { t } = useTranslation(); + const [data, setData] = useState(null); + const [error, setError] = useState(false); + const [version, setVersion] = useState(0); + + useEffect(() => { + let cancelled = false; + setError(false); + getPluginData(plugin.id) + .then((summary) => { + if (!cancelled) setData(summary); + }) + .catch(() => { + if (!cancelled) setError(true); + }); + return () => { + cancelled = true; + }; + }, [plugin.id, plugin.version, version]); + + const settings = data + ? data.settings.admin + data.settings.user + data.settings.host + : 0; + const rows = data?.tables.reduce((sum, table) => sum + (table.rows ?? 0), 0); + + return ( + } + action={ + + } + > + {error ? ( +
+ {t("plugins.manager.data.loadFailed")} +
+ ) : !data ? ( +
+ {t("plugins.manager.loading")} +
+ ) : ( +
+
+ + + + +
+ {data.tables.length > 0 && ( +
+ {data.tables.map((table) => ( +
+ + {table.name} + + + {table.rows === null + ? "-" + : t("plugins.manager.data.rows", { count: table.rows })} + +
+ ))} +
+ )} +
+ + {t("plugins.manager.data.migrations", { + count: data.migrations.length, + })} + + + {t("plugins.manager.data.grants", { count: data.grants.length })} + +
+
+ )} +
+ ); +} + +function Stat({ + label, + value, + hint, +}: { + label: string; + value: string; + hint?: string; +}) { + return ( +
+ + {label} + + {value} + {hint && ( + {hint} + )} +
+ ); +} diff --git a/src/ui/plugins/PluginsTab.tsx b/src/ui/plugins/PluginsTab.tsx new file mode 100644 index 000000000..910ab5680 --- /dev/null +++ b/src/ui/plugins/PluginsTab.tsx @@ -0,0 +1,818 @@ +import { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + ArrowDownWideNarrow, + Check, + Download, + Globe, + Puzzle, + RefreshCw, + RotateCcw, + Search, + Trash2, + TriangleAlert, +} from "lucide-react"; +import { Button } from "@/components/button"; +import { Card } from "@/components/card"; +import { Input } from "@/components/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/select"; +import { EmptyState } from "@/components/empty-state"; +import { TabStrip } from "@/components/tab-strip"; +import { FakeSwitch } from "@/components/section-card"; +import { + Facts, + GroupHeading, + PANEL, + PanelShell, +} from "@/components/panel-layout"; +import { InlineView, SurfaceScope } from "@/components/surface/surface-scope"; +import { cn } from "@/lib/utils"; +import { PluginIconBox } from "./plugin-bits"; +import { PluginConsentPrompt } from "./PluginConsentPrompt"; +import { PluginDetail } from "./PluginDetail"; +import { + categoriesOf, + describeContributions, + isSevere, + matchesQuery, + orderByRisk, + sortPlugins, + type PluginEntry, + type SortKey, +} from "./plugin-model"; +import { usePluginsManager, type PluginsManager } from "./use-plugins-manager"; + +type Section = "installed" | "browse" | "updates"; + +export function PluginsTab() { + return ( + + + + ); +} + +function PluginsTabBody() { + const { t } = useTranslation(); + const manager = usePluginsManager(); + const [section, setSection] = useState
("installed"); + const [query, setQuery] = useState(""); + const [detailId, setDetailId] = useState(null); + + const installed = manager.plugins.filter((p) => p.installed); + const available = manager.plugins.filter((p) => p.inRegistry); + const updates = installed.filter((p) => p.updateAvailable); + const running = installed.filter((p) => p.status === "running").length; + const detail = manager.plugins.find((p) => p.id === detailId) ?? null; + + const tabs = [ + { + id: "installed", + label: t("plugins.manager.tabs.installed"), + icon: , + count: installed.length, + }, + { + id: "browse", + label: t("plugins.manager.tabs.browse"), + icon: , + }, + { + id: "updates", + label: t("plugins.manager.tabs.updates"), + icon: , + count: updates.filter((p) => !p.pinnedVersion).length || undefined, + }, + ]; + + return ( + <> + } + title={t("nav.plugins")} + status={t("plugins.manager.status.summary", { + installed: installed.length, + running, + })} + scroll={false} + actions={ + + } + tabs={ + setSection(id as Section)} + /> + } + > +
+ {manager.managedByServer && ( + {t("plugins.manager.managedByServer")} + )} + {manager.registryError && ( + + {t("plugins.manager.registryUnreachable", { + error: manager.registryError, + })} + + )} + {manager.loading ? ( +
+ +
+ ) : section === "installed" ? ( + + ) : section === "browse" ? ( + + ) : ( + + )} +
+
+ + !open && setDetailId(null)} + title={detail?.name ?? ""} + icon={} + width="wide" + bare + > + {detail && ( + setDetailId(null)} + /> + )} + + + manager.setConsent(null)} + onConfirm={() => void manager.confirmConsent()} + /> + + ); +} + +function Notice({ children }: { children: React.ReactNode }) { + return ( +
+ + + {children} + +
+ ); +} + +function SortSelect({ + value, + onChange, +}: { + value: SortKey; + onChange: (next: SortKey) => void; +}) { + const { t } = useTranslation(); + const keys: SortKey[] = ["name", "updated", "category"]; + return ( + + ); +} + +function Toolbar({ + query, + onQuery, + placeholder, + children, +}: { + query: string; + onQuery: (v: string) => void; + placeholder: string; + children?: React.ReactNode; +}) { + return ( +
+
+ + onQuery(e.target.value)} + placeholder={placeholder} + className="h-8 pl-8 text-xs" + /> +
+ {children} +
+ ); +} + +function Group({ + title, + count, + children, +}: { + title: string; + count: number; + children: React.ReactNode; +}) { + return ( +
+ +
+ {children} +
+
+ ); +} + +function InstalledSection({ + plugins, + manager, + query, + onQuery, + onOpen, +}: { + plugins: PluginEntry[]; + manager: PluginsManager; + query: string; + onQuery: (v: string) => void; + onOpen: (id: string) => void; +}) { + const { t } = useTranslation(); + const [sort, setSort] = useState("name"); + const rows = sortPlugins( + plugins.filter((p) => matchesQuery(p, query)), + sort, + ); + const running = rows.filter((p) => p.status === "running"); + const stopped = rows.filter((p) => p.status !== "running"); + const pending = plugins.filter((p) => p.updateAvailable && !p.pinnedVersion); + + return ( +
+ +
+ + +
+
+ + {rows.length === 0 ? ( + + ) : ( + <> + {running.length > 0 && ( + + {running.map((p) => ( + onOpen(p.id)} + /> + ))} + + )} + {stopped.length > 0 && ( + + {stopped.map((p) => ( + onOpen(p.id)} + /> + ))} + + )} + + )} +
+ ); +} + +function CardHead({ + plugin, + muted, + facts, + onOpen, +}: { + plugin: PluginEntry; + muted?: boolean; + facts: React.ReactNode; + onOpen: () => void; +}) { + return ( + + ); +} + +function InstalledCard({ + plugin, + manager, + onOpen, +}: { + plugin: PluginEntry; + manager: PluginsManager; + onOpen: () => void; +}) { + const { t } = useTranslation(); + const running = plugin.status === "running"; + const failed = plugin.status === "failed" || plugin.status === "blocked"; + const busy = manager.busy.has(plugin.id) || manager.busy.has("*"); + const locked = manager.managedByServer; + const adds = describeContributions(plugin.contributes) + .slice(0, 3) + .map((c) => t(`plugins.manager.contributes.${c.kind}`, { count: c.count })); + + return ( + + + {plugin.author && {plugin.author}} + v{plugin.version} + {adds.length > 0 && {adds.join(", ")}} + + } + /> + + {failed ? ( +
+ + + {plugin.status === "blocked" + ? t("plugins.manager.status.blocked") + : t("plugins.manager.status.failed")} + +
+ ) : !running ? ( +
+ + {t("plugins.manager.status.disabled")} + +
+ ) : ( +
+ + + {plugin.pinnedVersion + ? t("plugins.manager.pinnedTo", { + version: plugin.pinnedVersion, + }) + : t("plugins.manager.status.running")} + +
+ )} + +
+ {plugin.updateAvailable && !plugin.pinnedVersion && !failed && ( + + )} + {failed ? ( + + ) : ( + + )} + +
+
+ ); +} + +function BrowseSection({ + plugins, + manager, + query, + onQuery, + onOpen, +}: { + plugins: PluginEntry[]; + manager: PluginsManager; + query: string; + onQuery: (v: string) => void; + onOpen: (id: string) => void; +}) { + const { t } = useTranslation(); + const [category, setCategory] = useState(null); + const [sort, setSort] = useState("name"); + const categories = useMemo(() => categoriesOf(plugins), [plugins]); + + const rows = sortPlugins( + plugins.filter( + (p) => + matchesQuery(p, query) && + (category === null || p.category === category), + ), + sort, + ); + + return ( +
+ +
+ +
+
+ + {categories.length > 0 && ( +
+ {[null, ...categories].map((c) => ( + + ))} +
+ )} + + {rows.length === 0 ? ( + + ) : ( +
+ {rows.map((p) => ( + onOpen(p.id)} + /> + ))} +
+ )} +
+ ); +} + +function BrowseCard({ + plugin, + manager, + onOpen, +}: { + plugin: PluginEntry; + manager: PluginsManager; + onOpen: () => void; +}) { + const { t } = useTranslation(); + const severe = orderByRisk(plugin.capabilities).filter(isSevere); + const busy = manager.busy.has(plugin.id) || manager.busy.has("*"); + + return ( + + + {plugin.author && {plugin.author}} + {plugin.latestVersion && v{plugin.latestVersion}} + {plugin.category && {plugin.category}} + + } + /> +
+ + {severe.length > 0 + ? t(`plugins.capabilities.${severe[0]}.title`, { + defaultValue: severe[0], + nsSeparator: false, + }) + : t("plugins.manager.noServerAccess")} + + {plugin.installed ? ( + + + {t("plugins.manager.installedBadge")} + + ) : ( + + )} +
+
+ ); +} + +function UpdatesSection({ + plugins, + manager, + onOpen, +}: { + plugins: PluginEntry[]; + manager: PluginsManager; + onOpen: (id: string) => void; +}) { + const { t } = useTranslation(); + const pinned = plugins.filter((p) => p.pinnedVersion); + const open = plugins.filter((p) => !p.pinnedVersion); + const gated = open.filter((p) => p.addedCapabilities.length > 0); + const plain = open.filter((p) => p.addedCapabilities.length === 0); + + if (plugins.length === 0) { + return ( +
+ +
+ ); + } + + return ( +
+ {plain.length > 0 && ( + + + {t("plugins.manager.updatesReady", { count: plain.length })} + {gated.length > 0 && + ` ${t("plugins.manager.updatesNeedReview", { count: gated.length })}`} + + + + )} + +
+ {[...gated, ...plain, ...pinned].map((p) => ( + onOpen(p.id)} + /> + ))} +
+
+ ); +} + +function UpdateRow({ + plugin, + manager, + onOpen, +}: { + plugin: PluginEntry; + manager: PluginsManager; + onOpen: () => void; +}) { + const { t } = useTranslation(); + const gated = plugin.addedCapabilities.length > 0; + const busy = manager.busy.has(plugin.id) || manager.busy.has("*"); + const notes = plugin.versions.find((v) => v.version === plugin.latestVersion); + + return ( + +
+ +
+
+ + + {t("plugins.manager.versionChange", { + from: plugin.version, + to: plugin.latestVersion, + })} + +
+ {plugin.pinnedVersion ? ( + + {t("plugins.manager.pinnedTo", { version: plugin.pinnedVersion })} + + ) : gated ? ( + + {t("plugins.manager.updateAsksMore")} + + ) : null} + {notes?.releaseNotesUrl && ( + + {t("plugins.manager.releaseNotes")} + + )} +
+
+ + {!plugin.pinnedVersion && ( + + )} +
+
+
+ ); +} diff --git a/src/ui/plugins/open-plugins.ts b/src/ui/plugins/open-plugins.ts new file mode 100644 index 000000000..89591ee83 --- /dev/null +++ b/src/ui/plugins/open-plugins.ts @@ -0,0 +1,2 @@ +/** Asks the shell to open the Plugins tab. */ +export const OPEN_PLUGINS_EVENT = "termix:open-plugins"; diff --git a/src/ui/plugins/plugin-bits.tsx b/src/ui/plugins/plugin-bits.tsx new file mode 100644 index 000000000..0d9484d67 --- /dev/null +++ b/src/ui/plugins/plugin-bits.tsx @@ -0,0 +1,76 @@ +import { useTranslation } from "react-i18next"; +import { PluginIcon } from "@/lib/plugin-icon"; +import { cn } from "@/lib/utils"; +import { capabilityRisk, isSevere } from "./plugin-model"; + +export function PluginIconBox({ + name, + size = "md", + muted, +}: { + name?: string; + size?: "sm" | "md" | "lg"; + muted?: boolean; +}) { + const box = size === "lg" ? "size-10" : size === "sm" ? "size-7" : "size-9"; + const glyph = + size === "lg" ? "size-5" : size === "sm" ? "size-3.5" : "size-4"; + return ( +
+ +
+ ); +} + +/** One capability as its consequence, with a mark only on the severe ones. */ +export function CapabilityRow({ + capability, + showId, + className, +}: { + capability: string; + showId?: boolean; + className?: string; +}) { + const { t } = useTranslation(); + const severe = isSevere(capability); + return ( +
+ + {severe && ( + + ! + + )} + +
+ + {t(`plugins.capabilities.${capability}.title`, { + defaultValue: capability, + nsSeparator: false, + })} + + + {t(`plugins.capabilities.${capability}.consequence`, { + defaultValue: "", + nsSeparator: false, + })} + +
+ {showId && ( + + {capability} + + )} +
+ ); +} diff --git a/src/ui/plugins/plugin-model.ts b/src/ui/plugins/plugin-model.ts new file mode 100644 index 000000000..92f2f7d9a --- /dev/null +++ b/src/ui/plugins/plugin-model.ts @@ -0,0 +1,232 @@ +import { + CAPABILITY_RISK_ORDER, + getCapabilityInfo, + type CapabilityRisk, +} from "@termix-ssh/plugin-sdk/capabilities"; +import type { + PluginSummary, + RegistryPluginEntry, + RegistryPluginVersion, +} from "@/api/plugins-api"; + +export type PluginStatus = "running" | "disabled" | "failed" | "blocked"; + +/** One row of the Plugins tab: what is installed merged with the registry. */ +export interface PluginEntry { + id: string; + name: string; + description: string; + author: string; + category: string; + icon?: string; + repository?: string; + installed: boolean; + version: string | null; + latestVersion: string | null; + updateAvailable: boolean; + addedCapabilities: string[]; + versions: RegistryPluginVersion[]; + capabilities: string[]; + status: PluginStatus | null; + enabled: boolean; + lastError: string | null; + autoUpdate: boolean; + pinnedVersion: string | null; + bundled: boolean; + inRegistry: boolean; + contributes: Record | null; + publishedAt: string | null; +} + +export function pluginStatus(summary: PluginSummary): PluginStatus { + if (summary.state === "failed") return "failed"; + if (summary.state === "blocked") return "blocked"; + if (!summary.enabled) return "disabled"; + return "running"; +} + +export function mergePlugins( + installed: PluginSummary[], + registry: RegistryPluginEntry[] | null, +): PluginEntry[] { + const byId = new Map((registry ?? []).map((entry) => [entry.id, entry])); + const rows: PluginEntry[] = []; + + for (const summary of installed) { + const entry = byId.get(summary.id); + byId.delete(summary.id); + rows.push({ + id: summary.id, + name: summary.name, + description: summary.description ?? entry?.description ?? "", + author: summary.author ?? entry?.author ?? "", + category: entry?.category ?? "", + icon: summary.icon ?? entry?.icon, + repository: summary.repository ?? entry?.repository, + installed: true, + version: summary.version, + latestVersion: entry?.latestVersion ?? null, + updateAvailable: entry?.updateAvailable ?? false, + addedCapabilities: entry?.addedCapabilities ?? [], + versions: entry?.versions ?? [], + capabilities: summary.capabilities ?? [], + status: pluginStatus(summary), + enabled: summary.enabled, + lastError: summary.lastError ?? null, + autoUpdate: summary.autoUpdate ?? entry?.autoUpdate ?? false, + pinnedVersion: summary.pinnedVersion ?? entry?.pinnedVersion ?? null, + bundled: entry?.bundled ?? summary.tier === "bundled", + inRegistry: Boolean(entry), + contributes: (summary.contributes as Record) ?? null, + publishedAt: latestPublished(entry?.versions ?? []), + }); + } + + for (const entry of byId.values()) { + const latest = + entry.versions.find((v) => v.version === entry.latestVersion) ?? + entry.versions[0]; + rows.push({ + id: entry.id, + name: entry.name, + description: entry.description, + author: entry.author, + category: entry.category, + icon: entry.icon, + repository: entry.repository, + installed: false, + version: null, + latestVersion: entry.latestVersion, + updateAvailable: false, + addedCapabilities: [], + versions: entry.versions, + capabilities: latest?.capabilities ?? [], + status: null, + enabled: false, + lastError: null, + autoUpdate: false, + pinnedVersion: null, + bundled: entry.bundled, + inRegistry: true, + contributes: null, + publishedAt: latestPublished(entry.versions), + }); + } + + return rows; +} + +function latestPublished(versions: RegistryPluginVersion[]): string | null { + const dates = versions + .map((v) => v.publishedAt) + .filter((d): d is string => Boolean(d)) + .sort(); + return dates.at(-1) ?? null; +} + +export type SortKey = "name" | "updated" | "category"; + +export function sortPlugins(rows: PluginEntry[], key: SortKey): PluginEntry[] { + const sorted = [...rows]; + if (key === "updated") { + sorted.sort((a, b) => + (b.publishedAt ?? "").localeCompare(a.publishedAt ?? ""), + ); + } else if (key === "category") { + sorted.sort( + (a, b) => + a.category.localeCompare(b.category) || a.name.localeCompare(b.name), + ); + } else { + sorted.sort((a, b) => a.name.localeCompare(b.name)); + } + return sorted; +} + +export function matchesQuery(row: PluginEntry, query: string): boolean { + const q = query.trim().toLowerCase(); + if (!q) return true; + return ( + row.name.toLowerCase().includes(q) || + row.description.toLowerCase().includes(q) || + row.id.includes(q) + ); +} + +export function categoriesOf(rows: PluginEntry[]): string[] { + return [...new Set(rows.map((row) => row.category).filter(Boolean))].sort(); +} + +export function capabilityRisk(capability: string): CapabilityRisk { + return getCapabilityInfo(capability)?.risk ?? "medium"; +} + +/** Worst first, the order the consent copy is read in. */ +export function orderByRisk(capabilities: string[]): string[] { + return [...capabilities].sort( + (a, b) => + CAPABILITY_RISK_ORDER.indexOf(capabilityRisk(a)) - + CAPABILITY_RISK_ORDER.indexOf(capabilityRisk(b)), + ); +} + +export function isSevere(capability: string): boolean { + const risk = capabilityRisk(capability); + return risk === "critical" || risk === "high"; +} + +/** Contribution kinds worth naming in "Adds to Termix", with a count each. */ +const CONTRIBUTION_KINDS = [ + "tabs", + "panels", + "dashboardCards", + "actions", + "protocols", + "auth", + "settings", + "hostCapability", + "permissions", + "keybindingActions", + "syncEntities", +] as const; + +export type ContributionKind = (typeof CONTRIBUTION_KINDS)[number]; + +export function describeContributions( + contributes: Record | null, +): Array<{ kind: ContributionKind; count: number }> { + if (!contributes) return []; + const result: Array<{ kind: ContributionKind; count: number }> = []; + for (const kind of CONTRIBUTION_KINDS) { + const value = contributes[kind]; + if (value === undefined || value === null || value === false) continue; + if (Array.isArray(value)) { + if (value.length > 0) result.push({ kind, count: value.length }); + continue; + } + if (kind === "auth" && typeof value === "object") { + const auth = value as { + loginMethods?: unknown[]; + secondFactors?: unknown[]; + }; + const count = + (auth.loginMethods?.length ?? 0) + (auth.secondFactors?.length ?? 0); + if (count > 0) result.push({ kind, count }); + continue; + } + result.push({ kind, count: 1 }); + } + return result; +} + +export function formatBytes(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + const units = ["KB", "MB", "GB"]; + let value = bytes / 1024; + let unit = 0; + while (value >= 1024 && unit < units.length - 1) { + value /= 1024; + unit += 1; + } + return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[unit]}`; +} diff --git a/src/ui/plugins/use-plugins-manager.ts b/src/ui/plugins/use-plugins-manager.ts new file mode 100644 index 000000000..b26468be5 --- /dev/null +++ b/src/ui/plugins/use-plugins-manager.ts @@ -0,0 +1,455 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import semver from "semver"; +import { + deletePluginData, + getPluginRegistry, + getPlugins, + installPlugin, + previewPluginState, + retryPlugin, + setPluginOptions, + setPluginState, + uninstallPlugin, + updateAllPlugins, + updatePlugin, + type PluginSummary, + type RegistryListing, +} from "@/api/plugins-api"; +import { useConfirm } from "@/components/surface/surface-scope"; +import { mergePlugins, type PluginEntry } from "./plugin-model"; +import type { ConsentRequest } from "./PluginConsentPrompt"; + +interface ApiError { + message: string; + code?: string; + data: Record; +} + +function readError(error: unknown, fallback: string): ApiError { + const data = (error as { response?: { data?: Record } }) + ?.response?.data; + if (data && typeof data === "object") { + return { + message: typeof data.error === "string" ? data.error : fallback, + code: typeof data.code === "string" ? data.code : undefined, + data, + }; + } + return { + message: error instanceof Error ? error.message : fallback, + data: {}, + }; +} + +export function usePluginsManager() { + const { t } = useTranslation(); + const confirm = useConfirm(); + const [installed, setInstalled] = useState([]); + const [registry, setRegistry] = useState(null); + const [registryError, setRegistryError] = useState(null); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [busy, setBusy] = useState>(new Set()); + const [consent, setConsent] = useState(null); + const mounted = useRef(true); + + useEffect(() => { + mounted.current = true; + return () => { + mounted.current = false; + }; + }, []); + + const load = useCallback(async (refresh = false) => { + if (refresh) setRefreshing(true); + const [plugins, listing] = await Promise.allSettled([ + getPlugins(), + getPluginRegistry(refresh), + ]); + if (!mounted.current) return; + if (plugins.status === "fulfilled") setInstalled(plugins.value); + if (listing.status === "fulfilled") { + setRegistry(listing.value); + setRegistryError(listing.value.registry.error); + } else { + setRegistryError(readError(listing.reason, "").message || "unreachable"); + } + setLoading(false); + setRefreshing(false); + }, []); + + useEffect(() => { + void load(); + }, [load]); + + const plugins = useMemo( + () => mergePlugins(installed, registry?.plugins ?? null), + [installed, registry], + ); + + const run = useCallback( + async (id: string, action: () => Promise) => { + setBusy((current) => new Set(current).add(id)); + try { + await action(); + } finally { + if (mounted.current) { + setBusy((current) => { + const next = new Set(current); + next.delete(id); + return next; + }); + } + await load(); + } + }, + [load], + ); + + const failToast = useCallback( + (error: unknown, key: string, name: string) => { + const info = readError(error, t("plugins.manager.errors.generic")); + if (info.code === "MANAGED_BY_SERVER") { + toast.error(t("plugins.manager.errors.managedByServer")); + } else if (info.code === "HAS_DEPENDENTS") { + toast.error( + t("plugins.manager.errors.hasDependents", { + name, + list: ((info.data.dependents as string[]) ?? []).join(", "), + }), + ); + } else if (info.code === "MISSING_DEPENDENCIES") { + toast.error( + t("plugins.manager.errors.missingDependencies", { + name, + list: ((info.data.missing as string[]) ?? []).join(", "), + }), + ); + } else { + toast.error(t(key, { name, error: info.message })); + } + }, + [t], + ); + + const versionCapabilities = (plugin: PluginEntry, version: string) => + plugin.versions.find((v) => v.version === version)?.capabilities ?? []; + + const requestInstall = useCallback( + (plugin: PluginEntry, version?: string) => { + const target = version ?? plugin.latestVersion; + if (!target) return; + setConsent({ + plugin, + mode: "install", + version: target, + capabilities: versionCapabilities(plugin, target), + }); + }, + [], + ); + + const doInstall = useCallback( + (plugin: PluginEntry, version: string) => + run(plugin.id, async () => { + try { + const result = await installPlugin( + plugin.id, + version === plugin.latestVersion ? undefined : version, + ); + if (result.state === "failed" || result.state === "blocked") { + toast.error( + t("plugins.manager.toast.installedNotRunning", { + name: plugin.name, + }), + ); + } else { + toast.success( + t("plugins.manager.toast.installed", { name: plugin.name }), + ); + } + } catch (error) { + failToast(error, "plugins.manager.errors.install", plugin.name); + } + }), + [run, t, failToast], + ); + + const doUpdate = useCallback( + (plugin: PluginEntry, version: string, acceptCapabilities: boolean) => + run(plugin.id, async () => { + try { + await updatePlugin(plugin.id, { + version: version === plugin.latestVersion ? undefined : version, + acceptCapabilities, + }); + toast.success( + t("plugins.manager.toast.updated", { + name: plugin.name, + version, + }), + ); + } catch (error) { + const info = readError(error, ""); + if (info.code === "CAPABILITIES_ADDED") { + setConsent({ + plugin, + mode: "update", + version, + capabilities: (info.data.capabilities as string[]) ?? [], + }); + return; + } + failToast(error, "plugins.manager.errors.update", plugin.name); + } + }), + [run, t, failToast], + ); + + /** Updates, or changes to another version. New capabilities ask first. */ + const requestUpdate = useCallback( + async (plugin: PluginEntry, version?: string) => { + const target = version ?? plugin.latestVersion; + if (!target || !plugin.version) return; + if ( + semver.valid(target) && + semver.valid(plugin.version) && + semver.lt(target, plugin.version) + ) { + const ok = await confirm({ + title: t("plugins.manager.confirm.downgradeTitle", { + name: plugin.name, + version: target, + }), + description: t("plugins.manager.confirm.downgradeBody"), + confirmLabel: t("plugins.manager.confirm.downgradeConfirm"), + destructive: false, + }); + if (!ok) return; + } + const have = new Set(plugin.capabilities); + const added = versionCapabilities(plugin, target).filter( + (c) => !have.has(c), + ); + if (added.length > 0) { + setConsent({ + plugin, + mode: "update", + version: target, + capabilities: added, + }); + return; + } + await doUpdate(plugin, target, false); + }, + [confirm, t, doUpdate], + ); + + const confirmConsent = useCallback(async () => { + if (!consent) return; + const request = consent; + setConsent(null); + if (request.mode === "install") { + await doInstall(request.plugin, request.version); + } else { + await doUpdate(request.plugin, request.version, true); + } + }, [consent, doInstall, doUpdate]); + + const nameOf = useCallback( + (id: string) => plugins.find((p) => p.id === id)?.name ?? id, + [plugins], + ); + + const toggle = useCallback( + async (plugin: PluginEntry) => { + const enable = !plugin.enabled; + try { + const preview = await previewPluginState(plugin.id, enable); + if (enable && preview.missing.length > 0) { + toast.error( + t("plugins.manager.errors.missingDependencies", { + name: plugin.name, + list: preview.missing.join(", "), + }), + ); + return; + } + const others = enable ? preview.enable : preview.disable; + if (others.length > 0) { + const ok = await confirm({ + title: enable + ? t("plugins.manager.confirm.enableAlsoTitle", { + name: plugin.name, + }) + : t("plugins.manager.confirm.disableAlsoTitle", { + name: plugin.name, + }), + description: enable + ? t("plugins.manager.confirm.enableAlsoBody", { + list: others.map(nameOf).join(", "), + }) + : t("plugins.manager.confirm.disableAlsoBody", { + list: others.map(nameOf).join(", "), + }), + confirmLabel: enable + ? t("plugins.manager.enable") + : t("plugins.manager.disable"), + destructive: !enable, + }); + if (!ok) return; + } + } catch (error) { + failToast(error, "plugins.manager.errors.state", plugin.name); + return; + } + await run(plugin.id, async () => { + try { + const result = await setPluginState(plugin.id, enable); + if (result.state === "failed" || result.state === "blocked") { + toast.error( + t("plugins.manager.toast.failedToStart", { name: plugin.name }), + ); + } + } catch (error) { + failToast(error, "plugins.manager.errors.state", plugin.name); + } + }); + }, + [confirm, t, run, failToast, nameOf], + ); + + const retry = useCallback( + (plugin: PluginEntry) => + run(plugin.id, async () => { + try { + await retryPlugin(plugin.id); + } catch (error) { + failToast(error, "plugins.manager.errors.retry", plugin.name); + } + }), + [run, failToast], + ); + + const uninstall = useCallback( + async (plugin: PluginEntry): Promise => { + const ok = await confirm({ + title: t("plugins.manager.confirm.uninstallTitle", { + name: plugin.name, + }), + description: t("plugins.manager.confirm.uninstallBody"), + confirmLabel: t("plugins.manager.uninstall"), + }); + if (!ok) return false; + let done = false; + await run(plugin.id, async () => { + try { + await uninstallPlugin(plugin.id); + done = true; + toast.success( + t("plugins.manager.toast.uninstalled", { name: plugin.name }), + ); + } catch (error) { + failToast(error, "plugins.manager.errors.uninstall", plugin.name); + } + }); + return done; + }, + [confirm, t, run, failToast], + ); + + const removeData = useCallback( + async (plugin: PluginEntry): Promise => { + const ok = await confirm({ + title: t("plugins.manager.confirm.deleteDataTitle", { + name: plugin.name, + }), + description: t("plugins.manager.confirm.deleteDataBody"), + confirmLabel: t("plugins.manager.data.delete"), + }); + if (!ok) return false; + let done = false; + await run(plugin.id, async () => { + try { + await deletePluginData(plugin.id); + done = true; + toast.success( + t("plugins.manager.toast.dataDeleted", { name: plugin.name }), + ); + } catch (error) { + failToast(error, "plugins.manager.errors.deleteData", plugin.name); + } + }); + return done; + }, + [confirm, t, run, failToast], + ); + + const setOptions = useCallback( + ( + plugin: PluginEntry, + options: { autoUpdate?: boolean; pinned?: boolean }, + ) => + run(plugin.id, async () => { + try { + await setPluginOptions(plugin.id, options); + } catch (error) { + failToast(error, "plugins.manager.errors.options", plugin.name); + } + }), + [run, failToast], + ); + + const updateAll = useCallback( + () => + run("*", async () => { + try { + const result = await updateAllPlugins(); + if (result.updated.length > 0) { + toast.success( + t("plugins.manager.toast.updatedMany", { + count: result.updated.length, + }), + ); + } + for (const failed of result.failed) { + toast.error( + t("plugins.manager.errors.update", { + name: nameOf(failed.id), + error: failed.error, + }), + ); + } + } catch (error) { + failToast(error, "plugins.manager.errors.updateAll", ""); + } + }), + [run, t, failToast, nameOf], + ); + + return { + plugins, + loading, + refreshing, + registryError, + managedByServer: registry?.managedByServer ?? false, + lastCheckedAt: registry?.registry.lastCheckedAt ?? null, + busy, + consent, + setConsent, + refresh: () => load(true), + requestInstall, + requestUpdate, + confirmConsent, + toggle, + retry, + uninstall, + removeData, + setOptions, + updateAll, + }; +} + +export type PluginsManager = ReturnType; diff --git a/src/ui/shell/shell-layout.ts b/src/ui/shell/shell-layout.ts index a3c0a1c30..2c6f0dca8 100644 --- a/src/ui/shell/shell-layout.ts +++ b/src/ui/shell/shell-layout.ts @@ -26,6 +26,7 @@ const CORE_UNSAVED = new Set([ "user-profile", "admin-settings", "split-screen", + "plugins", ]); /** diff --git a/src/ui/shell/tabUtils.tsx b/src/ui/shell/tabUtils.tsx index 1c183d541..4b314eb7c 100644 --- a/src/ui/shell/tabUtils.tsx +++ b/src/ui/shell/tabUtils.tsx @@ -2,6 +2,7 @@ import { LayoutDashboard, LayoutPanelLeft, + Puzzle, LibraryBig, Server, Settings, @@ -30,6 +31,9 @@ const ManageTab = lazy(() => const SettingsTab = lazy(() => import("@/settings/SettingsTab").then((m) => ({ default: m.SettingsTab })), ); +const PluginsTab = lazy(() => + import("@/plugins/PluginsTab").then((m) => ({ default: m.PluginsTab })), +); const DashboardTab = lazy(() => import("@/dashboard/DashboardTab").then((m) => ({ default: m.DashboardTab, @@ -105,6 +109,8 @@ export function tabIcon(type: TabType) { return ; case "split-screen": return ; + case "plugins": + return ; default: { const Icon = getTabType(type)?.icon; return Icon ? : null; @@ -236,6 +242,9 @@ export function renderTabContent(tab: Tab, context: TabRenderContext) { case "host-manager": return withTabSuspense(); + case "plugins": + return withTabSuspense(); + case "settings": return context.settings ? withTabSuspense( diff --git a/src/ui/sidebar/rail-items.ts b/src/ui/sidebar/rail-items.ts index 74c5e1d68..2f63536a8 100644 --- a/src/ui/sidebar/rail-items.ts +++ b/src/ui/sidebar/rail-items.ts @@ -4,6 +4,7 @@ import { Plug, Server, LibraryBig, + Puzzle, Settings, Zap, type LucideIcon, @@ -118,6 +119,15 @@ export const RAIL_ITEMS: RailItemDef[] = [ group: "system", useBadge: useSyncAttentionCount, }, + { + id: "plugins", + icon: Puzzle, + labelKey: "nav.plugins", + kind: "tab", + placement: "footer", + group: "system", + permission: "admin.plugins.manage", + }, ]; /** Main rail items split into their bands, in band order, empty bands left out. */ diff --git a/src/ui/tests/plugins/PluginConsentPrompt.test.tsx b/src/ui/tests/plugins/PluginConsentPrompt.test.tsx new file mode 100644 index 000000000..9d363923d --- /dev/null +++ b/src/ui/tests/plugins/PluginConsentPrompt.test.tsx @@ -0,0 +1,90 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})); + +import { PluginConsentPrompt } from "@/plugins/PluginConsentPrompt"; +import { mergePlugins } from "@/plugins/plugin-model"; + +afterEach(cleanup); + +const [plugin] = mergePlugins( + [], + [ + { + id: "docker", + name: "Docker", + description: "", + author: "Termix", + category: "", + versions: [], + latestVersion: "1.0.0", + installed: false, + installedVersion: null, + updateAvailable: false, + addedCapabilities: [], + pinnedVersion: null, + autoUpdate: false, + bundled: false, + }, + ], +); + +describe("PluginConsentPrompt", () => { + it("lists notable capabilities and folds the low risk ones", () => { + render( + {}} + onConfirm={() => {}} + />, + ); + expect( + screen.getByText("plugins.capabilities.ssh:connect.title"), + ).toBeTruthy(); + expect(screen.queryByText("plugins.capabilities.kv:own.title")).toBeNull(); + expect(screen.getByText("plugins.manager.consent.minor")).toBeTruthy(); + }); + + it("confirms and cancels", () => { + const onConfirm = vi.fn(); + const onCancel = vi.fn(); + render( + , + ); + expect( + screen.getByText("plugins.manager.consent.updateExplain"), + ).toBeTruthy(); + fireEvent.click(screen.getByText("plugins.manager.consent.agreeUpdate")); + fireEvent.click(screen.getByText("common.cancel")); + expect(onConfirm).toHaveBeenCalledOnce(); + expect(onCancel).toHaveBeenCalledOnce(); + }); + + it("renders nothing without a request", () => { + const { container } = render( + {}} + onConfirm={() => {}} + />, + ); + expect(container.innerHTML).toBe(""); + }); +}); diff --git a/src/ui/tests/plugins/plugin-bits.test.tsx b/src/ui/tests/plugins/plugin-bits.test.tsx new file mode 100644 index 000000000..45ad2779d --- /dev/null +++ b/src/ui/tests/plugins/plugin-bits.test.tsx @@ -0,0 +1,39 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { cleanup, render, screen } from "@testing-library/react"; +import i18next from "i18next"; +import { I18nextProvider, initReactI18next } from "react-i18next"; +import { CapabilityRow } from "@/plugins/plugin-bits"; + +afterEach(cleanup); + +describe("CapabilityRow", () => { + it("resolves capability ids that contain a colon", async () => { + const i18n = i18next.createInstance(); + await i18n.use(initReactI18next).init({ + lng: "en", + resources: { + en: { + translation: { + plugins: { + capabilities: { + "users:impersonate": { + title: "Act as other users", + consequence: "It can do anything a user can.", + }, + }, + }, + }, + }, + }, + }); + + render( + + + , + ); + + expect(screen.getByText("Act as other users")).toBeTruthy(); + expect(screen.getByText("It can do anything a user can.")).toBeTruthy(); + }); +}); diff --git a/src/ui/tests/plugins/plugin-model.test.ts b/src/ui/tests/plugins/plugin-model.test.ts new file mode 100644 index 000000000..7c533d851 --- /dev/null +++ b/src/ui/tests/plugins/plugin-model.test.ts @@ -0,0 +1,174 @@ +import { describe, expect, it } from "vitest"; +import type { PluginSummary, RegistryPluginEntry } from "@/api/plugins-api"; +import { + categoriesOf, + describeContributions, + formatBytes, + matchesQuery, + mergePlugins, + orderByRisk, + pluginStatus, + sortPlugins, +} from "@/plugins/plugin-model"; + +function summary(overrides: Partial = {}): PluginSummary { + return { + id: "docker", + name: "Docker", + version: "1.0.0", + enabled: true, + state: "active", + contributes: null, + capabilities: ["kv:own"], + ...overrides, + }; +} + +function entry( + overrides: Partial = {}, +): RegistryPluginEntry { + return { + id: "docker", + name: "Docker", + description: "Containers", + author: "Termix", + category: "Infrastructure", + versions: [ + { + version: "1.1.0", + compatible: true, + capabilities: ["kv:own", "ssh:connect"], + publishedAt: "2026-10-02", + size: 1, + }, + ], + latestVersion: "1.1.0", + installed: true, + installedVersion: "1.0.0", + updateAvailable: true, + addedCapabilities: ["ssh:connect"], + pinnedVersion: null, + autoUpdate: false, + bundled: true, + ...overrides, + }; +} + +describe("mergePlugins", () => { + it("joins an installed plugin with its registry entry", () => { + const [row] = mergePlugins([summary()], [entry()]); + expect(row).toMatchObject({ + installed: true, + version: "1.0.0", + latestVersion: "1.1.0", + updateAvailable: true, + addedCapabilities: ["ssh:connect"], + capabilities: ["kv:own"], + category: "Infrastructure", + inRegistry: true, + status: "running", + }); + }); + + it("lists registry plugins that are not installed with the latest capabilities", () => { + const rows = mergePlugins( + [], + [entry({ installed: false, installedVersion: null })], + ); + expect(rows[0]).toMatchObject({ + installed: false, + version: null, + status: null, + capabilities: ["kv:own", "ssh:connect"], + }); + }); + + it("keeps installed plugins when the registry is unreachable", () => { + const rows = mergePlugins([summary()], null); + expect(rows).toHaveLength(1); + expect(rows[0].inRegistry).toBe(false); + }); +}); + +describe("pluginStatus", () => { + it("maps loader and stored states", () => { + expect(pluginStatus(summary({ state: "failed" }))).toBe("failed"); + expect(pluginStatus(summary({ state: "blocked" }))).toBe("blocked"); + expect(pluginStatus(summary({ enabled: false, state: "disabled" }))).toBe( + "disabled", + ); + expect(pluginStatus(summary())).toBe("running"); + }); +}); + +describe("sorting and filtering", () => { + const rows = mergePlugins( + [], + [ + entry({ id: "b", name: "Beta", category: "Monitoring" }), + entry({ + id: "a", + name: "Alpha", + category: "Access", + description: "Sign in", + versions: [ + { + version: "1.0.0", + compatible: true, + capabilities: [], + publishedAt: "2026-10-05", + size: 1, + }, + ], + }), + ], + ); + + it("sorts by name, date and category", () => { + expect(sortPlugins(rows, "name").map((r) => r.id)).toEqual(["a", "b"]); + expect(sortPlugins(rows, "updated").map((r) => r.id)).toEqual(["a", "b"]); + expect(sortPlugins(rows, "category").map((r) => r.id)).toEqual(["a", "b"]); + }); + + it("searches name, description and id", () => { + expect( + rows.filter((r) => matchesQuery(r, "sign")).map((r) => r.id), + ).toEqual(["a"]); + expect(rows.filter((r) => matchesQuery(r, " ")).length).toBe(2); + }); + + it("collects categories", () => { + expect(categoriesOf(rows)).toEqual(["Access", "Monitoring"]); + }); +}); + +describe("helpers", () => { + it("orders capabilities worst first", () => { + expect(orderByRisk(["kv:own", "hosts:write", "credentials:read"])).toEqual([ + "credentials:read", + "hosts:write", + "kv:own", + ]); + }); + + it("counts contributions", () => { + expect( + describeContributions({ + tabs: [{}, {}], + settings: { admin: [] }, + auth: { loginMethods: [{}], secondFactors: [] }, + panels: [], + }), + ).toEqual([ + { kind: "tabs", count: 2 }, + { kind: "auth", count: 1 }, + { kind: "settings", count: 1 }, + ]); + }); + + it("formats sizes", () => { + expect(formatBytes(512)).toBe("512 B"); + expect(formatBytes(2048)).toBe("2.0 KB"); + expect(formatBytes(50 * 1024 * 1024)).toBe("50 MB"); + }); +}); diff --git a/src/ui/tests/sidebar/rail-items.test.ts b/src/ui/tests/sidebar/rail-items.test.ts index ae4f2f53c..2eea4d42d 100644 --- a/src/ui/tests/sidebar/rail-items.test.ts +++ b/src/ui/tests/sidebar/rail-items.test.ts @@ -63,9 +63,19 @@ describe("RAIL_ITEMS", () => { "connections", "quick-connect", "sync", + "plugins", ]); }); + it("opens the Plugins tab only for plugin managers", () => { + const plugins = RAIL_ITEMS.find((item) => item.id === "plugins"); + expect(plugins).toMatchObject({ + kind: "tab", + placement: "footer", + permission: "admin.plugins.manage", + }); + }); + it("exposes every visible rail item as hideable", () => { expect(hideableRailIds()).toEqual( visibleRailItems().map((item) => item.id), diff --git a/vitest.config.ts b/vitest.config.ts index e6a0b2315..161ba7df7 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -29,6 +29,9 @@ export default defineConfig({ test: { globals: true, setupFiles: ["./vitest.setup.ts"], + // Forks workers on Windows abort now and then (0xC0000409) in a random + // file and Vitest reports "Worker exited unexpectedly". Threads don't. + pool: "threads", coverage: { provider: "v8", reporter: ["text", "html"],