From 6b4d1432abe31a35a3f15dc9528201009a1e1e0b Mon Sep 17 00:00:00 2001 From: LukeGus Date: Thu, 24 Sep 2026 13:05:44 -0500 Subject: [PATCH] feat: finish the automations plugin (B17) --- docker/nginx-https.conf | 8 +- docker/nginx.conf | 8 +- drizzle/mysql/0046_rich_logan.sql | 4 + drizzle/mysql/meta/0046_snapshot.json | 6522 +++++++++++++++++ drizzle/mysql/meta/_journal.json | 7 + drizzle/postgres/0047_mighty_butterfly.sql | 4 + drizzle/postgres/meta/0047_snapshot.json | 6343 ++++++++++++++++ drizzle/postgres/meta/_journal.json | 7 + drizzle/sqlite/0043_curvy_red_shift.sql | 4 + drizzle/sqlite/meta/0043_snapshot.json | 6151 ++++++++++++++++ drizzle/sqlite/meta/_journal.json | 7 + packages/plugin-sdk/ARCHITECTURE.md | 79 +- packages/plugin-sdk/FINISH-LIST.md | 64 +- packages/plugin-sdk/src/backend.ts | 78 + packages/plugin-sdk/src/testing.ts | 67 +- plugins/ai/manifest.json | 11 +- plugins/ai/src/backend/services.ts | 81 + plugins/ai/src/backend/tools/executor.ts | 25 +- plugins/ai/src/backend/tools/read-tools.ts | 31 +- .../backend/tools/proposal-executor.test.ts | 42 +- plugins/automations/locales/en.json | 10 + plugins/automations/manifest.json | 32 +- .../mysql/0001_adopt_automations.sql | 86 + .../postgres/0001_adopt_automations.sql | 94 + plugins/automations/migrations/snapshot.json | 303 + .../sqlite/0001_adopt_automations.sql | 94 + .../src/backend/actions/host-targets.ts | 85 +- .../automations/src/backend/actions/index.ts | 249 +- .../automations/src/backend/actions/types.ts | 10 +- plugins/automations/src/backend/conditions.ts | 5 +- plugins/automations/src/backend/deps.ts | 121 + .../automations/src/backend/docker-watcher.ts | 159 +- plugins/automations/src/backend/docker.ts | 85 - plugins/automations/src/backend/engine.ts | 150 +- .../src/backend/headless-viewer.ts | 172 +- plugins/automations/src/backend/http.ts | 79 - plugins/automations/src/backend/index.ts | 98 +- plugins/automations/src/backend/notify.ts | 193 - plugins/automations/src/backend/repository.ts | 738 ++ .../automations/src/backend/requirements.ts | 93 + plugins/automations/src/backend/routes.ts | 1300 ++-- plugins/automations/src/backend/scheduler.ts | 329 +- plugins/automations/src/backend/service.ts | 139 + plugins/automations/src/backend/snippets.ts | 91 - plugins/automations/src/backend/ssh.ts | 36 - plugins/automations/src/backend/tables.ts | 197 + plugins/automations/src/backend/triggers.ts | 745 +- plugins/automations/src/backend/tunnels.ts | 63 - plugins/automations/src/backend/validate.ts | 153 + .../src/frontend/AutomationsPanel.tsx | 134 +- .../src/frontend/automations-api.ts | 144 +- .../frontend/automations/AutomationEditor.tsx | 21 +- .../automations/HostSelectorField.tsx | 14 +- .../src/frontend/automations/StepBlock.tsx | 40 +- .../src/frontend/automations/TriggerCard.tsx | 32 +- .../src/frontend/automations/editor-types.ts | 53 + plugins/automations/src/frontend/index.tsx | 5 +- .../automations/src/types.ts | 0 .../tests/backend/activate.test.ts | 82 + .../tests/backend/docker-watcher.test.ts | 131 - .../tests/backend/engine-runtime.test.ts | 383 + .../automations/tests/backend/engine.test.ts | 112 +- .../tests/backend/headless-viewer.test.ts | 136 - plugins/automations/tests/backend/helpers.ts | 205 + .../tests/backend/migrations.test.ts | 210 + .../automations/tests/backend/notify.test.ts | 44 - .../automations/tests/backend/routes.test.ts | 664 +- .../tests/backend/triggers.test.ts | 28 +- .../automations/tests/backend/tunnels.test.ts | 90 - .../tests/frontend/automations-editor.test.ts | 2 +- .../automations/tests/frontend/panel.test.tsx | 78 + scripts/plugin-boundary-allowlist.json | 20 +- src/backend/auth/session-issuer.ts | 6 +- src/backend/database/db/index.ts | 152 - .../database/db/performance-indexes.ts | 45 - src/backend/database/db/schema.mysql.ts | 159 - src/backend/database/db/schema.pg.ts | 159 - src/backend/database/db/schema.ts | 159 - .../repositories/automation-repository.ts | 784 -- src/backend/database/repositories/factory.ts | 8 - src/backend/database/routes/host.ts | 13 +- src/backend/hosts/automation-events.ts | 35 - src/backend/hosts/internal-events.ts | 30 + src/backend/plugins/ctx-notify.ts | 150 + src/backend/plugins/ctx.ts | 3 + src/backend/plugins/events.ts | 17 +- src/backend/plugins/index.ts | 2 +- src/backend/plugins/settings.ts | 5 +- src/backend/tests/auth/legacy-methods.test.ts | 4 +- src/backend/tests/auth/login-pipeline.test.ts | 4 +- .../routes/user-webauthn-routes.test.ts | 4 +- .../tests/plugins/bundled-plugin.test.ts | 10 +- src/backend/tests/plugins/ctx-notify.test.ts | 184 + src/backend/tests/plugins/events.test.ts | 16 +- src/backend/utils/notification-sender.ts | 144 + src/ui/plugin-host/sdk-ui.ts | 10 + 96 files changed, 25106 insertions(+), 5077 deletions(-) create mode 100644 drizzle/mysql/0046_rich_logan.sql create mode 100644 drizzle/mysql/meta/0046_snapshot.json create mode 100644 drizzle/postgres/0047_mighty_butterfly.sql create mode 100644 drizzle/postgres/meta/0047_snapshot.json create mode 100644 drizzle/sqlite/0043_curvy_red_shift.sql create mode 100644 drizzle/sqlite/meta/0043_snapshot.json create mode 100644 plugins/automations/migrations/mysql/0001_adopt_automations.sql create mode 100644 plugins/automations/migrations/postgres/0001_adopt_automations.sql create mode 100644 plugins/automations/migrations/snapshot.json create mode 100644 plugins/automations/migrations/sqlite/0001_adopt_automations.sql create mode 100644 plugins/automations/src/backend/deps.ts delete mode 100644 plugins/automations/src/backend/docker.ts delete mode 100644 plugins/automations/src/backend/http.ts delete mode 100644 plugins/automations/src/backend/notify.ts create mode 100644 plugins/automations/src/backend/repository.ts create mode 100644 plugins/automations/src/backend/requirements.ts create mode 100644 plugins/automations/src/backend/service.ts delete mode 100644 plugins/automations/src/backend/snippets.ts delete mode 100644 plugins/automations/src/backend/ssh.ts create mode 100644 plugins/automations/src/backend/tables.ts delete mode 100644 plugins/automations/src/backend/tunnels.ts create mode 100644 plugins/automations/src/backend/validate.ts rename src/types/automations.ts => plugins/automations/src/types.ts (100%) create mode 100644 plugins/automations/tests/backend/activate.test.ts delete mode 100644 plugins/automations/tests/backend/docker-watcher.test.ts create mode 100644 plugins/automations/tests/backend/engine-runtime.test.ts delete mode 100644 plugins/automations/tests/backend/headless-viewer.test.ts create mode 100644 plugins/automations/tests/backend/helpers.ts create mode 100644 plugins/automations/tests/backend/migrations.test.ts delete mode 100644 plugins/automations/tests/backend/notify.test.ts delete mode 100644 plugins/automations/tests/backend/tunnels.test.ts create mode 100644 plugins/automations/tests/frontend/panel.test.tsx delete mode 100644 src/backend/database/repositories/automation-repository.ts delete mode 100644 src/backend/hosts/automation-events.ts create mode 100644 src/backend/hosts/internal-events.ts create mode 100644 src/backend/plugins/ctx-notify.ts create mode 100644 src/backend/tests/plugins/ctx-notify.test.ts diff --git a/docker/nginx-https.conf b/docker/nginx-https.conf index 04c0a0ca3..37610e456 100644 --- a/docker/nginx-https.conf +++ b/docker/nginx-https.conf @@ -161,7 +161,7 @@ http { root /app/html; index index.html index.htm; expires off; - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-gjgrKvv0zkJ6QUgUJBcK4CPIIa2JbLMmHecileCoA9A='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; media-src 'self' data: blob: http: https:; worker-src 'self' blob:; frame-src http: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-u6nDWD7Mm1U/e9z2vyubUQB8Wk00VJsojS2q1tGdZwg='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; media-src 'self' data: blob: http: https:; worker-src 'self' blob:; frame-src http: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; @@ -323,6 +323,12 @@ http { proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; } + # Automation webhooks lived at /automations/webhook/ before + # 2.9.0 and external systems still call that URL. + location ^~ /automations/webhook/ { + rewrite ^/automations/webhook/(.*)$ /plugin-api/automations/webhook/$1 last; + } + # Every plugin's HTTP routes. One block, no per-plugin entries: a # plugin serves at /plugin-api// on the main backend and never # gets a port or a block of its own. Buffering is off so SSE streams diff --git a/docker/nginx.conf b/docker/nginx.conf index 56d395307..508455d5c 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -142,7 +142,7 @@ http { root /app/html; index index.html index.htm; expires off; - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-gjgrKvv0zkJ6QUgUJBcK4CPIIa2JbLMmHecileCoA9A='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; media-src 'self' data: blob: http: https:; worker-src 'self' blob:; frame-src http: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-u6nDWD7Mm1U/e9z2vyubUQB8Wk00VJsojS2q1tGdZwg='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; media-src 'self' data: blob: http: https:; worker-src 'self' blob:; frame-src http: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; @@ -303,6 +303,12 @@ http { proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; } + # Automation webhooks lived at /automations/webhook/ before + # 2.9.0 and external systems still call that URL. + location ^~ /automations/webhook/ { + rewrite ^/automations/webhook/(.*)$ /plugin-api/automations/webhook/$1 last; + } + # Every plugin's HTTP routes. One block, no per-plugin entries: a # plugin serves at /plugin-api// on the main backend and never # gets a port or a block of its own. Buffering is off so SSE streams diff --git a/drizzle/mysql/0046_rich_logan.sql b/drizzle/mysql/0046_rich_logan.sql new file mode 100644 index 000000000..b80928097 --- /dev/null +++ b/drizzle/mysql/0046_rich_logan.sql @@ -0,0 +1,4 @@ +-- The automation tables moved to the automations plugin, which renames them +-- into its own namespace at activation. That runs after this migration, so a +-- real DROP TABLE here would delete every automation first. +SELECT 1; diff --git a/drizzle/mysql/meta/0046_snapshot.json b/drizzle/mysql/meta/0046_snapshot.json new file mode 100644 index 000000000..c41126e77 --- /dev/null +++ b/drizzle/mysql/meta/0046_snapshot.json @@ -0,0 +1,6522 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "004a3020-a354-42b9-b7bf-ab314d4ceb73", + "prevId": "ca1e226b-6111-4345-9702-2f82da4c3090", + "tables": { + "ai_conversations": { + "name": "ai_conversations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_conversations_user": { + "name": "idx_ai_conversations_user", + "columns": [ + "user_id", + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_conversations_user_id_users_id_fk": { + "name": "ai_conversations_user_id_users_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_conversations_id": { + "name": "ai_conversations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ai_messages": { + "name": "ai_messages", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('')" + }, + "tool_calls": { + "name": "tool_calls", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_messages_conversation": { + "name": "idx_ai_messages_conversation", + "columns": [ + "conversation_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_messages_conversation_id_ai_conversations_id_fk": { + "name": "ai_messages_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_messages", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_messages_id": { + "name": "ai_messages_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ai_proposals": { + "name": "ai_proposals", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_proposals_user": { + "name": "idx_ai_proposals_user", + "columns": [ + "user_id", + "status" + ], + "isUnique": false + }, + "idx_ai_proposals_conversation": { + "name": "idx_ai_proposals_conversation", + "columns": [ + "conversation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_proposals_conversation_id_ai_conversations_id_fk": { + "name": "ai_proposals_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_proposals_user_id_users_id_fk": { + "name": "ai_proposals_user_id_users_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_proposals_id": { + "name": "ai_proposals_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ai_providers": { + "name": "ai_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key_prefix": { + "name": "api_key_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ai_providers_user_label": { + "name": "idx_ai_providers_user_label", + "columns": [ + "user_id", + "label" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ai_providers_user_id_users_id_fk": { + "name": "ai_providers_user_id_users_id_fk", + "tableFrom": "ai_providers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ai_providers_id": { + "name": "ai_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_keys_id": { + "name": "api_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_access": { + "name": "credential_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('use')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_access_id": { + "name": "credential_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_sidebar_preferences_user_id": { + "name": "credential_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dashboard_service_links_id": { + "name": "dashboard_service_links_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dismissed_alerts_id": { + "name": "dismissed_alerts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "folder_access": { + "name": "folder_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + "owner_user_id", + "folder" + ], + "isUnique": false + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "folder_access_id": { + "name": "folder_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "folder_id": { + "name": "folder_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_homepage_items_user_id": { + "name": "idx_homepage_items_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_items_id": { + "name": "homepage_items_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{}')" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "homepage_layouts_id": { + "name": "homepage_layouts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ] + } + }, + "checkConstraint": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_access_id": { + "name": "host_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_sidebar_preferences_user_id": { + "name": "host_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('ssh')" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_web_ui": { + "name": "enable_web_ui", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_ai_assistant": { + "name": "enable_ai_assistant", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "web_ui_config": { + "name": "web_ui_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('sha256')" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_data_id": { + "name": "ssh_data_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "notification_channels_id": { + "name": "notification_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "opkssh_tokens_id": { + "name": "opkssh_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_install_counts": { + "name": "plugin_install_counts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('aggregate-telemetry')" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + "plugin_id", + "registry_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_install_counts_id": { + "name": "plugin_install_counts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_migrations": { + "name": "plugin_migrations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "migration_id": { + "name": "migration_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + "plugin_id", + "migration_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_migrations_id": { + "name": "plugin_migrations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_permission_grants": { + "name": "plugin_permission_grants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capability": { + "name": "capability", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('admin')" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + "plugin_id", + "capability" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_permission_grants_id": { + "name": "plugin_permission_grants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_registries": { + "name": "plugin_registries", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('community')" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_registries_id": { + "name": "plugin_registries_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_settings": { + "name": "plugin_settings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + "plugin_id", + "scope", + "scope_id", + "key" + ], + "isUnique": true + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + "plugin_id", + "scope" + ], + "isUnique": false + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_settings_id": { + "name": "plugin_settings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_storage": { + "name": "plugin_storage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + "plugin_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_storage_id": { + "name": "plugin_storage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugins": { + "name": "plugins", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('available')" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('community')" + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('disabled')" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + "registry_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugins_id": { + "name": "plugins_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "role_name": { + "name": "role_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + "role_name", + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rbac_applied_defaults_id": { + "name": "rbac_applied_defaults_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rbac_known_permissions": { + "name": "rbac_known_permissions", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rbac_known_permissions_id": { + "name": "rbac_known_permissions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recent_activity_id": { + "name": "recent_activity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "secret_sources": { + "name": "secret_sources", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('onepassword-connect')" + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_secret_sources_user": { + "name": "idx_secret_sources_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "secret_sources_user_id_users_id_fk": { + "name": "secret_sources_user_id_users_id_fk", + "tableFrom": "secret_sources", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "secret_sources_id": { + "name": "secret_sources_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "settings_key": { + "name": "settings_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_credential_secrets": { + "name": "shared_credential_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('password')" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + "credential_access_id", + "target_user_id" + ], + "isUnique": true + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + "target_user_id", + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_credential_secrets_id": { + "name": "shared_credential_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_auth_overrides_id": { + "name": "shared_host_auth_overrides_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('credential')" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_secrets_id": { + "name": "shared_host_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credential_usage_id": { + "name": "ssh_credential_usage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credentials_id": { + "name": "ssh_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_folders_id": { + "name": "ssh_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sso_providers_id": { + "name": "sso_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_tombstones_id": { + "name": "sync_tombstones_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identities_id": { + "name": "termix_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_ca_id": { + "name": "termix_identity_ca_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_keys_id": { + "name": "termix_identity_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "trusted_devices_id": { + "name": "trusted_devices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ui_preferences_user_id": { + "name": "ui_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_external_identities": { + "name": "user_external_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + "provider_id", + "subject" + ], + "isUnique": true + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_external_identities_id": { + "name": "user_external_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_open_tabs_id": { + "name": "user_open_tabs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_assistant_enabled": { + "name": "ai_assistant_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_read_only_commands": { + "name": "ai_read_only_commands", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_defaults": { + "name": "terminal_defaults", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_macros": { + "name": "terminal_macros", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id": { + "name": "user_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_id": { + "name": "user_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_second_factors": { + "name": "user_second_factors", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "factor_id": { + "name": "factor_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + "user_id", + "plugin_id", + "factor_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_second_factors_id": { + "name": "user_second_factors_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('openid email profile')" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_profiles_id": { + "name": "vault_profiles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_tokens_id": { + "name": "vault_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('preferred')" + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "webauthn_credentials_id": { + "name": "webauthn_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/_journal.json b/drizzle/mysql/meta/_journal.json index 686dcce64..f6275149d 100644 --- a/drizzle/mysql/meta/_journal.json +++ b/drizzle/mysql/meta/_journal.json @@ -323,6 +323,13 @@ "when": 1790268466430, "tag": "0045_flowery_the_hood", "breakpoints": true + }, + { + "idx": 46, + "version": "5", + "when": 1790272734438, + "tag": "0046_rich_logan", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/postgres/0047_mighty_butterfly.sql b/drizzle/postgres/0047_mighty_butterfly.sql new file mode 100644 index 000000000..b80928097 --- /dev/null +++ b/drizzle/postgres/0047_mighty_butterfly.sql @@ -0,0 +1,4 @@ +-- The automation tables moved to the automations plugin, which renames them +-- into its own namespace at activation. That runs after this migration, so a +-- real DROP TABLE here would delete every automation first. +SELECT 1; diff --git a/drizzle/postgres/meta/0047_snapshot.json b/drizzle/postgres/meta/0047_snapshot.json new file mode 100644 index 000000000..330066883 --- /dev/null +++ b/drizzle/postgres/meta/0047_snapshot.json @@ -0,0 +1,6343 @@ +{ + "id": "2409d374-fdcc-4143-8d51-6a55e514b1ef", + "prevId": "6d15efa0-bf3b-4ada-9af2-56eb1a359b72", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ai_conversations": { + "name": "ai_conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_conversations_user": { + "name": "idx_ai_conversations_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_conversations_user_id_users_id_fk": { + "name": "ai_conversations_user_id_users_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_messages": { + "name": "ai_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "tool_calls": { + "name": "tool_calls", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_messages_conversation": { + "name": "idx_ai_messages_conversation", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_messages_conversation_id_ai_conversations_id_fk": { + "name": "ai_messages_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_messages", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_proposals": { + "name": "ai_proposals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_proposals_user": { + "name": "idx_ai_proposals_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_proposals_conversation": { + "name": "idx_ai_proposals_conversation", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_proposals_conversation_id_ai_conversations_id_fk": { + "name": "ai_proposals_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_proposals_user_id_users_id_fk": { + "name": "ai_proposals_user_id_users_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_providers": { + "name": "ai_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_prefix": { + "name": "api_key_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_providers_user_label": { + "name": "idx_ai_providers_user_label", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "label", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_providers_user_id_users_id_fk": { + "name": "ai_providers_user_id_users_id_fk", + "tableFrom": "ai_providers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_access": { + "name": "credential_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'use'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_service_links": { + "name": "dashboard_service_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dismissed_alerts": { + "name": "dismissed_alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.folder_access": { + "name": "folder_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "folder", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_items": { + "name": "homepage_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_homepage_items_user_id": { + "name": "idx_homepage_items_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.homepage_layouts": { + "name": "homepage_layouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_access": { + "name": "host_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_data": { + "name": "ssh_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_web_ui": { + "name": "enable_web_ui", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_ai_assistant": { + "name": "enable_ai_assistant", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "web_ui_config": { + "name": "web_ui_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + { + "expression": "parent_host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channels": { + "name": "notification_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.opkssh_tokens": { + "name": "opkssh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_install_counts": { + "name": "plugin_install_counts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'aggregate-telemetry'" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_migrations": { + "name": "plugin_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "migration_id": { + "name": "migration_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "migration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_permission_grants": { + "name": "plugin_permission_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "capability": { + "name": "capability", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "capability", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_registries": { + "name": "plugin_registries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'community'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_settings": { + "name": "plugin_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_storage": { + "name": "plugin_storage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugins": { + "name": "plugins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'community'" + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'disabled'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + { + "expression": "registry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "role_name": { + "name": "role_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + { + "expression": "role_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rbac_known_permissions": { + "name": "rbac_known_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recent_activity": { + "name": "recent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.secret_sources": { + "name": "secret_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'onepassword-connect'" + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_secret_sources_user": { + "name": "idx_secret_sources_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "secret_sources_user_id_users_id_fk": { + "name": "secret_sources_user_id_users_id_fk", + "tableFrom": "secret_sources", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_credential_secrets": { + "name": "shared_credential_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'password'" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + { + "expression": "credential_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_secrets": { + "name": "shared_host_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + { + "expression": "host_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credential_usage": { + "name": "ssh_credential_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credentials": { + "name": "ssh_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_folders": { + "name": "ssh_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_providers": { + "name": "sso_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_tombstones": { + "name": "sync_tombstones", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identities": { + "name": "termix_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_ca": { + "name": "termix_identity_ca", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "nullsNotDistinct": false, + "columns": [ + "identity_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_keys": { + "name": "termix_identity_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_devices": { + "name": "trusted_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ui_preferences": { + "name": "ui_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_external_identities": { + "name": "user_external_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_open_tabs": { + "name": "user_open_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_assistant_enabled": { + "name": "ai_assistant_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ai_read_only_commands": { + "name": "ai_read_only_commands", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_defaults": { + "name": "terminal_defaults", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_macros": { + "name": "terminal_macros", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_second_factors": { + "name": "user_second_factors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "factor_id": { + "name": "factor_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "factor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_profiles": { + "name": "vault_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_tokens": { + "name": "vault_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webauthn_credentials": { + "name": "webauthn_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/postgres/meta/_journal.json b/drizzle/postgres/meta/_journal.json index 21cb3d70b..47a3dc8d9 100644 --- a/drizzle/postgres/meta/_journal.json +++ b/drizzle/postgres/meta/_journal.json @@ -330,6 +330,13 @@ "when": 1790268465907, "tag": "0046_small_white_tiger", "breakpoints": true + }, + { + "idx": 47, + "version": "7", + "when": 1790272733784, + "tag": "0047_mighty_butterfly", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/sqlite/0043_curvy_red_shift.sql b/drizzle/sqlite/0043_curvy_red_shift.sql new file mode 100644 index 000000000..b80928097 --- /dev/null +++ b/drizzle/sqlite/0043_curvy_red_shift.sql @@ -0,0 +1,4 @@ +-- The automation tables moved to the automations plugin, which renames them +-- into its own namespace at activation. That runs after this migration, so a +-- real DROP TABLE here would delete every automation first. +SELECT 1; diff --git a/drizzle/sqlite/meta/0043_snapshot.json b/drizzle/sqlite/meta/0043_snapshot.json new file mode 100644 index 000000000..7da1c2c44 --- /dev/null +++ b/drizzle/sqlite/meta/0043_snapshot.json @@ -0,0 +1,6151 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "f43cf928-7535-4aa8-a406-f030212b01e5", + "prevId": "32849df3-69f5-42b0-88cd-e55aff3a00d9", + "tables": { + "ai_conversations": { + "name": "ai_conversations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_conversations_user": { + "name": "idx_ai_conversations_user", + "columns": [ + "user_id", + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_conversations_user_id_users_id_fk": { + "name": "ai_conversations_user_id_users_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_messages": { + "name": "ai_messages", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "tool_calls": { + "name": "tool_calls", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_messages_conversation": { + "name": "idx_ai_messages_conversation", + "columns": [ + "conversation_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_messages_conversation_id_ai_conversations_id_fk": { + "name": "ai_messages_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_messages", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_proposals": { + "name": "ai_proposals", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_proposals_user": { + "name": "idx_ai_proposals_user", + "columns": [ + "user_id", + "status" + ], + "isUnique": false + }, + "idx_ai_proposals_conversation": { + "name": "idx_ai_proposals_conversation", + "columns": [ + "conversation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ai_proposals_conversation_id_ai_conversations_id_fk": { + "name": "ai_proposals_conversation_id_ai_conversations_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_proposals_user_id_users_id_fk": { + "name": "ai_proposals_user_id_users_id_fk", + "tableFrom": "ai_proposals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_providers": { + "name": "ai_providers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key_prefix": { + "name": "api_key_prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ai_providers_user_label": { + "name": "idx_ai_providers_user_label", + "columns": [ + "user_id", + "label" + ], + "isUnique": true + } + }, + "foreignKeys": { + "ai_providers_user_id_users_id_fk": { + "name": "ai_providers_user_id_users_id_fk", + "tableFrom": "ai_providers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_access": { + "name": "credential_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'use'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dashboard_service_links": { + "name": "dashboard_service_links", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "dashboard_service_links_sync_id_unique": { + "name": "dashboard_service_links_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "dashboard_service_links_user_id_users_id_fk": { + "name": "dashboard_service_links_user_id_users_id_fk", + "tableFrom": "dashboard_service_links", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "folder_access": { + "name": "folder_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + "owner_user_id", + "folder" + ], + "isUnique": false + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_items": { + "name": "homepage_items", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "folder_id": { + "name": "folder_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_items_sync_id_unique": { + "name": "homepage_items_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_homepage_items_user_id": { + "name": "idx_homepage_items_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "homepage_items_user_id_users_id_fk": { + "name": "homepage_items_user_id_users_id_fk", + "tableFrom": "homepage_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "homepage_layouts": { + "name": "homepage_layouts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "homepage_layouts_user_id_unique": { + "name": "homepage_layouts_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "homepage_layouts_user_id_users_id_fk": { + "name": "homepage_layouts_user_id_users_id_fk", + "tableFrom": "homepage_layouts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_web_ui": { + "name": "enable_web_ui", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_ai_assistant": { + "name": "enable_ai_assistant", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "web_ui_config": { + "name": "web_ui_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wol_broadcast_address": { + "name": "wol_broadcast_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_install_counts": { + "name": "plugin_install_counts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registry_id": { + "name": "registry_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'aggregate-telemetry'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + "plugin_id", + "registry_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_migrations": { + "name": "plugin_migrations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "migration_id": { + "name": "migration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + "plugin_id", + "migration_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_permission_grants": { + "name": "plugin_permission_grants", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capability": { + "name": "capability", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'admin'" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + "plugin_id", + "capability" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_registries": { + "name": "plugin_registries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'community'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_settings": { + "name": "plugin_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted": { + "name": "encrypted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + "plugin_id", + "scope", + "scope_id", + "key" + ], + "isUnique": true + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + "plugin_id", + "scope" + ], + "isUnique": false + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_storage": { + "name": "plugin_storage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + "plugin_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugins": { + "name": "plugins", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'available'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'community'" + }, + "registry_id": { + "name": "registry_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'disabled'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + "registry_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "role_name": { + "name": "role_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + "role_name", + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rbac_known_permissions": { + "name": "rbac_known_permissions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "secret_sources": { + "name": "secret_sources", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'onepassword-connect'" + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_secret_sources_user": { + "name": "idx_secret_sources_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "secret_sources_user_id_users_id_fk": { + "name": "secret_sources_user_id_users_id_fk", + "tableFrom": "secret_sources", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_credential_secrets": { + "name": "shared_credential_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'password'" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + "credential_access_id", + "target_user_id" + ], + "isUnique": true + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + "target_user_id", + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_external_identities": { + "name": "user_external_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + "provider_id", + "subject" + ], + "isUnique": true + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_assistant_enabled": { + "name": "ai_assistant_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ai_read_only_commands": { + "name": "ai_read_only_commands", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_defaults": { + "name": "terminal_defaults", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_macros": { + "name": "terminal_macros", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_second_factors": { + "name": "user_second_factors", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "factor_id": { + "name": "factor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + "user_id", + "plugin_id", + "factor_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'openid email profile'" + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totp_enabled": { + "name": "totp_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "totp_backup_codes": { + "name": "totp_backup_codes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webauthn_credentials": { + "name": "webauthn_credentials", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_verification": { + "name": "user_verification", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'preferred'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "webauthn_credentials_user_id_users_id_fk": { + "name": "webauthn_credentials_user_id_users_id_fk", + "tableFrom": "webauthn_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/_journal.json b/drizzle/sqlite/meta/_journal.json index 292ba1482..a0091e43f 100644 --- a/drizzle/sqlite/meta/_journal.json +++ b/drizzle/sqlite/meta/_journal.json @@ -302,6 +302,13 @@ "when": 1790268465325, "tag": "0042_many_old_lace", "breakpoints": true + }, + { + "idx": 43, + "version": "6", + "when": 1790272733108, + "tag": "0043_curvy_red_shift", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/plugin-sdk/ARCHITECTURE.md b/packages/plugin-sdk/ARCHITECTURE.md index b2ef44539..0d8dcd185 100644 --- a/packages/plugin-sdk/ARCHITECTURE.md +++ b/packages/plugin-sdk/ARCHITECTURE.md @@ -598,7 +598,10 @@ The `docker` and `hostMetrics` areas are still declared in core's status for a host, from the shell's own polling, or null outside the shell. **B14** added `buildOriginWsUrl`, `getBasePath`, `resolveRemoteHostId` and the `ConnectionStage` type, for remote desktop's display socket and its remote -host lookup. `useConnectionDefaults` now carries terminal defaults only; the +host lookup. **B17** added `NotificationChannelDialog` and the channel API +(`getNotificationChannels`, `deleteNotificationChannel`, +`testNotificationChannel`, the `NotificationChannel` type): channels are core, +so a plugin that manages them shows core's dialog. `useConnectionDefaults` now carries terminal defaults only; the RDP half became remote desktop's user settings. Publishing its `.d.ts` for plugins outside this repo is a follow-up for the repo split. @@ -732,6 +735,38 @@ the `shareable`, `canShare`, `openShareModal` and `getShareTarget` members of `TabOptions`/`TabHandle` are gone. Core's active connections list gets the sessions shared with a user from the `sessions.sharedWithMe` action. +**Automations (B17)** imports nothing from core. It reaches snippets +(`snippets.access`, a hard dependency), fleets (`fleets.access`), tunnels +(`tunnels.access`), Docker (`docker.containers`, `docker.events`), host metrics +(`host-metrics.viewers`) and wake-on-lan (`wake-on-lan.send` v1, +`wake(hostId)`, which B20 provides) as services, notification channels through +`ctx.notify`, the HTTP step through `ctx.fetch`, hosts through `ctx.hosts` and +`ctx.ssh.resolveHost`, and commands through `ctx.ssh.withConnection`. Every run +executes inside `ctx.asUser()`, so each of those calls is the owner's. +The scheduler tick is `ctx.schedule.every`, and triggers listen through +`ctx.events` to `host.status`, `host.login`, `internal.event`, +`plugin.host-metrics.snapshot`, `plugin.host-metrics.health-check`, +`plugin.tunnels.tunnel_disconnected` and its own +`plugin.automations.automation_failed`, which replaced the core +`automation_failed` internal event. + +A definition's trigger and steps say which of those optional plugins it needs +(`requirements.ts`). An automation whose provider is not running is skipped: +the run is recorded as `skipped` with "Needs the plugin", the reason +is logged, and the engine carries on with the next one. The editor asks +`GET /editor-options` for the running providers and only offers the triggers +and steps they allow, and the list marks such an automation "Needs ". +It provides `automations.access` v1 (`list`, `get`, `create`, `run`, gated +on `automations.view`, with `create` and `run` also checking their own +permissions) for the AI assistant, which lists automations as an optional +dependency. It adopts `automations` and the five `automation_*` tables; the +migrations keep the legacy links between them and to `notification_channels` +by hand. Core's `hosts/automation-events.ts` became `hosts/internal-events.ts` +(`emitInternalEvent`), since it never was specific to automations. Webhooks +live at `/plugin-api/automations/webhook/`; the 2.8 path +`/automations/webhook/` is rewritten to it in both nginx configs, +because external systems hold that URL. + #### Host status is core The status dot in the host list, the dashboard and the homepage widget is @@ -1358,14 +1393,14 @@ Built per plugin in `src/backend/plugins/ctx.ts` and passed to `activate`. | `ctx.hosts.status.*` | `hosts:read` | **B16** | | `ctx.ssh.*` | `ssh:connect`, `credentials:use` | **A8** | | `ctx.settings.*` | `settings:read-core` (readCore only) | **A6** | -| `ctx.notify.*` | `notify:send` | A6 | +| `ctx.notify.channels` / `.send` | `notify:send` | **B17** | | `ctx.auth.*` | `auth:provide` | **A8** | | `ctx.desktop.openIsolatedWindow` | `desktop:window` | **B8** | | `ctx.desktop.launchNativeRdp` / `.available` | `desktop:window` (launch only) | **B14** | | `ctx.credentials.resolveHostProtocol` | `credentials:read` | **B14** | | `ctx.audit.record` | none, the actor is the runtime's | **B9** | | `ctx.schedule.every` / `.after` | none | **B16** | -| `ctx.fetch` | `network:outbound` | B | +| `ctx.fetch` | `network:outbound` | **B17** | **B11** added `ctx.capabilities.has(capability)` / `.require(capability)`, a generic check for a capability no other ctx member wraps. Unlike every other @@ -1555,6 +1590,25 @@ per-host timers spreads out. Needs no capability. the debounced save write it, for frequent low-value writes such as metrics samples; the default still saves before returning. +**B17** added `ctx.notify` and `ctx.fetch`: + +- `ctx.notify.channels()` lists the acting user's notification channels + (id, name, type, enabled, never the config), and `send(channelIds, +notification)` delivers to the ones among them that are theirs and + enabled, returning `{ delivered, failures }`. Both need `notify:send` and + an actor, and are audited (`plugin_notify_channels`, `plugin_notify_send`). + Channels stay core (`notification_channels`, its routes and + `NotificationChannelDialog`); delivery is `deliverNotification` in + `utils/notification-sender.ts`, which keeps the channel's private-network + opt-in plus the admin allowlist. +- `ctx.fetch(url, { method, headers, body, timeoutMs, allowPrivateHosts })` + goes through `safeOutboundFetch`: http and https only, no embedded + credentials, DNS pinned, no redirects, and private or loopback addresses + refused unless the exact host is in `allowPrivateHosts`. Needs + `network:outbound`, audited as `plugin_fetch`. Automations' HTTP step + passes the admin allowlist it reads through `ctx.settings.readCore` + (`notification_private_endpoint_allowlist` joined `CORE_SETTINGS_ALLOWLIST`). + `ctx.hosts.create` (**B14**) hands the created row to every plugin's `hostImportNormalizer`, the same as a bulk import row, so a plugin creating a host can set another plugin's host settings (proxmox marks an imported @@ -2085,7 +2139,8 @@ Then, with the app running: ## Legacy core imports: the debt D1 removes The bundled plugins predate the SDK, apart from workspaces (A9), snippets -(B2), remote-desktop (B14), docker (B15) and host-metrics (B16), which import +(B2), remote-desktop (B14), docker (B15), host-metrics (B16) and automations +(B17), which import nothing from core. The others still reach core by relative path (`../../../../src/backend/...`), which an esbuild plugin, `packages/plugin-sdk/cli/lib/legacy-core-imports.mjs`, keeps out of the bundle @@ -2113,9 +2168,9 @@ What the lint fence enforces today, in `eslint.config.mjs`: | Core importing a plugin backend | **Error** | 0 | - | | A plugin backend importing frontend code or `@/` | **Error** | 0 | - | | The shell importing plugin code | **Error** | 0 | - | -| A plugin frontend importing core through `@/` | Warning | 70 files | D1 | -| A plugin importing core by relative path | Warning | 24 files | D1 | -| A plugin importing another plugin's source | Warning | 1 file | B18 | +| A plugin frontend importing core through `@/` | Warning | 64 files | D1 | +| A plugin importing core by relative path | Warning | 12 files | D1 | +| A plugin importing another plugin's source | Warning | 0 files | - | A warning does not fail a build, so the counts are held by `scripts/check-plugin-boundaries.cjs`, run by `npm run lint`. Every offender is @@ -2131,12 +2186,10 @@ rule cannot carry both severities on the same files. Known specifics: -- **TODO(B18):** `plugins/ai/src/backend/tools/executor.ts` imports - `automations`' `routes.js` directly. It is declared as a hard `dependencies` - entry so the loader starts automations first and marks `ai` blocked if - automations is disabled. In B18 `ai` calls automations through an - `automations` service as an `optionalDependency` and the direct import goes - away. +- **B17** removed the last plugin-to-plugin import: ai's proposal executor + and automation read tools call automations through the `automations.access` + service, and automations is an optional dependency of ai rather than a hard + one. - **B16** moved host-metrics onto the SDK: it imports nothing from core or automations. Automations reaches it through the `host-metrics.viewers` service and its `plugin.host-metrics.*` events, and core reaches it through diff --git a/packages/plugin-sdk/FINISH-LIST.md b/packages/plugin-sdk/FINISH-LIST.md index e6c422921..8826a80bd 100644 --- a/packages/plugin-sdk/FINISH-LIST.md +++ b/packages/plugin-sdk/FINISH-LIST.md @@ -334,16 +334,6 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite `@termix/plugin-sdk/ui`), workspaces included. `npm run type-check` covers plugins through `tsconfig.plugins*.json` and passes. Owner: D0. -- **B16 (host-metrics):** automations still sets up its headless viewers - through `setViewerRegistry` in `plugins/automations/src/backend/headless-viewer.ts`, - which nothing calls any more, so hosts an automation watches are only - sampled while somebody has them open. Switch it to the - `host-metrics.viewers` service (`register(hostId)` as the automation's - owner, `heartbeat`, `unregister`) and make host-metrics an optional - dependency again. Its triggers already listen to the final topics - (`plugin.host-metrics.snapshot`, `plugin.host-metrics.health-check`, core - `host.status`, `host.login`), but through core's bus by relative import - instead of `ctx.events`. Owner: B17. - **B16 (host-metrics):** Termix-Mobile read `statsConfig` on each host (`statusCheckEnabled`, `enabledWidgets`) and the host status from the metrics API. The host payload now carries `statusCheckEnabled` and @@ -367,12 +357,6 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite `widgets/network-collector.ts`) to compute rates. The poller clears them on deactivate and per host, so nothing leaks, but they are still module state rather than per-activation. Owner: D1. -- **Pre-existing, found in B16:** automations - `tests/backend/tunnels.test.ts` fails "fails the step cleanly while the - tunnels plugin is off": it expects "tunnels plugin is not available" and - gets "tunnels.start is not a function". Same cause as the tunnels line - above: since B12 `ctx.services.get` hands back an empty handle instead of - throwing, so the step has to check `typeof start`. Owner: B17 or D0. - **B15 (docker), for D2:** the console socket `/plugin-ws/docker/console` is a public route with `optionalAuth`. The handler refuses a socket with no signed-in user or without `docker.use` and checks the data key per connect @@ -394,10 +378,39 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite tools still report `enableDocker` from the host row, which no longer has the column, so it always reads null. Read `pluginSettings.docker` or ask the `docker.containers` service instead. Owner: B18. -- **B15 (docker), for B17:** automations' `docker-watcher.ts` still reads its - own rules through core's automation repository and types by relative - import; only the Docker side is on the SDK (`docker.containers` for the - step, `docker.events` for the trigger). Owner: B17. + +- **B17 (automations), for B20:** the wake-on-lan step calls the optional + `wake-on-lan.send` v1 service, `wake(hostId): Promise`, as the + automation's owner; the provider resolves the host's MAC and checks access + itself. Until B20 provides it, the step is offered nowhere and an existing + one is skipped with "Needs the wake-on-lan plugin". Owner: B20. +- **B17 (automations), for D2:** the ctx table says `ctx.events.on` needs + `events:core` for core topics, but the runtime only checks `emit`. + docker, file-manager, host-metrics, session-recording and snippets + subscribe to core topics (`host.*`, `user.*`) without declaring it. + Either gate `on` and add `events:core` to those five, or change the doc to + say subscribing is ungated. Owner: D2. +- **B17 (automations):** the 2.8 webhook URL `/automations/webhook/` + is kept only by a rewrite in `docker/nginx.conf` and + `docker/nginx-https.conf`. An install that serves the backend without + nginx (the desktop app's embedded backend, a hand-rolled proxy) answers + 404 there. Decide whether that matters enough for a generic legacy-path + alias in the plugin HTTP layer. Owner: D0. +- **B17 (automations):** there are two notification senders now: + `deliverNotification` (ctx.notify, per-channel private opt-in, throws) and + the older `sendWebhook`/`sendNtfy`/`sendDiscord` that the channel "test" + route in `notification-channels-routes.ts` still uses (always applies the + allowlist, retries once). Point the test route at `deliverNotification` + and delete the old ones. Owner: D1. +- **B17 (automations):** the scheduler no longer skips a user whose data key + cannot be resolved (`DataCrypto.canUserAccessData` has no SDK equivalent). + With system-wrapped DEKs that check was effectively always true; if it + ever is not, the run now fails on its first SSH step instead of being + skipped silently. Owner: none unless a background "is this user unlocked" + check is added to the SDK. +- **B17 (automations):** AI's `list_notification_channels` read tool still reads + notification channels through core's repository by relative import; it + can use `ctx.notify.channels()` now. Owner: B18. ## Manual checks after 2.9.0 @@ -444,3 +457,14 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite with no stored password asks for credentials; the homepage Docker widget; an automation Docker step and a `docker_event` trigger; disable Docker with a console open, then enable it and open another. +- Automations: on a 2.8 database the automations, their run history and + their channel links are there; a schedule fires; a webhook fires on both + `/plugin-api/automations/webhook/` and the old + `/automations/webhook/`; a metric threshold on a host nobody has + open still fires (headless viewers); a `docker_event` trigger and a Docker + step; a notify step to webhook, ntfy and Discord channels, and to a LAN + ntfy with the private opt-in plus the admin allowlist; disable Docker and + the automation shows "Needs docker", its scheduled runs are recorded as + skipped and the others keep running, then enable it again; the AI + assistant lists and creates automations, and says they are unavailable + with automations off. diff --git a/packages/plugin-sdk/src/backend.ts b/packages/plugin-sdk/src/backend.ts index 87c539a78..db7c06c34 100644 --- a/packages/plugin-sdk/src/backend.ts +++ b/packages/plugin-sdk/src/backend.ts @@ -1255,6 +1255,80 @@ export interface PluginAudit { }) => Promise; } +/** A notification channel the acting user set up. Its config never leaves core. */ +export interface PluginNotificationChannel { + id: number; + name: string; + /** "webhook", "ntfy" or "discord". */ + type: string; + enabled: boolean; +} + +export interface PluginNotification { + title: string; + body: string; + severity?: "info" | "warning" | "critical"; + /** + * Extra fields a webhook receiver gets alongside the message. `sourceId` + * and `sourceName` name what sent it (an automation, a rule). + */ + context?: { + hostId?: number; + hostName?: string; + sourceId?: number | string; + sourceName?: string; + triggerType?: string; + value?: unknown; + threshold?: unknown; + }; +} + +export interface PluginNotifyResult { + delivered: number; + failures: Array<{ channelId: number; name: string; error: string }>; +} + +/** + * Core's notification channels, as the acting user. Needs notify:send, and + * refuses a call with no actor: channels belong to a user. + */ +export interface PluginNotify { + /** The acting user's channels, without their config. */ + channels: () => Promise; + /** + * Sends to the listed channels the acting user owns. Disabled channels and + * ids the user does not own are skipped. One channel failing does not stop + * the others. + */ + send: ( + channelIds: number[], + notification: PluginNotification, + ) => Promise; +} + +export interface PluginFetchInit { + method?: string; + headers?: Record; + body?: string; + /** Aborts the request after this long. Defaults to 30 seconds. */ + timeoutMs?: number; + /** + * Exact private or loopback hosts this request may reach. Everything else + * private is refused, and redirects are never followed. + */ + allowPrivateHosts?: readonly string[]; +} + +/** + * Outbound HTTP through core's SSRF guard: http and https only, no embedded + * credentials, DNS pinned, private addresses refused unless listed. Needs + * network:outbound. + */ +export type PluginFetch = ( + url: string, + init?: PluginFetchInit, +) => Promise; + /** * A generic capability check, for a privileged action that has no dedicated * ctx method to wrap it: a plugin bundling its own native dependency to touch @@ -1325,6 +1399,10 @@ export interface PluginContext { readonly audit: PluginAudit; /** Timers that stop on deactivate. */ readonly schedule: PluginSchedule; + /** Core's notification channels. Needs notify:send. */ + readonly notify: PluginNotify; + /** Outbound HTTP through core's SSRF guard. Needs network:outbound. */ + readonly fetch: PluginFetch; /** * Runs `fn` with `userId` as the acting user, for background work that has diff --git a/packages/plugin-sdk/src/testing.ts b/packages/plugin-sdk/src/testing.ts index 130485af0..f6f7be0bc 100644 --- a/packages/plugin-sdk/src/testing.ts +++ b/packages/plugin-sdk/src/testing.ts @@ -56,6 +56,9 @@ import type { PluginNativeRdpRequest, PluginProtocolTarget, PluginHostStatusEntry, + PluginNotification, + PluginNotificationChannel, + PluginFetchInit, } from "./backend.js"; /** A timer registered through ctx.schedule, run by hand with runScheduled. */ @@ -108,6 +111,13 @@ export interface FakeContextOptions { desktopAvailable?: boolean; /** What ctx.hosts.status.get and check answer, by host id. */ hostStatuses?: Record; + /** The acting user's channels, what ctx.notify.channels answers. */ + notificationChannels?: PluginNotificationChannel[]; + /** + * Answers ctx.fetch. Without it every fetch rejects, so a test never + * reaches the network by accident. + */ + fetch?: (url: string, init?: PluginFetchInit) => Promise; } export interface FakeAuthRegistrations { @@ -169,6 +179,14 @@ export interface FakePluginContext { nativeRdpLaunches: PluginNativeRdpRequest[]; /** Every ctx.credentials.resolveHostProtocol call, in order. */ credentialReads: Array<{ hostId: number; protocol: string }>; + /** Every ctx.notify.send call, with the actor it ran as. */ + notifications: Array<{ + actor: string | undefined; + channelIds: number[]; + notification: PluginNotification; + }>; + /** Every ctx.fetch call, in order. */ + fetches: Array<{ url: string; init?: PluginFetchInit }>; /** Every ctx.audit.record entry, in order. */ audits: Array<{ action: string; success: boolean; [key: string]: unknown }>; /** Every ctx.hosts.recordActivity call, in order. */ @@ -270,6 +288,8 @@ export function createFakeContext( const desktopWindows: FakePluginContext["desktopWindows"] = []; const nativeRdpLaunches: FakePluginContext["nativeRdpLaunches"] = []; const credentialReads: FakePluginContext["credentialReads"] = []; + const notifications: FakePluginContext["notifications"] = []; + const fetches: FakePluginContext["fetches"] = []; const audits: FakePluginContext["audits"] = []; const activities: FakePluginContext["activities"] = []; const trackedSessions: number[] = []; @@ -766,6 +786,23 @@ export function createFakeContext( }, }, + notify: { + channels: async () => options.notificationChannels ?? [], + send: async (channelIds, notification) => { + notifications.push({ actor, channelIds, notification }); + const selected = (options.notificationChannels ?? []).filter( + (channel) => channelIds.includes(channel.id) && channel.enabled, + ); + return { delivered: selected.length, failures: [] }; + }, + }, + + fetch: async (url, init) => { + fetches.push({ url, init }); + if (!options.fetch) throw new Error("ctx.fetch is not stubbed"); + return options.fetch(url, init); + }, + asUser: async (userId, fn) => { const previous = actor; actor = userId; @@ -797,6 +834,8 @@ export function createFakeContext( desktopWindows, nativeRdpLaunches, credentialReads, + notifications, + fetches, audits, activities, trackedSessions, @@ -852,6 +891,13 @@ export interface MockContextOptions { desktopAvailable?: boolean; /** What ctx.hosts.status answers. See FakeContextOptions. */ hostStatuses?: Record; + /** The acting user's channels, what ctx.notify.channels answers. */ + notificationChannels?: PluginNotificationChannel[]; + /** + * Answers ctx.fetch. Without it every fetch rejects, so a test never + * reaches the network by accident. + */ + fetch?: (url: string, init?: PluginFetchInit) => Promise; } export interface MockPluginContext extends FakePluginContext { @@ -866,7 +912,8 @@ export interface MockPluginContext extends FakePluginContext { * The gates on today's ctx surface: kv:own on every ctx.kv call, db:own on * ctx.db, network:serve on ctx.http and ctx.ws, events:core on emitting a * topic outside the plugin's own namespace, settings:read-core on - * ctx.settings.readCore, ssh:connect plus credentials:use on ctx.ssh, and + * ctx.settings.readCore, ssh:connect plus credentials:use on ctx.ssh, + * notify:send on ctx.notify, network:outbound on ctx.fetch, and * auth:provide on ctx.auth. Reading a plugin's own settings is deliberately * ungated. As the SDK grows a member, add its gate here in the same shape. */ @@ -891,6 +938,8 @@ export function createMockCtx( protocolTargets: options.protocolTargets, desktopAvailable: options.desktopAvailable, hostStatuses: options.hostStatuses, + notificationChannels: options.notificationChannels, + fetch: options.fetch, manifest: { capabilities: options.capabilities ?? [], ...options.manifest, @@ -1155,6 +1204,22 @@ export function createMockCtx( }, }, + notify: { + channels: async () => { + require("notify:send"); + return ctx.notify.channels(); + }, + send: async (channelIds, notification) => { + require("notify:send"); + return ctx.notify.send(channelIds, notification); + }, + }, + + fetch: async (url, init) => { + require("network:outbound"); + return ctx.fetch(url, init); + }, + capabilities: { has: async (capability) => granted.has(capability), require: async (capability) => { diff --git a/plugins/ai/manifest.json b/plugins/ai/manifest.json index b7c3fd5a6..1150bdaec 100644 --- a/plugins/ai/manifest.json +++ b/plugins/ai/manifest.json @@ -26,15 +26,13 @@ "network:serve", "ui:surface" ], - "dependencies": { - "automations": "^1.0.0" - }, "optionalDependencies": { "workspaces": "^1.0.0", "network-topology": "^1.0.0", "snippets": "^1.0.0", "fleets": "^1.0.0", - "ssh-terminal": "^1.0.0" + "ssh-terminal": "^1.0.0", + "automations": "^1.0.0" }, "requires": [ { @@ -61,6 +59,11 @@ "service": "terminal.history", "versionRange": "^1.0.0", "optional": true + }, + { + "service": "automations.access", + "versionRange": "^1.0.0", + "optional": true } ], "providesSecret": [ diff --git a/plugins/ai/src/backend/services.ts b/plugins/ai/src/backend/services.ts index 3a8619e09..1b1d5495c 100644 --- a/plugins/ai/src/backend/services.ts +++ b/plugins/ai/src/backend/services.ts @@ -248,3 +248,84 @@ export async function listCommandHistory( return null; } } + +/** The automations plugin, as ctx.services.get("automations.access") returns it. */ +interface AutomationsAccess { + list: () => Promise< + Array<{ + id: number; + name: string; + description: string | null; + enabled: boolean; + triggerKind: string | null; + lastRunAt: string | null; + lastRunStatus: string | null; + missingPlugins: string[]; + }> + >; + get: (id: number) => Promise<{ + id: number; + name: string; + enabled: boolean; + definition: unknown; + } | null>; + create: (input: { + name: string; + description?: string | null; + definition: unknown; + enabled?: boolean; + }) => Promise<{ id: number; name: string }>; +} + +function automations(userId: string): Partial { + if (!current) return {}; + try { + return current.get("automations.access", { userId }); + } catch { + return {}; + } +} + +/** + * The user's automations, or null when the automations plugin is off or the + * user may not use it. + */ +export async function listAutomations( + userId: string, +): Promise> | null> { + const list = automations(userId).list; + if (typeof list !== "function") return null; + try { + return await list(); + } catch { + return null; + } +} + +export async function getAutomation( + userId: string, + id: number, +): Promise> | undefined> { + const get = automations(userId).get; + if (typeof get !== "function") return undefined; + try { + return await get(id); + } catch { + return undefined; + } +} + +/** + * Creates an automation through the plugin, which validates it exactly as + * its own route does. Throws when the plugin is off. + */ +export async function createAutomation( + userId: string, + input: Parameters[0], +): Promise<{ id: number; name: string }> { + const create = automations(userId).create; + if (typeof create !== "function") { + throw new Error("The automations plugin is not available"); + } + return create(input); +} diff --git a/plugins/ai/src/backend/tools/executor.ts b/plugins/ai/src/backend/tools/executor.ts index 7da2324ea..3c34b2c9f 100644 --- a/plugins/ai/src/backend/tools/executor.ts +++ b/plugins/ai/src/backend/tools/executor.ts @@ -1,8 +1,4 @@ -import { - createCurrentAutomationRepository, - createCurrentHostRepository, -} from "../../../../../src/backend/database/repositories/factory.js"; -import { validateDefinition } from "../../../../automations/src/backend/routes.js"; +import { createCurrentHostRepository } from "../../../../../src/backend/database/repositories/factory.js"; import { resolveHostById } from "../../../../../src/backend/hosts/host-resolver.js"; import { execCommand } from "@termix/plugin-sdk/host-commands"; import { withSshConnection } from "../ssh.js"; @@ -14,6 +10,7 @@ import { getSnippet, createFleet, addFleetMember, + createAutomation, } from "../services.js"; /** Approved commands get a bounded window rather than hanging the request. */ @@ -204,20 +201,12 @@ export async function applyProposal( } case "propose_create_automation": { - // Reuses the same validator the automations route runs, so an - // LLM-authored definition is held to exactly the human standard. - const validation = validateDefinition(payload.definition); - if (!validation.ok || !validation.definition) { - throw new Error( - validation.error ?? "The automation definition is invalid", - ); - } - - const created = await createCurrentAutomationRepository().create({ - userId, + // The automations plugin runs the same validator its own route does, + // so an LLM-authored definition is held to exactly the human standard. + const created = await createAutomation(userId, { name: requireString(payload.name, "name"), description: optionalString(payload.description), - definition: JSON.stringify(validation.definition), + definition: payload.definition, // Starts disabled: an automation the user has not watched run once // should not begin firing against their servers on approval. enabled: false, @@ -225,7 +214,7 @@ export async function applyProposal( return { ok: true, - summary: `Created automation ${(created as any).name} (disabled until you enable it)`, + summary: `Created automation ${created.name} (disabled until you enable it)`, }; } diff --git a/plugins/ai/src/backend/tools/read-tools.ts b/plugins/ai/src/backend/tools/read-tools.ts index 5b1c3c285..db3cc2cb9 100644 --- a/plugins/ai/src/backend/tools/read-tools.ts +++ b/plugins/ai/src/backend/tools/read-tools.ts @@ -1,6 +1,5 @@ import { createCurrentNotificationChannelRepository, - createCurrentAutomationRepository, createCurrentHomepageItemRepository, createCurrentHostRepository, } from "../../../../../src/backend/database/repositories/factory.js"; @@ -11,6 +10,8 @@ import { listSnippets, listFleets, listCommandHistory, + listAutomations, + getAutomation, } from "../services.js"; /** @@ -128,17 +129,18 @@ export const readTools: AiTool[] = [ category: "read", parameters: objectSchema({}), handler: async (_args, context) => { - const automations = await createCurrentAutomationRepository().list( - context.userId, - ); + const automations = await listAutomations(context.userId); + if (!automations) return { automations: [], unavailable: true }; return { - automations: automations.map((automation: any) => ({ + automations: automations.map((automation) => ({ id: automation.id, name: automation.name, - description: automation.description ?? null, + description: automation.description, enabled: automation.enabled, - lastRunAt: automation.lastRunAt ?? null, - lastRunStatus: automation.lastRunStatus ?? null, + triggerKind: automation.triggerKind, + lastRunAt: automation.lastRunAt, + lastRunStatus: automation.lastRunStatus, + needsPlugins: automation.missingPlugins, })), }; }, @@ -155,17 +157,18 @@ export const readTools: AiTool[] = [ ["automationId"], ), handler: async (args, context) => { - const automation = await createCurrentAutomationRepository().findForUser( - Number(args.automationId), + const automation = await getAutomation( context.userId, + Number(args.automationId), ); + if (automation === undefined) return { unavailable: true }; if (!automation) return { error: "Automation not found" }; return { automation: { - id: (automation as any).id, - name: (automation as any).name, - enabled: (automation as any).enabled, - definition: (automation as any).definition, + id: automation.id, + name: automation.name, + enabled: automation.enabled, + definition: automation.definition, }, }; }, diff --git a/plugins/ai/tests/backend/tools/proposal-executor.test.ts b/plugins/ai/tests/backend/tools/proposal-executor.test.ts index 1958fe9a1..fdb8e9883 100644 --- a/plugins/ai/tests/backend/tools/proposal-executor.test.ts +++ b/plugins/ai/tests/backend/tools/proposal-executor.test.ts @@ -12,13 +12,12 @@ const snippetRepository = { updateSnippet: vi.fn(), deleteSnippet: vi.fn(), }; -const automationRepository = { create: vi.fn() }; +const automationsAccess = { create: vi.fn() }; const fleetsAccess = { create: vi.fn(), addMember: vi.fn() }; vi.mock("../../../../../src/backend/database/repositories/factory.js", () => ({ createCurrentHostRepository: () => hostRepository, createCurrentSnippetRepository: () => snippetRepository, - createCurrentAutomationRepository: () => automationRepository, })); const resolveHostById = vi.fn(); @@ -57,9 +56,24 @@ describe("applyProposal", () => { ).rejects.toThrow("Unknown proposal kind"); }); - it("validates an automation definition before creating it", async () => { - // Reuses the automations route's own validator, so a definition the model + function withAutomations() { + setPluginServices({ + get: (service: string) => { + if (service !== "automations.access") + throw new Error(`unexpected ${service}`); + return automationsAccess as never; + }, + provide: vi.fn(), + } as never); + } + + it("hands an automation to the automations plugin, which validates it", async () => { + // The plugin runs its own route's validator, so a definition the model // invented is held to the same standard as a hand-written one. + withAutomations(); + automationsAccess.create.mockRejectedValue( + new Error("Unknown or missing trigger kind"), + ); await expect( applyProposal( "propose_create_automation", @@ -69,12 +83,22 @@ describe("applyProposal", () => { }, "user-1", ), - ).rejects.toThrow(); - expect(automationRepository.create).not.toHaveBeenCalled(); + ).rejects.toThrow("Unknown or missing trigger kind"); + }); + + it("refuses an automation while the automations plugin is off", async () => { + await expect( + applyProposal( + "propose_create_automation", + { name: "x", definition: {} }, + "user-1", + ), + ).rejects.toThrow("The automations plugin is not available"); }); it("creates a valid automation disabled so it cannot fire unwatched", async () => { - automationRepository.create.mockResolvedValue({ id: 5, name: "nightly" }); + withAutomations(); + automationsAccess.create.mockResolvedValue({ id: 5, name: "nightly" }); const result = await applyProposal( "propose_create_automation", @@ -89,8 +113,8 @@ describe("applyProposal", () => { ); expect(result.ok).toBe(true); - expect(automationRepository.create).toHaveBeenCalledWith( - expect.objectContaining({ userId: "user-1", enabled: false }), + expect(automationsAccess.create).toHaveBeenCalledWith( + expect.objectContaining({ name: "nightly", enabled: false }), ); }); diff --git a/plugins/automations/locales/en.json b/plugins/automations/locales/en.json index 732c00570..0c3bf4b79 100644 --- a/plugins/automations/locales/en.json +++ b/plugins/automations/locales/en.json @@ -157,6 +157,16 @@ "onError": { "stop": "Stop the run", "continue": "Carry on" + }, + "needsPlugin": "Needs {{plugin}}", + "needsPluginHint": "This automation is skipped until the plugin it needs is turned on again.", + "offBadge": "Off", + "docsLink": "View docs", + "ago": { + "seconds": "{{count}}s ago", + "minutes": "{{count}}m ago", + "hours": "{{count}}h ago", + "days": "{{count}}d ago" } } } diff --git a/plugins/automations/manifest.json b/plugins/automations/manifest.json index c891e4767..c4761e6b4 100644 --- a/plugins/automations/manifest.json +++ b/plugins/automations/manifest.json @@ -16,7 +16,6 @@ }, "capabilities": [ "hosts:read", - "hosts:write", "credentials:use", "ssh:connect", "db:own", @@ -24,19 +23,20 @@ "notify:send", "network:outbound", "network:serve", + "settings:read-core", "ui:surface" ], "optionalDependencies": { - "snippets": "^1.0.0", "fleets": "^1.0.0", "tunnels": "^1.0.0", - "docker": "^1.0.0" + "docker": "^1.0.0", + "host-metrics": "^1.0.0", + "wake-on-lan": "^1.0.0" }, "requires": [ { "service": "snippets.access", - "versionRange": "^1.0.0", - "optional": true + "versionRange": "^1.0.0" }, { "service": "fleets.access", @@ -57,6 +57,23 @@ "service": "docker.events", "versionRange": "^1.0.0", "optional": true + }, + { + "service": "host-metrics.viewers", + "versionRange": "^1.0.0", + "optional": true + }, + { + "service": "wake-on-lan.send", + "versionRange": "^1.0.0", + "optional": true + } + ], + "provides": [ + { + "service": "automations.access", + "version": "1.0.0", + "permission": "automations.view" } ], "contributes": { @@ -102,5 +119,8 @@ ] }, "locales": "locales", - "platforms": ["linux", "win32", "darwin"] + "platforms": ["linux", "win32", "darwin"], + "dependencies": { + "snippets": "^1.0.0" + } } diff --git a/plugins/automations/migrations/mysql/0001_adopt_automations.sql b/plugins/automations/migrations/mysql/0001_adopt_automations.sql new file mode 100644 index 000000000..714c5f203 --- /dev/null +++ b/plugins/automations/migrations/mysql/0001_adopt_automations.sql @@ -0,0 +1,86 @@ +-- automations 0001: adopt_automations +-- Generated by termix-plugin migrations. Review before committing. +-- Hand-edited: keeps the legacy foreign keys the SDK cannot declare +-- (links between these tables, and a channel link to notification_channels). + +CREATE TABLE IF NOT EXISTS `automations` ( + `id` int AUTO_INCREMENT PRIMARY KEY, + `user_id` varchar(255) NOT NULL, + `name` text NOT NULL, + `description` text, + `enabled` boolean NOT NULL DEFAULT true, + `definition` text NOT NULL, + `definition_version` int NOT NULL DEFAULT 1, + `concurrency_policy` text NOT NULL DEFAULT ('skip'), + `max_run_seconds` int NOT NULL DEFAULT 300, + `dry_run` boolean NOT NULL DEFAULT false, + `last_run_at` text, + `last_run_status` text, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +); +RENAME TABLE `automations` TO `p_automations_automations`; +CREATE TABLE IF NOT EXISTS `automation_trigger_state` ( + `id` int AUTO_INCREMENT PRIMARY KEY, + `automation_id` int NOT NULL, + `state_key` varchar(255) NOT NULL, + `breach_started_at` text, + `last_fired_at` text, + `last_value` double, + `last_observed_state` text, + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + FOREIGN KEY (`automation_id`) REFERENCES `p_automations_automations` (`id`) ON DELETE CASCADE +); +RENAME TABLE `automation_trigger_state` TO `p_automations_trigger_state`; +CREATE TABLE IF NOT EXISTS `automation_schedules` ( + `id` int AUTO_INCREMENT PRIMARY KEY, + `automation_id` int NOT NULL, + `cron` text, + `interval_seconds` int, + `timezone` text, + `next_due_at` varchar(255), + `last_tick_at` text, + FOREIGN KEY (`automation_id`) REFERENCES `p_automations_automations` (`id`) ON DELETE CASCADE +); +RENAME TABLE `automation_schedules` TO `p_automations_schedules`; +CREATE TABLE IF NOT EXISTS `automation_runs` ( + `id` int AUTO_INCREMENT PRIMARY KEY, + `automation_id` int NOT NULL, + `user_id` varchar(255) NOT NULL, + `trigger_type` text NOT NULL, + `trigger_context` text, + `status` text NOT NULL, + `started_at` varchar(255) NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `finished_at` text, + `duration_ms` int, + `error` text, + `dry_run` boolean NOT NULL DEFAULT false, + `parent_run_id` int, + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`automation_id`) REFERENCES `p_automations_automations` (`id`) ON DELETE CASCADE +); +RENAME TABLE `automation_runs` TO `p_automations_runs`; +CREATE TABLE IF NOT EXISTS `automation_run_steps` ( + `id` int AUTO_INCREMENT PRIMARY KEY, + `run_id` int NOT NULL, + `step_index` int NOT NULL, + `step_id` text NOT NULL, + `step_type` text NOT NULL, + `status` text NOT NULL, + `started_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `finished_at` text, + `output` text, + `error` text, + `truncated` boolean NOT NULL DEFAULT false, + FOREIGN KEY (`run_id`) REFERENCES `p_automations_runs` (`id`) ON DELETE CASCADE +); +RENAME TABLE `automation_run_steps` TO `p_automations_run_steps`; +CREATE TABLE IF NOT EXISTS `automation_channels` ( + `id` int AUTO_INCREMENT PRIMARY KEY, + `automation_id` int NOT NULL, + `channel_id` int NOT NULL, + FOREIGN KEY (`automation_id`) REFERENCES `p_automations_automations` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`channel_id`) REFERENCES `notification_channels` (`id`) ON DELETE CASCADE +); +RENAME TABLE `automation_channels` TO `p_automations_channels`; diff --git a/plugins/automations/migrations/postgres/0001_adopt_automations.sql b/plugins/automations/migrations/postgres/0001_adopt_automations.sql new file mode 100644 index 000000000..e47ad7701 --- /dev/null +++ b/plugins/automations/migrations/postgres/0001_adopt_automations.sql @@ -0,0 +1,94 @@ +-- automations 0001: adopt_automations +-- Generated by termix-plugin migrations. Review before committing. +-- Hand-edited: keeps the legacy foreign keys the SDK cannot declare +-- (links between these tables, and a channel link to notification_channels). + +CREATE TABLE IF NOT EXISTS "automations" ( + "id" serial PRIMARY KEY, + "user_id" varchar(255) NOT NULL, + "name" text NOT NULL, + "description" text, + "enabled" boolean NOT NULL DEFAULT true, + "definition" text NOT NULL, + "definition_version" integer NOT NULL DEFAULT 1, + "concurrency_policy" text NOT NULL DEFAULT 'skip', + "max_run_seconds" integer NOT NULL DEFAULT 300, + "dry_run" boolean NOT NULL DEFAULT false, + "last_run_at" text, + "last_run_status" text, + "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS "idx_automations_user" ON "automations" ("user_id", "enabled"); +ALTER TABLE "automations" RENAME TO "p_automations_automations"; +CREATE TABLE IF NOT EXISTS "automation_trigger_state" ( + "id" serial PRIMARY KEY, + "automation_id" integer NOT NULL, + "state_key" varchar(255) NOT NULL, + "breach_started_at" text, + "last_fired_at" text, + "last_value" double precision, + "last_observed_state" text, + "updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("automation_id") REFERENCES "p_automations_automations" ("id") ON DELETE CASCADE +); +CREATE UNIQUE INDEX IF NOT EXISTS "idx_automation_trigger_state_key" ON "automation_trigger_state" ("automation_id", "state_key"); +ALTER TABLE "automation_trigger_state" RENAME TO "p_automations_trigger_state"; +CREATE TABLE IF NOT EXISTS "automation_schedules" ( + "id" serial PRIMARY KEY, + "automation_id" integer NOT NULL, + "cron" text, + "interval_seconds" integer, + "timezone" text, + "next_due_at" varchar(255), + "last_tick_at" text, + FOREIGN KEY ("automation_id") REFERENCES "p_automations_automations" ("id") ON DELETE CASCADE +); +CREATE UNIQUE INDEX IF NOT EXISTS "idx_automation_schedules_automation" ON "automation_schedules" ("automation_id"); +CREATE INDEX IF NOT EXISTS "idx_automation_schedules_due" ON "automation_schedules" ("next_due_at"); +ALTER TABLE "automation_schedules" RENAME TO "p_automations_schedules"; +CREATE TABLE IF NOT EXISTS "automation_runs" ( + "id" serial PRIMARY KEY, + "automation_id" integer NOT NULL, + "user_id" varchar(255) NOT NULL, + "trigger_type" text NOT NULL, + "trigger_context" text, + "status" text NOT NULL, + "started_at" varchar(255) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "finished_at" text, + "duration_ms" integer, + "error" text, + "dry_run" boolean NOT NULL DEFAULT false, + "parent_run_id" integer, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE, + FOREIGN KEY ("automation_id") REFERENCES "p_automations_automations" ("id") ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS "idx_automation_runs_automation" ON "automation_runs" ("automation_id", "started_at"); +CREATE INDEX IF NOT EXISTS "idx_automation_runs_user" ON "automation_runs" ("user_id", "started_at"); +ALTER TABLE "automation_runs" RENAME TO "p_automations_runs"; +CREATE TABLE IF NOT EXISTS "automation_run_steps" ( + "id" serial PRIMARY KEY, + "run_id" integer NOT NULL, + "step_index" integer NOT NULL, + "step_id" text NOT NULL, + "step_type" text NOT NULL, + "status" text NOT NULL, + "started_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "finished_at" text, + "output" text, + "error" text, + "truncated" boolean NOT NULL DEFAULT false, + FOREIGN KEY ("run_id") REFERENCES "p_automations_runs" ("id") ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS "idx_automation_run_steps_run" ON "automation_run_steps" ("run_id", "step_index"); +ALTER TABLE "automation_run_steps" RENAME TO "p_automations_run_steps"; +CREATE TABLE IF NOT EXISTS "automation_channels" ( + "id" serial PRIMARY KEY, + "automation_id" integer NOT NULL, + "channel_id" integer NOT NULL, + FOREIGN KEY ("automation_id") REFERENCES "p_automations_automations" ("id") ON DELETE CASCADE, + FOREIGN KEY ("channel_id") REFERENCES "notification_channels" ("id") ON DELETE CASCADE +); +CREATE UNIQUE INDEX IF NOT EXISTS "idx_automation_channels_pair" ON "automation_channels" ("automation_id", "channel_id"); +ALTER TABLE "automation_channels" RENAME TO "p_automations_channels"; diff --git a/plugins/automations/migrations/snapshot.json b/plugins/automations/migrations/snapshot.json new file mode 100644 index 000000000..b08fde26e --- /dev/null +++ b/plugins/automations/migrations/snapshot.json @@ -0,0 +1,303 @@ +{ + "version": 1, + "tables": { + "automations": { + "columns": { + "id": { + "type": "id", + "primaryKey": true, + "notNull": true + }, + "userId": { + "type": "refUser", + "length": 255, + "notNull": true + }, + "name": { + "type": "text", + "notNull": true + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean", + "notNull": true, + "defaultValue": true + }, + "definition": { + "type": "text", + "notNull": true + }, + "definitionVersion": { + "type": "integer", + "notNull": true, + "defaultValue": 1 + }, + "concurrencyPolicy": { + "type": "text", + "notNull": true, + "defaultValue": "skip" + }, + "maxRunSeconds": { + "type": "integer", + "notNull": true, + "defaultValue": 300 + }, + "dryRun": { + "type": "boolean", + "notNull": true, + "defaultValue": false + }, + "lastRunAt": { + "type": "text" + }, + "lastRunStatus": { + "type": "text" + }, + "createdAt": { + "type": "timestamp", + "notNull": true, + "defaultNow": true + }, + "updatedAt": { + "type": "timestamp", + "notNull": true, + "defaultNow": true + } + }, + "indexes": [ + { + "name": "idx_automations_user", + "columns": ["userId", "enabled"] + } + ], + "adopts": "automations" + }, + "trigger_state": { + "columns": { + "id": { + "type": "id", + "primaryKey": true, + "notNull": true + }, + "automationId": { + "type": "integer", + "notNull": true + }, + "stateKey": { + "type": "varchar", + "length": 255, + "notNull": true + }, + "breachStartedAt": { + "type": "text" + }, + "lastFiredAt": { + "type": "text" + }, + "lastValue": { + "type": "real" + }, + "lastObservedState": { + "type": "text" + }, + "updatedAt": { + "type": "timestamp", + "notNull": true, + "defaultNow": true + } + }, + "indexes": [ + { + "name": "idx_automation_trigger_state_key", + "columns": ["automationId", "stateKey"], + "unique": true + } + ], + "adopts": "automation_trigger_state" + }, + "schedules": { + "columns": { + "id": { + "type": "id", + "primaryKey": true, + "notNull": true + }, + "automationId": { + "type": "integer", + "notNull": true + }, + "cron": { + "type": "text" + }, + "intervalSeconds": { + "type": "integer" + }, + "timezone": { + "type": "text" + }, + "nextDueAt": { + "type": "timestamp" + }, + "lastTickAt": { + "type": "text" + } + }, + "indexes": [ + { + "name": "idx_automation_schedules_automation", + "columns": ["automationId"], + "unique": true + }, + { + "name": "idx_automation_schedules_due", + "columns": ["nextDueAt"] + } + ], + "adopts": "automation_schedules" + }, + "runs": { + "columns": { + "id": { + "type": "id", + "primaryKey": true, + "notNull": true + }, + "automationId": { + "type": "integer", + "notNull": true + }, + "userId": { + "type": "refUser", + "length": 255, + "notNull": true + }, + "triggerType": { + "type": "text", + "notNull": true + }, + "triggerContext": { + "type": "text" + }, + "status": { + "type": "text", + "notNull": true + }, + "startedAt": { + "type": "timestamp", + "notNull": true, + "defaultNow": true + }, + "finishedAt": { + "type": "text" + }, + "durationMs": { + "type": "integer" + }, + "error": { + "type": "text" + }, + "dryRun": { + "type": "boolean", + "notNull": true, + "defaultValue": false + }, + "parentRunId": { + "type": "integer" + } + }, + "indexes": [ + { + "name": "idx_automation_runs_automation", + "columns": ["automationId", "startedAt"] + }, + { + "name": "idx_automation_runs_user", + "columns": ["userId", "startedAt"] + } + ], + "adopts": "automation_runs" + }, + "run_steps": { + "columns": { + "id": { + "type": "id", + "primaryKey": true, + "notNull": true + }, + "runId": { + "type": "integer", + "notNull": true + }, + "stepIndex": { + "type": "integer", + "notNull": true + }, + "stepId": { + "type": "text", + "notNull": true + }, + "stepType": { + "type": "text", + "notNull": true + }, + "status": { + "type": "text", + "notNull": true + }, + "startedAt": { + "type": "timestamp", + "notNull": true, + "defaultNow": true + }, + "finishedAt": { + "type": "text" + }, + "output": { + "type": "text" + }, + "error": { + "type": "text" + }, + "truncated": { + "type": "boolean", + "notNull": true, + "defaultValue": false + } + }, + "indexes": [ + { + "name": "idx_automation_run_steps_run", + "columns": ["runId", "stepIndex"] + } + ], + "adopts": "automation_run_steps" + }, + "channels": { + "columns": { + "id": { + "type": "id", + "primaryKey": true, + "notNull": true + }, + "automationId": { + "type": "integer", + "notNull": true + }, + "channelId": { + "type": "integer", + "notNull": true + } + }, + "indexes": [ + { + "name": "idx_automation_channels_pair", + "columns": ["automationId", "channelId"], + "unique": true + } + ], + "adopts": "automation_channels" + } + } +} diff --git a/plugins/automations/migrations/sqlite/0001_adopt_automations.sql b/plugins/automations/migrations/sqlite/0001_adopt_automations.sql new file mode 100644 index 000000000..d355f85f3 --- /dev/null +++ b/plugins/automations/migrations/sqlite/0001_adopt_automations.sql @@ -0,0 +1,94 @@ +-- automations 0001: adopt_automations +-- Generated by termix-plugin migrations. Review before committing. +-- Hand-edited: keeps the legacy foreign keys the SDK cannot declare +-- (links between these tables, and a channel link to notification_channels). + +CREATE TABLE IF NOT EXISTS "automations" ( + "id" integer PRIMARY KEY AUTOINCREMENT, + "user_id" text NOT NULL, + "name" text NOT NULL, + "description" text, + "enabled" integer NOT NULL DEFAULT 1, + "definition" text NOT NULL, + "definition_version" integer NOT NULL DEFAULT 1, + "concurrency_policy" text NOT NULL DEFAULT 'skip', + "max_run_seconds" integer NOT NULL DEFAULT 300, + "dry_run" integer NOT NULL DEFAULT 0, + "last_run_at" text, + "last_run_status" text, + "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS "idx_automations_user" ON "automations" ("user_id", "enabled"); +ALTER TABLE "automations" RENAME TO "p_automations_automations"; +CREATE TABLE IF NOT EXISTS "automation_trigger_state" ( + "id" integer PRIMARY KEY AUTOINCREMENT, + "automation_id" integer NOT NULL, + "state_key" text NOT NULL, + "breach_started_at" text, + "last_fired_at" text, + "last_value" real, + "last_observed_state" text, + "updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("automation_id") REFERENCES "p_automations_automations" ("id") ON DELETE CASCADE +); +CREATE UNIQUE INDEX IF NOT EXISTS "idx_automation_trigger_state_key" ON "automation_trigger_state" ("automation_id", "state_key"); +ALTER TABLE "automation_trigger_state" RENAME TO "p_automations_trigger_state"; +CREATE TABLE IF NOT EXISTS "automation_schedules" ( + "id" integer PRIMARY KEY AUTOINCREMENT, + "automation_id" integer NOT NULL, + "cron" text, + "interval_seconds" integer, + "timezone" text, + "next_due_at" text, + "last_tick_at" text, + FOREIGN KEY ("automation_id") REFERENCES "p_automations_automations" ("id") ON DELETE CASCADE +); +CREATE UNIQUE INDEX IF NOT EXISTS "idx_automation_schedules_automation" ON "automation_schedules" ("automation_id"); +CREATE INDEX IF NOT EXISTS "idx_automation_schedules_due" ON "automation_schedules" ("next_due_at"); +ALTER TABLE "automation_schedules" RENAME TO "p_automations_schedules"; +CREATE TABLE IF NOT EXISTS "automation_runs" ( + "id" integer PRIMARY KEY AUTOINCREMENT, + "automation_id" integer NOT NULL, + "user_id" text NOT NULL, + "trigger_type" text NOT NULL, + "trigger_context" text, + "status" text NOT NULL, + "started_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "finished_at" text, + "duration_ms" integer, + "error" text, + "dry_run" integer NOT NULL DEFAULT 0, + "parent_run_id" integer, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE, + FOREIGN KEY ("automation_id") REFERENCES "p_automations_automations" ("id") ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS "idx_automation_runs_automation" ON "automation_runs" ("automation_id", "started_at"); +CREATE INDEX IF NOT EXISTS "idx_automation_runs_user" ON "automation_runs" ("user_id", "started_at"); +ALTER TABLE "automation_runs" RENAME TO "p_automations_runs"; +CREATE TABLE IF NOT EXISTS "automation_run_steps" ( + "id" integer PRIMARY KEY AUTOINCREMENT, + "run_id" integer NOT NULL, + "step_index" integer NOT NULL, + "step_id" text NOT NULL, + "step_type" text NOT NULL, + "status" text NOT NULL, + "started_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "finished_at" text, + "output" text, + "error" text, + "truncated" integer NOT NULL DEFAULT 0, + FOREIGN KEY ("run_id") REFERENCES "p_automations_runs" ("id") ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS "idx_automation_run_steps_run" ON "automation_run_steps" ("run_id", "step_index"); +ALTER TABLE "automation_run_steps" RENAME TO "p_automations_run_steps"; +CREATE TABLE IF NOT EXISTS "automation_channels" ( + "id" integer PRIMARY KEY AUTOINCREMENT, + "automation_id" integer NOT NULL, + "channel_id" integer NOT NULL, + FOREIGN KEY ("automation_id") REFERENCES "p_automations_automations" ("id") ON DELETE CASCADE, + FOREIGN KEY ("channel_id") REFERENCES "notification_channels" ("id") ON DELETE CASCADE +); +CREATE UNIQUE INDEX IF NOT EXISTS "idx_automation_channels_pair" ON "automation_channels" ("automation_id", "channel_id"); +ALTER TABLE "automation_channels" RENAME TO "p_automations_channels"; diff --git a/plugins/automations/src/backend/actions/host-targets.ts b/plugins/automations/src/backend/actions/host-targets.ts index 62a62f29b..431f193a6 100644 --- a/plugins/automations/src/backend/actions/host-targets.ts +++ b/plugins/automations/src/backend/actions/host-targets.ts @@ -1,39 +1,45 @@ -import type { HostSelector } from "../../../../../src/types/automations.js"; -import { resolveHostById } from "../../../../../src/backend/hosts/host-resolver.js"; -import { PermissionManager } from "../../../../../src/backend/utils/permission-manager.js"; -import { fleetMemberIds } from "../snippets.js"; -import type { StepExecutionContext } from "./types.js"; +import type { PluginSshHost } from "@termix/plugin-sdk/backend"; +import type { HostSelector } from "../../types.js"; +import type { StepExecutionContext, StepRuntime } from "./types.js"; -/** A host the caller is allowed to act on. `host` is always resolved. */ +/** A host the owner may act on, resolved through core's connect pipeline. */ export interface ResolvedTarget { id: number; name: string; - host: NonNullable>>; + host: PluginSshHost; } /** * Turns a selector into the hosts a step may actually act on. * - * Access is checked here, at execution time rather than when the automation - * was saved, so a permission revoked after the fact takes effect on the next - * run. resolveHostById performs its own connect-level check and returns null - * when the owner can no longer reach the host. + * Access is checked at execution time rather than when the automation was + * saved, so a permission revoked after the fact takes effect on the next run. + * resolveHost runs as the automation's owner and answers null when they can + * no longer reach the host. */ export async function resolveTargets( selector: HostSelector, context: StepExecutionContext, + runtime: StepRuntime, ): Promise<{ targets: ResolvedTarget[]; skipped: number[] }> { - const ids = await selectorHostIds(selector, context); + const ids = await selectorHostIds(selector, context, runtime); const targets: ResolvedTarget[] = []; const skipped: number[] = []; for (const id of ids) { - const host = await resolveHostById(id, context.userId); + let host: PluginSshHost | null = null; + try { + host = await runtime.ctx.ssh.resolveHost(id); + } catch { + host = null; + } if (!host) { skipped.push(id); continue; } - targets.push({ id, name: host.name || host.ip, host }); + const name = + typeof host.name === "string" && host.name ? host.name : host.ip; + targets.push({ id, name, host }); } return { targets, skipped }; @@ -42,6 +48,7 @@ export async function resolveTargets( async function selectorHostIds( selector: HostSelector, context: StepExecutionContext, + runtime: StepRuntime, ): Promise { switch (selector.kind) { case "host": @@ -50,35 +57,29 @@ async function selectorHostIds( return selector.hostIds; case "trigger": return context.triggerHostId ? [context.triggerHostId] : []; - case "fleet": - return fleetHostIds(selector.fleetId, context.userId); - case "all": - return allAccessibleHostIds(context.userId); + case "fleet": { + const fleets = runtime.deps.fleets(); + if (typeof fleets.members !== "function") return []; + try { + const members = await fleets.members(selector.fleetId); + return members.map((member) => member.id); + } catch { + return []; + } + } + case "all": { + try { + const hosts = await runtime.ctx.hosts.list(); + // The owner's own hosts, as before: "all" never fans out onto hosts + // someone else shared with them. + return hosts + .filter((host) => host.userId === context.userId) + .map((host) => host.id); + } catch { + return []; + } + } default: return []; } } - -async function fleetHostIds( - fleetId: number, - userId: string, -): Promise { - return fleetMemberIds(userId, fleetId); -} - -async function allAccessibleHostIds(userId: string): Promise { - try { - const { createCurrentHostRepository } = - await import("../../../../../src/backend/database/repositories/factory.js"); - const hosts = await createCurrentHostRepository().listByUserId(userId); - const ids = hosts.map((host) => host.id); - const allowed = - await PermissionManager.getInstance().filterAccessibleHostIds( - userId, - ids, - ); - return ids.filter((id) => allowed.has(id)); - } catch { - return []; - } -} diff --git a/plugins/automations/src/backend/actions/index.ts b/plugins/automations/src/backend/actions/index.ts index 32d011a29..6bda0d64b 100644 --- a/plugins/automations/src/backend/actions/index.ts +++ b/plugins/automations/src/backend/actions/index.ts @@ -1,15 +1,6 @@ -import { - DEFAULT_STEP_TIMEOUT_MS, - type Step, -} from "../../../../../src/types/automations.js"; +import type { Client } from "ssh2"; +import { DEFAULT_STEP_TIMEOUT_MS, type Step } from "../../types.js"; import { execCommand, execElevated } from "@termix/plugin-sdk/host-commands"; -import { withSshConnection } from "../ssh.js"; -import { createCurrentNotificationChannelRepository } from "../../../../../src/backend/database/repositories/factory.js"; -import { getSnippet, resolveSnippetCommandFor } from "../snippets.js"; -import { runDockerAction } from "../docker.js"; -import { runTunnelAction } from "../tunnels.js"; -import { sendAutomationNotification } from "../notify.js"; -import { automationFetch } from "../http.js"; import { renderRecord, renderTemplate } from "../template.js"; import { resolveTargets, type ResolvedTarget } from "./host-targets.js"; import { @@ -18,8 +9,14 @@ import { stepTimeout, type StepExecutionContext, type StepResult, + type StepRuntime, } from "./types.js"; +/** The admin allowlist of private hosts an HTTP step may reach. */ +const PRIVATE_ALLOWLIST_KEY = "notification_private_endpoint_allowlist"; +const errorText = (error: unknown) => + error instanceof Error ? error.message : String(error); + /** * One executor per step type. * @@ -30,22 +27,23 @@ import { export async function executeStep( step: Step, context: StepExecutionContext, + runtime: StepRuntime, ): Promise { switch (step.type) { case "notify": - return runNotify(step, context); + return runNotify(step, context, runtime); case "http": - return runHttp(step, context); + return runHttp(step, context, runtime); case "run_command": - return runCommand(step, context); + return runCommand(step, context, runtime); case "run_snippet": - return runSnippet(step, context); + return runSnippet(step, context, runtime); case "docker": - return runDocker(step, context); + return runDocker(step, context, runtime); case "tunnel": - return runTunnel(step, context); + return runTunnel(step, context, runtime); case "wol": - return runWol(step, context); + return runWol(step, context, runtime); case "wait": return runWait(step, context); case "set_var": @@ -64,6 +62,7 @@ export async function executeStep( async function runNotify( step: Extract, context: StepExecutionContext, + runtime: StepRuntime, ): Promise { const title = renderTemplate(step.title ?? "", context.template); const body = renderTemplate(step.body ?? "", context.template); @@ -77,49 +76,45 @@ async function runNotify( ); } - const repository = createCurrentNotificationChannelRepository(); - const channels = await repository.listNotificationChannels(context.userId); - const selected = channels.filter((channel) => - step.channelIds.includes(channel.id), - ); - - if (selected.length === 0) { - return fail("Selected notification channels no longer exist"); + const trigger = context.template.trigger ?? {}; + let result; + try { + result = await runtime.ctx.notify.send(step.channelIds, { + title: title || "Termix automation", + body, + severity: step.severity ?? "warning", + context: { + hostId: + context.template.host?.id ?? + (typeof trigger.hostId === "number" ? trigger.hostId : undefined), + hostName: + context.template.host?.name ?? + (typeof trigger.hostName === "string" ? trigger.hostName : undefined), + sourceId: context.automationId, + sourceName: title || undefined, + triggerType: + typeof trigger.type === "string" ? trigger.type : undefined, + value: trigger.value, + threshold: trigger.threshold, + }, + }); + } catch (error) { + return fail(errorText(error)); } - let delivered = 0; - const errors: string[] = []; - for (const channel of selected) { - if (!channel.enabled) continue; - try { - await sendAutomationNotification( - { id: channel.id, type: channel.type, config: channel.config }, - { - title, - body, - severity: step.severity ?? "warning", - context: context.template, - }, - ); - delivered++; - } catch (error) { - errors.push( - `${channel.name}: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - if (delivered === 0) { + const errors = result.failures.map((f) => `${f.name}: ${f.error}`); + if (result.delivered === 0) { return fail(errors.join("; ") || "No enabled channels to notify"); } return ok( - `Notified ${delivered} channel(s)${errors.length ? `; ${errors.join("; ")}` : ""}`, + `Notified ${result.delivered} channel(s)${errors.length ? `; ${errors.join("; ")}` : ""}`, ); } async function runHttp( step: Extract, context: StepExecutionContext, + runtime: StepRuntime, ): Promise { const url = renderTemplate(step.url, context.template); const headers = renderRecord(step.headers, context.template); @@ -132,11 +127,20 @@ async function runHttp( } try { - const response = await automationFetch(url, { + // Private delivery needs both the step's opt-in and an exact host in + // the admin allowlist. + const allowPrivateHosts = step.allowPrivateNetwork + ? parseAllowlist( + await runtime.ctx.settings.readCore(PRIVATE_ALLOWLIST_KEY), + ) + : []; + const response = await runtime.ctx.fetch(url, { method: step.method, - headers, + headers: body + ? { "Content-Type": "application/json", ...(headers ?? {}) } + : headers, body, - allowPrivateNetwork: step.allowPrivateNetwork, + allowPrivateHosts, timeoutMs: stepTimeout(context, step.timeoutMs, DEFAULT_STEP_TIMEOUT_MS), }); @@ -144,16 +148,31 @@ async function runHttp( const summary = `HTTP ${response.status} ${response.statusText}\n${text}`; return response.ok ? ok(summary) : fail(`HTTP ${response.status}`, summary); } catch (error) { - return fail(error instanceof Error ? error.message : String(error)); + return fail(errorText(error)); + } +} + +function parseAllowlist(raw: unknown): string[] { + if (typeof raw !== "string" || !raw) return []; + try { + const value = JSON.parse(raw); + if (!Array.isArray(value)) return []; + return value + .filter((entry): entry is string => typeof entry === "string") + .map((entry) => entry.trim().toLowerCase()) + .filter(Boolean); + } catch { + return []; } } async function runCommand( step: Extract, context: StepExecutionContext, + runtime: StepRuntime, ): Promise { const command = renderTemplate(step.command, context.template); - return runOnTargets(step.hostSelector, context, async (target) => { + return runOnTargets(step.hostSelector, context, runtime, async (target) => { if (context.dryRun) { return { output: `Would run on ${target.name}: ${command}` }; } @@ -162,6 +181,7 @@ async function runCommand( command, step.elevated, context, + runtime, step.timeoutMs, ); }); @@ -170,10 +190,20 @@ async function runCommand( async function runSnippet( step: Extract, context: StepExecutionContext, + runtime: StepRuntime, ): Promise { - const snippet = await getSnippet(context.userId, step.snippetId); + const snippets = runtime.deps.snippets(); + const getSnippet = snippets.get; + const resolveCommand = snippets.resolveCommand; + if ( + typeof getSnippet !== "function" || + typeof resolveCommand !== "function" + ) { + return fail("The snippets plugin is not available"); + } + const snippet = await getSnippet(step.snippetId).catch(() => null); - if (!snippet) return fail("Snippet not found, or the snippets plugin is off"); + if (!snippet) return fail("Snippet not found"); if (snippet.isNote) return fail("Notes cannot be executed on a host"); // Template values only ever reach the snippet through inputValues, never by @@ -181,18 +211,17 @@ async function runSnippet( // syntax of their own. const inputValues = renderRecord(step.inputValues, context.template) ?? {}; - return runOnTargets(step.hostSelector, context, async (target) => { - const command = await resolveSnippetCommandFor( - context.userId, + return runOnTargets(step.hostSelector, context, runtime, async (target) => { + const command = await resolveCommand( step.snippetId, { ip: target.host.ip, username: target.host.username, port: target.host.port, - name: target.host.name, + name: target.name, }, inputValues, - ); + ).catch(() => null); if (!command) { return { output: "", error: "Snippet could not be resolved" }; } @@ -205,6 +234,7 @@ async function runSnippet( command, step.elevated, context, + runtime, step.timeoutMs, ); }); @@ -213,40 +243,51 @@ async function runSnippet( async function runDocker( step: Extract, context: StepExecutionContext, + runtime: StepRuntime, ): Promise { const container = renderTemplate(step.container, context.template); if (!container) return fail("Container name is required"); + const action = runtime.deps.docker().action; + if (typeof action !== "function") { + return fail("The docker plugin is not available"); + } - return runOnTargets(step.hostSelector, context, async (target) => { + return runOnTargets(step.hostSelector, context, runtime, async (target) => { if (context.dryRun) { return { output: `Would ${step.action} container ${container} on ${target.name}`, }; } - const result = await runDockerAction( - context.userId, - target.id, - container, - step.action, - ); - return result.ok - ? { output: `Container ${container}: ${step.action} done` } - : { output: "", error: result.error }; + try { + await action(target.id, container, step.action); + return { output: `Container ${container}: ${step.action} done` }; + } catch (error) { + return { output: "", error: errorText(error) }; + } }); } async function runTunnel( step: Extract, context: StepExecutionContext, + runtime: StepRuntime, ): Promise { const name = renderTemplate(step.tunnelName, context.template); if (context.dryRun) return ok(`Would ${step.action} tunnel ${name}`); + const tunnels = runtime.deps.tunnels(); + const act = step.action === "connect" ? tunnels.start : tunnels.stop; + if (typeof act !== "function") { + return fail("The tunnels plugin is not available"); + } // A disconnect goes through the tunnels plugin's manual stop, which also // holds off its retries, so the tunnel stays down. - const result = await runTunnelAction(context.userId, step.action, name); - if (!result.ok) return fail(result.error); + try { + await act(name); + } catch (error) { + return fail(errorText(error)); + } return ok( step.action === "connect" ? `Tunnel ${name} connected` @@ -257,26 +298,23 @@ async function runTunnel( async function runWol( step: Extract, context: StepExecutionContext, + runtime: StepRuntime, ): Promise { - const host = await resolveTargets( - { kind: "host", hostId: step.hostId }, - context, - ); - const target = host.targets[0]; - if (!target?.host) return fail("Host not found or not accessible"); + const host = await runtime.ctx.hosts.get(step.hostId).catch(() => null); + if (!host) return fail("Host not found or not accessible"); + const name = host.name || host.ip; - const mac = (target.host as { macAddress?: string }).macAddress; - if (!mac) return fail("Host has no MAC address configured"); - if (context.dryRun) return ok(`Would wake ${target.name} (${mac})`); + const wake = runtime.deps.wakeOnLan().wake; + if (typeof wake !== "function") { + return fail("The wake-on-lan plugin is not available"); + } + if (context.dryRun) return ok(`Would wake ${name}`); try { - const { sendWakeOnLan, isValidMac } = - await import("../../../../../src/backend/utils/wake-on-lan.js"); - if (!isValidMac(mac)) return fail(`Invalid MAC address: ${mac}`); - await sendWakeOnLan(mac); - return ok(`Sent magic packet to ${target.name}`); + await wake(step.hostId); + return ok(`Sent magic packet to ${name}`); } catch (error) { - return fail(error instanceof Error ? error.message : String(error)); + return fail(errorText(error)); } } @@ -310,9 +348,10 @@ async function runSetVar( async function runOnTargets( selector: Extract["hostSelector"], context: StepExecutionContext, + runtime: StepRuntime, run: (target: ResolvedTarget) => Promise<{ output?: string; error?: string }>, ): Promise { - const { targets, skipped } = await resolveTargets(selector, context); + const { targets, skipped } = await resolveTargets(selector, context, runtime); if (targets.length === 0) { return fail( @@ -363,25 +402,27 @@ async function execOnHost( command: string, elevated: boolean | undefined, context: StepExecutionContext, + runtime: StepRuntime, timeoutMs: number | undefined, ): Promise<{ output?: string; error?: string }> { - const sshHost = host as ResolvedTarget["host"] & { sudoPassword?: string }; const timeout = stepTimeout(context, timeoutMs, DEFAULT_STEP_TIMEOUT_MS); if (timeout <= 0) return { error: "Run deadline exceeded" }; try { - const result = await withSshConnection( - sshHost as never, - { pool: "fleet", purpose: "fleet" }, - async (client) => { - if (elevated) { - return execElevated(client, command, sshHost.sudoPassword, { - timeoutMs: timeout, - }); - } - return execCommand(client, command, timeout); - }, - ); + const result = await runtime.ctx.ssh.withConnection< + { stdout: string; stderr: string; code: number | null }, + Client + >(host, { pool: "automations", purpose: "fleet" }, async (client) => { + if (elevated) { + return execElevated( + client, + command, + host.sudoPassword as string | undefined, + { timeoutMs: timeout }, + ); + } + return execCommand(client, command, timeout); + }); const output = [result.stdout, result.stderr].filter(Boolean).join("\n"); if (result.code === 0 || result.code === null) { @@ -389,6 +430,6 @@ async function execOnHost( } return { error: `Exited with code ${result.code}`, output }; } catch (error) { - return { error: error instanceof Error ? error.message : String(error) }; + return { error: errorText(error) }; } } diff --git a/plugins/automations/src/backend/actions/types.ts b/plugins/automations/src/backend/actions/types.ts index 8454272db..139387d49 100644 --- a/plugins/automations/src/backend/actions/types.ts +++ b/plugins/automations/src/backend/actions/types.ts @@ -1,4 +1,6 @@ -import type { Step } from "../../../../../src/types/automations.js"; +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import type { Step } from "../../types.js"; +import type { Deps } from "../deps.js"; import type { TemplateContext } from "../template.js"; /** What every executor returns. `output` is what later steps can read. */ @@ -12,6 +14,12 @@ export interface StepResult { halt?: { status: "success" | "failed" }; } +/** What the executors reach the rest of Termix through. */ +export interface StepRuntime { + ctx: PluginContext; + deps: Deps; +} + export interface StepExecutionContext { userId: string; automationId: number; diff --git a/plugins/automations/src/backend/conditions.ts b/plugins/automations/src/backend/conditions.ts index eadd45a0a..794c767d6 100644 --- a/plugins/automations/src/backend/conditions.ts +++ b/plugins/automations/src/backend/conditions.ts @@ -1,7 +1,4 @@ -import type { - MetricPath, - Operator, -} from "../../../../src/types/automations.js"; +import type { MetricPath, Operator } from "../types.js"; /** * Operator evaluation and metric extraction. diff --git a/plugins/automations/src/backend/deps.ts b/plugins/automations/src/backend/deps.ts new file mode 100644 index 000000000..a5b2d3234 --- /dev/null +++ b/plugins/automations/src/backend/deps.ts @@ -0,0 +1,121 @@ +import type { PluginServices } from "@termix/plugin-sdk/backend"; +import type { DockerEventKind } from "../types.js"; + +/* + * Other plugins, as automations reaches them. Every call runs as the current + * actor (the automation's owner, set by the engine with ctx.asUser). A + * missing provider answers an empty handle, so each method is probed before + * it is called. + */ + +export interface SnippetsAccessV1 { + list?: () => Promise>; + get: (id: number) => Promise<{ + id: number; + name: string; + content: string; + isNote: boolean; + } | null>; + resolveCommand: ( + id: number, + vars: { ip?: string; username?: string; port?: number; name?: string }, + inputValues?: Record, + ) => Promise; +} + +export interface FleetsAccessV1 { + list: () => Promise>; + members: ( + fleetId: number, + ) => Promise>; +} + +export interface TunnelsAccessV1 { + start: (name: string) => Promise; + stop: (name: string) => Promise; +} + +export interface DockerContainersV1 { + action: ( + hostId: number, + container: string, + action: "start" | "stop" | "restart", + ) => Promise; +} + +export interface DockerEvent { + hostId: number; + container: string; + event: DockerEventKind; +} + +export interface DockerEventsV1 { + subscribe: ( + hostId: number, + listener: (event: DockerEvent) => void, + ) => Promise<() => void>; +} + +export interface MetricsViewersV1 { + register: ( + hostId: number, + ) => Promise<{ viewerSessionId: string } | { skipped: true; reason: string }>; + heartbeat: (viewerSessionId: string) => boolean; + unregister: (hostId: number, viewerSessionId: string) => void; +} + +/** Provided by the wake-on-lan plugin. It resolves the host's MAC itself. */ +export interface WakeOnLanV1 { + wake: (hostId: number) => Promise; +} + +/** The plugin id behind each service, for "needs " messages. */ +export const PROVIDERS = { + snippets: "snippets.access", + fleets: "fleets.access", + tunnels: "tunnels.access", + docker: "docker.containers", + "docker-events": "docker.events", + "host-metrics": "host-metrics.viewers", + "wake-on-lan": "wake-on-lan.send", +} as const; + +export type ProviderKey = keyof typeof PROVIDERS; + +/** The plugin a provider key belongs to. */ +export function pluginOf(key: ProviderKey): string { + return key === "docker-events" ? "docker" : key; +} + +export type Deps = ReturnType; + +export function createDeps(services: PluginServices) { + function handle(name: string): Partial { + try { + return services.get(name) as Partial; + } catch { + return {}; + } + } + + return { + /** Which providers are running now. */ + available(key: ProviderKey): boolean { + return services.providers(PROVIDERS[key]).length > 0; + }, + providers(): Record { + const result = {} as Record; + for (const key of Object.keys(PROVIDERS) as ProviderKey[]) { + result[key] = services.providers(PROVIDERS[key]).length > 0; + } + return result; + }, + snippets: () => handle(PROVIDERS.snippets), + fleets: () => handle(PROVIDERS.fleets), + tunnels: () => handle(PROVIDERS.tunnels), + docker: () => handle(PROVIDERS.docker), + dockerEvents: () => handle(PROVIDERS["docker-events"]), + viewers: () => handle(PROVIDERS["host-metrics"]), + wakeOnLan: () => handle(PROVIDERS["wake-on-lan"]), + }; +} diff --git a/plugins/automations/src/backend/docker-watcher.ts b/plugins/automations/src/backend/docker-watcher.ts index f85366966..1a9c5a61f 100644 --- a/plugins/automations/src/backend/docker-watcher.ts +++ b/plugins/automations/src/backend/docker-watcher.ts @@ -1,7 +1,6 @@ -import type { AutomationDefinition } from "../../../../src/types/automations.js"; -import { createCurrentAutomationRepository } from "../../../../src/backend/database/repositories/factory.js"; -import { subscribeDockerEvents, type DockerEvent } from "./docker.js"; -import { onDockerEvent } from "./triggers.js"; +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import type { Deps, DockerEvent } from "./deps.js"; +import type { DockerEvent as TriggerDockerEvent } from "./triggers.js"; /** * Keeps a docker.events subscription for every host a docker_event trigger @@ -21,95 +20,71 @@ interface Watch { subscribedAt: number; } -const watches = new Map(); +export function createDockerWatcher( + ctx: Pick, + deps: Pick, + watchedHosts: () => Promise>, + onEvent: (event: TriggerDockerEvent) => Promise, +) { + const watches = new Map(); + const keyOf = (hostId: number, userId: string) => `${userId}:${hostId}`; -const keyOf = (hostId: number, userId: string) => `${userId}:${hostId}`; - -/** Hosts named by an enabled docker_event trigger, with the owning user. */ -export async function listDockerWatchedHosts(): Promise> { - const watched = new Map(); - - try { - const rows = await createCurrentAutomationRepository().listAllEnabled(); - for (const row of rows) { - let definition: AutomationDefinition; - try { - definition = JSON.parse(row.definition) as AutomationDefinition; - } catch { - continue; - } - - const trigger = definition.trigger; - if (trigger?.kind !== "docker_event") continue; - - const selector = trigger.hostSelector; - if (selector?.kind === "host") { - watched.set(selector.hostId, row.userId); - } else if (selector?.kind === "hosts") { - for (const hostId of selector.hostIds) watched.set(hostId, row.userId); - } - // Fleet and "all" selectors are deliberately not expanded: watching - // every host a user owns for container state is far too costly. - } - } catch { - return watched; - } - - return watched; -} - -/** Brings the subscriptions in line with the watched hosts. */ -export async function reconcileDockerWatch( - now: number = Date.now(), - watched?: Map, -): Promise { - const wanted = watched ?? (await listDockerWatchedHosts()); - const wantedKeys = new Set( - [...wanted].map(([hostId, userId]) => keyOf(hostId, userId)), - ); - - for (const [key, watch] of watches) { - if (wantedKeys.has(key)) continue; - watch.unsubscribe?.(); - watches.delete(key); - } - - for (const [hostId, userId] of wanted) { - const key = keyOf(hostId, userId); - let watch = watches.get(key); - if (!watch) { - watch = { - listener: (event) => { - void onDockerEvent({ - hostId: event.hostId, - ownerUserId: userId, - container: event.container, - event: event.event, - }).catch(() => undefined); - }, - unsubscribe: null, - subscribedAt: 0, - }; - watches.set(key, watch); - } - if (watch.subscribedAt && now - watch.subscribedAt < RESUBSCRIBE_MS) { - continue; - } - try { - watch.unsubscribe = await subscribeDockerEvents( - userId, - hostId, - watch.listener, + return { + /** Brings the subscriptions in line with the watched hosts. */ + async reconcile(now: number = Date.now()): Promise { + const wanted = await watchedHosts(); + const wantedKeys = new Set( + [...wanted].map(([hostId, userId]) => keyOf(hostId, userId)), ); - watch.subscribedAt = watch.unsubscribe ? now : 0; - } catch { - watch.subscribedAt = 0; - } - } -} -/** Drops every subscription, for shutdown and tests. */ -export function resetDockerWatcher(): void { - for (const watch of watches.values()) watch.unsubscribe?.(); - watches.clear(); + for (const [key, watch] of watches) { + if (wantedKeys.has(key)) continue; + watch.unsubscribe?.(); + watches.delete(key); + } + + for (const [hostId, userId] of wanted) { + const key = keyOf(hostId, userId); + let watch = watches.get(key); + if (!watch) { + watch = { + listener: (event) => { + void onEvent({ + hostId: event.hostId, + ownerUserId: userId, + container: event.container, + event: event.event, + }).catch(() => undefined); + }, + unsubscribe: null, + subscribedAt: 0, + }; + watches.set(key, watch); + } + if (watch.subscribedAt && now - watch.subscribedAt < RESUBSCRIBE_MS) { + continue; + } + const subscribe = deps.dockerEvents().subscribe; + if (typeof subscribe !== "function") { + watch.subscribedAt = 0; + continue; + } + try { + const current = watch; + watch.unsubscribe = await ctx.asUser(userId, () => + subscribe(hostId, current.listener), + ); + watch.subscribedAt = now; + } catch { + watch.subscribedAt = 0; + } + } + }, + + /** Drops every subscription, on deactivate. */ + reset(): void { + for (const watch of watches.values()) watch.unsubscribe?.(); + watches.clear(); + }, + }; } diff --git a/plugins/automations/src/backend/docker.ts b/plugins/automations/src/backend/docker.ts deleted file mode 100644 index 41d168c32..000000000 --- a/plugins/automations/src/backend/docker.ts +++ /dev/null @@ -1,85 +0,0 @@ -import type { PluginServices } from "@termix/plugin-sdk/backend"; - -let current: PluginServices | null = null; - -/** Set in activate, cleared on deactivate. */ -export function setDockerServices(services: PluginServices | null): void { - current = services; -} - -export type DockerEventName = "exited" | "started" | "unhealthy" | "restarting"; - -export interface DockerEvent { - hostId: number; - container: string; - event: DockerEventName; -} - -/** The docker plugin's "docker.containers" service, version 1. */ -interface DockerService { - action: ( - hostId: number, - container: string, - action: "start" | "stop" | "restart", - ) => Promise; -} - -/** The docker plugin's "docker.events" service, version 1. */ -interface DockerEventsService { - subscribe: ( - hostId: number, - listener: (event: DockerEvent) => void, - ) => Promise<() => void>; -} - -const UNAVAILABLE = "The docker plugin is not available"; - -function service( - name: string, - userId: string, -): Partial | null { - try { - return current?.get(name, { userId }) ?? null; - } catch { - return null; - } -} - -/** - * Starts, stops or restarts a container as userId. Optional: without the - * docker plugin the step fails with a clear message. - */ -export async function runDockerAction( - userId: string, - hostId: number, - container: string, - action: "start" | "stop" | "restart", -): Promise<{ ok: boolean; unavailable?: boolean; error?: string }> { - const docker = service("docker.containers", userId); - if (!docker || typeof docker.action !== "function") { - return { ok: false, unavailable: true, error: UNAVAILABLE }; - } - try { - await docker.action(hostId, container, action); - return { ok: true }; - } catch (error) { - return { - ok: false, - error: error instanceof Error ? error.message : String(error), - }; - } -} - -/** - * Subscribes `listener` to container changes on a host as userId, or - * resolves null while the docker plugin is off. - */ -export async function subscribeDockerEvents( - userId: string, - hostId: number, - listener: (event: DockerEvent) => void, -): Promise<(() => void) | null> { - const events = service("docker.events", userId); - if (!events || typeof events.subscribe !== "function") return null; - return events.subscribe(hostId, listener); -} diff --git a/plugins/automations/src/backend/engine.ts b/plugins/automations/src/backend/engine.ts index 60b5454db..902a584f5 100644 --- a/plugins/automations/src/backend/engine.ts +++ b/plugins/automations/src/backend/engine.ts @@ -1,22 +1,24 @@ -import type { - AutomationDefinition, - RunStatus, - Step, -} from "../../../../src/types/automations.js"; +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import type { AutomationDefinition, RunStatus, Step } from "../types.js"; import { DEFAULT_MAX_RUN_SECONDS, MAX_AUTOMATION_DEPTH, MAX_STEP_OUTPUT_BYTES, -} from "../../../../src/types/automations.js"; -import { createCurrentAutomationRepository } from "../../../../src/backend/database/repositories/factory.js"; -import { statsLogger } from "../../../../src/backend/utils/logger.js"; -import { resolveHostById } from "../../../../src/backend/hosts/host-resolver.js"; -import { notifyAutomationInternalEvent } from "../../../../src/backend/hosts/automation-events.js"; +} from "../types.js"; import { executeStep } from "./actions/index.js"; import type { StepExecutionContext, StepResult } from "./actions/types.js"; import { compare } from "./conditions.js"; +import type { Deps } from "./deps.js"; +import type { + AutomationEngineRow, + AutomationRepository, +} from "./repository.js"; +import { missingPlugins } from "./requirements.js"; import { renderTemplate, type TemplateContext } from "./template.js"; +/** Emitted when a run fails, so an automation_failed trigger can react. */ +export const TOPIC_AUTOMATION_FAILED = "plugin.automations.automation_failed"; + export interface RunRequest { automationId: number; triggerType: string; @@ -36,38 +38,53 @@ export interface RunOutcome { } /** - * Executes automations. - * - * Both HTTP handlers and the metrics hooks live in this same process, so this - * is a plain singleton rather than anything cross-process. State that has to - * survive a restart (cooldowns, dwell windows) lives in the database; the only - * thing held in memory is the set of runs currently in flight, which is - * meaningless after a restart anyway. + * Executes automations. One per activation. State that has to survive a + * restart (cooldowns, dwell windows) lives in the database; the only thing + * held in memory is the set of runs in flight, which is meaningless after a + * restart anyway. */ export class AutomationEngine { - private static instance: AutomationEngine; - private readonly running = new Set(); private readonly queued = new Map(); - static getInstance(): AutomationEngine { - if (!AutomationEngine.instance) { - AutomationEngine.instance = new AutomationEngine(); - } - return AutomationEngine.instance; - } + constructor( + private readonly ctx: PluginContext, + private readonly repository: AutomationRepository, + private readonly deps: Deps, + ) {} isRunning(automationId: number): boolean { return this.running.has(automationId); } + /** + * Runs an automation as its owner. Never throws: a failure is the outcome. + */ async run(request: RunRequest): Promise { - const repository = createCurrentAutomationRepository(); - const automation = await repository.findById(request.automationId); - + let automation: AutomationEngineRow | null; + try { + automation = await this.repository.findById(request.automationId); + } catch (err) { + return { runId: null, status: "failed", error: errorText(err) }; + } if (!automation) { return { runId: null, status: "failed", error: "Automation not found" }; } + const owner = automation; + try { + return await this.ctx.asUser(owner.userId, () => + this.runAsOwner(owner, request), + ); + } catch (err) { + return { runId: null, status: "failed", error: errorText(err) }; + } + } + + private async runAsOwner( + automation: AutomationEngineRow, + request: RunRequest, + ): Promise { + const repository = this.repository; const depth = request.depth ?? 0; const ancestry = request.ancestry ?? []; @@ -99,6 +116,30 @@ export class AutomationEngine { }; } + // A trigger or step whose plugin is off skips the whole run with the + // reason on record, rather than running half of it. + const missing = missingPlugins(definition, this.deps); + if (missing.length > 0) { + const reason = `Needs the ${missing.join(", ")} plugin${missing.length > 1 ? "s" : ""}`; + this.ctx.log.info( + `Skipping automation ${automation.id} (${request.triggerType}): ${reason}`, + ); + const run = await repository.createRun({ + automationId: automation.id, + userId: automation.userId, + triggerType: request.triggerType, + triggerContext: JSON.stringify(request.triggerContext ?? {}), + status: "skipped", + parentRunId: request.parentRunId ?? null, + }); + await repository.finishRun(run.id, { + status: "skipped", + error: reason, + durationMs: 0, + }); + return { runId: run.id, status: "skipped", error: reason }; + } + // A second trigger while a run is in flight is recorded as skipped rather // than dropped silently, so the history explains what happened. // @@ -182,10 +223,7 @@ export class AutomationEngine { let host: TemplateContext["host"]; if (request.triggerHostId) { try { - const resolved = await resolveHostById( - request.triggerHostId, - automation.userId, - ); + const resolved = await this.ctx.hosts.get(request.triggerHostId); if (resolved) { host = { id: request.triggerHostId, @@ -245,7 +283,7 @@ export class AutomationEngine { } } catch (err) { status = "failed"; - error = err instanceof Error ? err.message : String(err); + error = errorText(err); } finally { this.running.delete(automation.id); } @@ -257,31 +295,21 @@ export class AutomationEngine { }); if (status === "failed") { - statsLogger.warn(`Automation "${automation.name}" failed`, { - operation: "automation_run_failed", - automationId: automation.id, - runId: run.id, - error, - }); + this.ctx.log.warn( + `Automation "${automation.name}" (${automation.id}) failed in run ${run.id}: ${error ?? "unknown error"}`, + ); // An automation_failed handler that itself fails must not re-announce // its own failure, so the event is not emitted for runs that this event // already started. if (request.triggerType !== "internal_event") { - // Emitted onto the bus rather than imported from hosts/: the bus is - // fire-and-forget, so this no longer needs the lazy-import dance that - // was working around the repositories cycle. - notifyAutomationInternalEvent( - "automation_failed", - automation.userId, - undefined, - { - automationId: automation.id, - automationName: automation.name, - runId: run.id, - error: error ?? null, - }, - ); + this.ctx.events.emit(TOPIC_AUTOMATION_FAILED, { + userId: automation.userId, + automationId: automation.id, + automationName: automation.name, + runId: run.id, + error: error ?? null, + }); } } @@ -301,7 +329,7 @@ export class AutomationEngine { error?: string; halted?: { status: "success" | "failed" }; }> { - const repository = createCurrentAutomationRepository(); + const repository = this.repository; for (const step of steps) { if (step.enabled === false) continue; @@ -382,12 +410,12 @@ export class AutomationEngine { let result: StepResult; try { - result = await executeStep(step, context); + result = await executeStep(step, context, { + ctx: this.ctx, + deps: this.deps, + }); } catch (err) { - result = { - success: false, - error: err instanceof Error ? err.message : String(err), - }; + result = { success: false, error: errorText(err) }; } const { text, truncated } = truncate(result.output); @@ -432,6 +460,10 @@ export class AutomationEngine { } } +function errorText(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + /** Keeps a single step's output from bloating the database. */ function truncate(output: string | undefined): { text: string | null; diff --git a/plugins/automations/src/backend/headless-viewer.ts b/plugins/automations/src/backend/headless-viewer.ts index 8049c2568..ad07fd3f2 100644 --- a/plugins/automations/src/backend/headless-viewer.ts +++ b/plugins/automations/src/backend/headless-viewer.ts @@ -1,105 +1,101 @@ -import { statsLogger } from "../../../../src/backend/utils/logger.js"; -import { listAutomationWatchedHosts } from "./triggers.js"; +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import type { Deps } from "./deps.js"; /** * Keeps metric collection running for hosts an automation watches. * - * Heavy metric collection is normally started by a UI viewer and stops when - * the last one leaves, which means threshold rules only ever evaluated while - * somebody had the host open. Automations register a synthetic viewer instead - * of bypassing that mechanism, so a real viewer arriving or leaving still - * behaves exactly as before. - * - * The catch is `cleanupInactiveViewers`, which drops any viewer whose - * heartbeat is older than 120s. Without the heartbeat below, headless polling - * would quietly stop two minutes after it started. + * Host metrics only polls hosts somebody is looking at. Automations register + * as a viewer through the host-metrics.viewers service, as the automation's + * owner, and heartbeat every tick so the viewer is not reaped. A heartbeat + * that comes back false means host-metrics restarted and forgot the viewer, + * so the next tick registers it again. */ -export interface ViewerRegistry { - registerViewer(hostId: number, sessionId: string, userId: string): void; - unregisterViewer(hostId: number, sessionId: string): void; - updateHeartbeat(sessionId: string): boolean; +interface Registered { + userId: string; + viewerSessionId: string; } -const SESSION_PREFIX = "automation:"; - -let registry: ViewerRegistry | null = null; -const registered = new Map(); - -export function setViewerRegistry(next: ViewerRegistry | null): void { - registry = next; -} - -export function automationSessionId(hostId: number): string { - return `${SESSION_PREFIX}${hostId}`; -} - -/** - * Brings the set of synthetic viewers in line with what the enabled - * automations currently watch, and heartbeats the ones that stay. - */ -export async function reconcileHeadlessViewers(): Promise<{ - added: number; - removed: number; - active: number; -}> { - if (!registry) return { added: 0, removed: 0, active: 0 }; - - let watched: Map; - try { - watched = await listAutomationWatchedHosts(); - } catch { - return { added: 0, removed: 0, active: registered.size }; - } - - let added = 0; - let removed = 0; - - for (const [hostId, userId] of watched) { - const sessionId = automationSessionId(hostId); - if (registered.has(hostId)) { - // Refresh before the 120s reaper would take it. - registry.updateHeartbeat(sessionId); - continue; - } +export function createHeadlessViewers( + ctx: Pick, + deps: Pick, + watchedHosts: () => Promise>, +) { + const registered = new Map(); + async function release(hostId: number, entry: Registered): Promise { + registered.delete(hostId); + const unregister = deps.viewers().unregister; + if (typeof unregister !== "function") return; try { - registry.registerViewer(hostId, sessionId, userId); - registered.set(hostId, userId); - added++; - } catch (error) { - statsLogger.warn("Could not start headless metrics for a host", { - operation: "automation_headless_register_error", - hostId, - error: error instanceof Error ? error.message : String(error), - }); - } - } - - for (const hostId of [...registered.keys()]) { - if (watched.has(hostId)) continue; - try { - registry.unregisterViewer(hostId, automationSessionId(hostId)); + await ctx.asUser(entry.userId, () => + unregister(hostId, entry.viewerSessionId), + ); } catch { // Already gone; drop it either way. } - registered.delete(hostId); - removed++; } - return { added, removed, active: registered.size }; -} - -/** Drops every synthetic viewer, for shutdown and tests. */ -export function releaseHeadlessViewers(): void { - if (registry) { - for (const hostId of registered.keys()) { - try { - registry.unregisterViewer(hostId, automationSessionId(hostId)); - } catch { - // Nothing useful to do during teardown. + return { + /** Brings the viewers in line with what the automations watch. */ + async reconcile(): Promise<{ added: number; removed: number }> { + const viewers = deps.viewers(); + if (typeof viewers.register !== "function") { + // host-metrics is off: its viewers went with it. + registered.clear(); + return { added: 0, removed: 0 }; } - } - } - registered.clear(); + + const watched = await watchedHosts(); + let added = 0; + let removed = 0; + + for (const [hostId, entry] of [...registered]) { + if (watched.get(hostId) === entry.userId) continue; + await release(hostId, entry); + removed++; + } + + for (const [hostId, userId] of watched) { + const existing = registered.get(hostId); + if (existing) { + const alive = await ctx + .asUser(userId, () => viewers.heartbeat!(existing.viewerSessionId)) + .catch(() => false); + if (alive) continue; + registered.delete(hostId); + } + + try { + const result = await ctx.asUser(userId, () => + viewers.register!(hostId), + ); + if ("viewerSessionId" in result) { + registered.set(hostId, { + userId, + viewerSessionId: result.viewerSessionId, + }); + added++; + } + } catch (error) { + ctx.log.warn( + `Could not start headless metrics for host ${hostId}: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } + } + + return { added, removed }; + }, + + /** Drops every viewer, on deactivate. */ + async releaseAll(): Promise { + for (const [hostId, entry] of [...registered]) { + await release(hostId, entry); + } + }, + + registeredHosts: () => [...registered.keys()], + }; } diff --git a/plugins/automations/src/backend/http.ts b/plugins/automations/src/backend/http.ts deleted file mode 100644 index 2e15afd33..000000000 --- a/plugins/automations/src/backend/http.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { safeOutboundFetch } from "../../../../src/backend/utils/safe-outbound-fetch.js"; -import { readNotificationPrivateAllowlist } from "../../../../src/backend/utils/notification-egress.js"; - -/** - * Outbound HTTP for automation steps and notification channels. - * - * safeOutboundFetch refuses private and loopback addresses, which is the right - * default against SSRF but also blocks the self-hosted ntfy or Gotify sitting - * on a LAN that many installs actually use. Private delivery therefore needs - * both a channel opt-in and an exact host in the administrator allowlist; - * scheme validation, DNS pinning and redirect refusal remain in force. - */ -export interface AutomationFetchOptions { - method?: string; - headers?: Record; - body?: string; - allowPrivateNetwork?: boolean; - timeoutMs?: number; -} - -export async function automationFetch( - url: string, - options: AutomationFetchOptions = {}, -): Promise { - const { - method = "GET", - headers, - body, - allowPrivateNetwork, - timeoutMs = 30_000, - } = options; - - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), Math.max(timeoutMs, 1)); - - const init: RequestInit = { - method, - headers: body - ? { "Content-Type": "application/json", ...(headers ?? {}) } - : headers, - body, - signal: controller.signal, - }; - - try { - if (allowPrivateNetwork) { - return await privateNetworkFetch(url, init); - } - return await safeOutboundFetch(url, init); - } finally { - clearTimeout(timer); - } -} - -/** - * The opt-in path still goes through the guarded resolver. Only an exact host - * authorized by an administrator may resolve to a private address. - */ -async function privateNetworkFetch( - rawUrl: string, - init: RequestInit, -): Promise { - let parsed: URL; - try { - parsed = new URL(rawUrl); - } catch { - throw new Error("Invalid URL"); - } - - if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { - throw new Error("Only http and https URLs are allowed"); - } - if (parsed.username || parsed.password) { - throw new Error("URLs with embedded credentials are not allowed"); - } - - const allowlist = await readNotificationPrivateAllowlist(); - return safeOutboundFetch(rawUrl, init, allowlist); -} diff --git a/plugins/automations/src/backend/index.ts b/plugins/automations/src/backend/index.ts index f14725a5e..fa4832580 100644 --- a/plugins/automations/src/backend/index.ts +++ b/plugins/automations/src/backend/index.ts @@ -1,63 +1,61 @@ +import type { Router } from "express"; import type { PluginContext } from "@termix/plugin-sdk/backend"; -import { setPluginSsh } from "./ssh.js"; -import { setPluginServices } from "./snippets.js"; -import { setTunnelServices } from "./tunnels.js"; -import { setDockerServices } from "./docker.js"; -import { startAutomationsService, stopAutomationsService } from "./routes.js"; -import { - startAutomationScheduler, - stopAutomationScheduler, -} from "./scheduler.js"; -import { onInternalEvent } from "./triggers.js"; +import { createDeps } from "./deps.js"; +import { createDockerWatcher } from "./docker-watcher.js"; +import { AutomationEngine } from "./engine.js"; +import { createHeadlessViewers } from "./headless-viewer.js"; +import { createAutomationRepository } from "./repository.js"; +import { registerRoutes } from "./routes.js"; +import { createScheduler, STARTUP_DELAY_MS, TICK_MS } from "./scheduler.js"; +import { createAutomationsService } from "./service.js"; +import { createTriggers } from "./triggers.js"; -/** What the tunnels plugin emits when a tunnel drops without being asked to. */ -interface TunnelDisconnectedEvent { - userId?: string; - hostId?: number; - tunnelName?: string; -} +export type { AutomationsAccessV1, AutomationSummary } from "./service.js"; export async function activate(ctx: PluginContext) { - setPluginSsh(ctx.ssh); - ctx.disposables.add(() => setPluginSsh(null)); - setPluginServices(ctx.services); - ctx.disposables.add(() => setPluginServices(null)); - setTunnelServices(ctx.services); - ctx.disposables.add(() => setTunnelServices(null)); - setDockerServices(ctx.services); - ctx.disposables.add(() => setDockerServices(null)); + const repository = await createAutomationRepository(ctx.db, async () => { + const channels = await ctx.notify.channels(); + return channels.map((channel) => channel.id); + }); + const deps = createDeps(ctx.services); + const engine = new AutomationEngine(ctx, repository, deps); + const triggers = createTriggers(repository, engine, ctx.log); + triggers.subscribe(ctx.events); - // Nothing arrives here while the tunnels plugin is off, which is the whole - // of the optional dependency. - ctx.events.on("plugin.tunnels.tunnel_disconnected", (payload) => { - const event = payload as TunnelDisconnectedEvent; - if (!event?.userId) return; - void onInternalEvent({ - event: "tunnel_disconnected", - userId: event.userId, - hostId: event.hostId, - details: { tunnelName: event.tunnelName }, - }).catch((error: unknown) => { - ctx.log.warn( - `Tunnel disconnect trigger failed: ${ - error instanceof Error ? error.message : String(error) - }`, - ); - }); + const viewers = createHeadlessViewers(ctx, deps, () => + triggers.watchedHosts("metric_threshold"), + ); + const docker = createDockerWatcher( + ctx, + deps, + () => triggers.watchedHosts("docker_event"), + triggers.onDockerEvent, + ); + ctx.disposables.add(() => { + docker.reset(); + void viewers.releaseAll(); }); - startAutomationsService( + const scheduler = createScheduler({ + repository, + engine, + log: ctx.log, + reconcile: [() => viewers.reconcile(), (now) => docker.reconcile(now)], + }); + ctx.schedule.after(STARTUP_DELAY_MS, () => scheduler.tick()); + ctx.schedule.every(TICK_MS, () => scheduler.tick()); + + registerRoutes( // The webhook route authenticates on its own per-automation token rather // than a session, which is the point of an inbound webhook. - ctx.http.router({ public: ["/webhook/:token"] }), + ctx.http.router({ public: ["/webhook/:token"] }), + { ctx, repository, engine, deps }, ); - startAutomationScheduler(); - ctx.log.info( - "Automations routes mounted at /plugin-api/automations; scheduler started", + + ctx.services.provide( + "automations.access", + createAutomationsService(ctx, repository, engine, deps), ); } -export async function deactivate() { - stopAutomationScheduler(); - stopAutomationsService(); -} +export async function deactivate() {} diff --git a/plugins/automations/src/backend/notify.ts b/plugins/automations/src/backend/notify.ts deleted file mode 100644 index 25d0e2c61..000000000 --- a/plugins/automations/src/backend/notify.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { statsLogger } from "../../../../src/backend/utils/logger.js"; -import { automationFetch } from "./http.js"; -import type { TemplateContext } from "./template.js"; - -/** - * Notification delivery for automations. - * - * The alert engine special-cased Discord because its dispatcher only knew - * webhook and ntfy; here every transport goes through one switch, so adding a - * channel type is a single edit. - */ -export interface AutomationChannel { - id: number; - type: string; - config: string; -} - -export interface AutomationNotification { - title: string; - body: string; - severity: "info" | "warning" | "critical"; - context?: TemplateContext; -} - -const NTFY_PRIORITY: Record = { - info: 2, - warning: 3, - critical: 5, -}; - -const NTFY_TAGS: Record = { - info: "information_source", - warning: "warning", - critical: "rotating_light", -}; - -const DISCORD_COLORS: Record = { - info: 3066993, - warning: 16753920, - critical: 15158332, -}; - -export async function sendAutomationNotification( - channel: AutomationChannel, - notification: AutomationNotification, -): Promise { - let config: Record; - try { - config = JSON.parse(channel.config) as Record; - } catch { - throw new Error("Channel configuration is not valid JSON"); - } - - const allowPrivateNetwork = config.allowPrivateNetwork === true; - - switch (channel.type) { - case "webhook": - return sendWebhook(config, notification, allowPrivateNetwork); - case "ntfy": - return sendNtfy(config, notification, allowPrivateNetwork); - case "discord": - return sendDiscord(config, notification, allowPrivateNetwork); - default: - throw new Error(`Unsupported channel type: ${channel.type}`); - } -} - -function requireUrl(config: Record): string { - const url = typeof config.url === "string" ? config.url.trim() : ""; - if (!url) throw new Error("Channel is missing a URL"); - return url; -} - -async function sendWebhook( - config: Record, - notification: AutomationNotification, - allowPrivateNetwork: boolean, -): Promise { - const url = requireUrl(config); - const method = config.method === "PUT" ? "PUT" : "POST"; - const headers = - config.headers && typeof config.headers === "object" - ? (config.headers as Record) - : {}; - - const response = await automationFetch(url, { - method, - headers, - body: JSON.stringify({ - title: notification.title, - hostName: - notification.context?.host?.name ?? - notification.context?.trigger?.hostName, - hostId: - notification.context?.host?.id ?? notification.context?.trigger?.hostId, - ruleName: notification.title, - ruleId: notification.context?.run?.automationId, - triggerType: notification.context?.trigger?.type, - value: notification.context?.trigger?.value, - threshold: notification.context?.trigger?.threshold, - message: notification.body, - severity: notification.severity, - timestamp: new Date().toISOString(), - }), - allowPrivateNetwork, - }); - - if (!response.ok) { - throw new Error(`HTTP ${response.status} ${response.statusText}`); - } -} - -async function sendNtfy( - config: Record, - notification: AutomationNotification, - allowPrivateNetwork: boolean, -): Promise { - const base = requireUrl(config).replace(/\/$/, ""); - const topic = typeof config.topic === "string" ? config.topic.trim() : ""; - if (!topic) throw new Error("ntfy channel is missing a topic"); - - const headers: Record = { - Title: notification.title || "Termix automation", - Priority: String(NTFY_PRIORITY[notification.severity] ?? 3), - Tags: NTFY_TAGS[notification.severity] ?? "information_source", - }; - if (typeof config.token === "string" && config.token) { - headers.Authorization = `Bearer ${config.token}`; - } - - const response = await automationFetch(`${base}/${topic}`, { - method: "POST", - headers, - body: notification.body || notification.title, - allowPrivateNetwork, - }); - - if (!response.ok) { - throw new Error(`HTTP ${response.status} ${response.statusText}`); - } -} - -async function sendDiscord( - config: Record, - notification: AutomationNotification, - allowPrivateNetwork: boolean, -): Promise { - const url = requireUrl(config); - const payload: Record = { - embeds: [ - { - title: notification.title || "Termix automation", - description: notification.body || undefined, - color: DISCORD_COLORS[notification.severity] ?? 3447003, - timestamp: new Date().toISOString(), - }, - ], - }; - if (typeof config.username === "string" && config.username) { - payload.username = config.username; - } - if (typeof config.avatar_url === "string" && config.avatar_url) { - payload.avatar_url = config.avatar_url; - } - - const response = await automationFetch(url, { - method: "POST", - body: JSON.stringify(payload), - allowPrivateNetwork, - }); - - if (!response.ok) { - const detail = await response.text().catch(() => ""); - throw new Error( - `HTTP ${response.status} ${response.statusText}${detail ? `: ${detail}` : ""}`, - ); - } -} - -/** Fire-and-forget wrapper for callers that must not block on delivery. */ -export function sendAutomationNotificationSafely( - channel: AutomationChannel, - notification: AutomationNotification, -): void { - sendAutomationNotification(channel, notification).catch((error) => { - statsLogger.warn("Automation notification failed", { - operation: "automation_notification_error", - channelId: channel.id, - type: channel.type, - error: error instanceof Error ? error.message : String(error), - }); - }); -} diff --git a/plugins/automations/src/backend/repository.ts b/plugins/automations/src/backend/repository.ts new file mode 100644 index 000000000..dbf1f9908 --- /dev/null +++ b/plugins/automations/src/backend/repository.ts @@ -0,0 +1,738 @@ +import { and, asc, desc, eq, inArray, lt, lte, sql } from "drizzle-orm"; +import type { PluginDatabase } from "@termix/plugin-sdk/backend"; +import { + automations as automationsDef, + channels as channelsDef, + runSteps as runStepsDef, + runs as runsDef, + schedules as schedulesDef, + triggerState as triggerStateDef, +} from "./tables.js"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Tables come from ctx.db.define, which the SDK hands back untyped, and the +// drizzle handle is the server's own. Typed at this module's edge instead. +type Table = any; +type Drizzle = any; +type Row = Record; +/* eslint-enable @typescript-eslint/no-explicit-any */ + +export interface AutomationRow { + id: number; + user_id: string; + name: string; + description: string | null; + enabled: number; + definition: string; + definition_version: number; + concurrency_policy: string; + max_run_seconds: number; + dry_run: number; + last_run_at: string | null; + last_run_status: string | null; + created_at: string; + updated_at: string; + channels: number[]; +} + +export interface AutomationRunRow { + id: number; + automation_id: number; + user_id: string; + trigger_type: string; + trigger_context: string | null; + status: string; + started_at: string; + finished_at: string | null; + duration_ms: number | null; + error: string | null; + dry_run: number; + parent_run_id: number | null; + automation_name?: string | null; +} + +export interface AutomationRunStepRow { + id: number; + run_id: number; + step_index: number; + step_id: string; + step_type: string; + status: string; + started_at: string; + finished_at: string | null; + output: string | null; + error: string | null; + truncated: number; +} + +/** The shape the engine loads. */ +export interface AutomationEngineRow { + id: number; + userId: string; + name: string; + enabled: boolean; + definition: string; + concurrencyPolicy: string; + maxRunSeconds: number; + dryRun: boolean; +} + +export interface TriggerStateRow { + automationId: number; + stateKey: string; + breachStartedAt: string | null; + lastFiredAt: string | null; + lastValue: number | null; + lastObservedState: string | null; +} + +export interface DueScheduleRow { + automationId: number; + cron: string | null; + intervalSeconds: number | null; + timezone: string | null; + nextDueAt: string | null; +} + +export type AutomationRepository = Awaited< + ReturnType +>; + +/** + * Automation storage. `ownedChannelIds` answers which of core's notification + * channels the user owns, so a link can never point at another user's. + */ +export async function createAutomationRepository( + db: PluginDatabase, + ownedChannelIds: (userId: string) => Promise, +) { + const automations: Table = await db.define(automationsDef); + const triggerState: Table = await db.define(triggerStateDef); + const schedules: Table = await db.define(schedulesDef); + const runs: Table = await db.define(runsDef); + const runSteps: Table = await db.define(runStepsDef); + const channels: Table = await db.define(channelsDef); + + const client = () => db.client(); + + /** Inserts one row and returns its id. MySQL has no RETURNING. */ + async function insertId( + table: Table, + values: Record, + ): Promise { + const drizzle = await client(); + if (db.dialect === "mysql") { + const result = await drizzle.insert(table).values(values); + const header = Array.isArray(result) ? result[0] : result; + return Number(header?.insertId); + } + const rows = await drizzle + .insert(table) + .values(values) + .returning({ id: table.id }); + return rows[0].id as number; + } + + async function listChannelIds(automationId: number): Promise { + const drizzle = await client(); + const rows: Row[] = await drizzle + .select({ channelId: channels.channelId }) + .from(channels) + .where(eq(channels.automationId, automationId)); + return rows.map((row) => row.channelId as number); + } + + async function replaceChannels( + automationId: number, + userId: string, + channelIds: number[], + ): Promise { + const drizzle = await client(); + await drizzle + .delete(channels) + .where(eq(channels.automationId, automationId)); + if (channelIds.length === 0) return; + + const owned = new Set(await ownedChannelIds(userId)); + const linked = [...new Set(channelIds)].filter((id) => owned.has(id)); + if (linked.length > 0) { + await drizzle + .insert(channels) + .values(linked.map((channelId) => ({ automationId, channelId }))); + } + } + + async function findForUser( + id: number, + userId: string, + ): Promise { + const drizzle = await client(); + const rows: Row[] = await drizzle + .select() + .from(automations) + .where(and(eq(automations.id, id), eq(automations.userId, userId))) + .limit(1); + if (!rows[0]) return null; + return mapAutomationRow(rows[0], await listChannelIds(id)); + } + + async function getTriggerState( + automationId: number, + stateKey: string, + ): Promise { + const drizzle = await client(); + const rows: Row[] = await drizzle + .select() + .from(triggerState) + .where( + and( + eq(triggerState.automationId, automationId), + eq(triggerState.stateKey, stateKey), + ), + ) + .limit(1); + return rows[0] ? mapTriggerStateRow(rows[0]) : null; + } + + return { + async list(userId: string): Promise { + const drizzle = await client(); + const rows: Row[] = await drizzle + .select() + .from(automations) + .where(eq(automations.userId, userId)) + .orderBy(asc(automations.name)); + if (rows.length === 0) return []; + + // One query for every automation's channels rather than one per row. + const links: Row[] = await drizzle + .select({ + automationId: channels.automationId, + channelId: channels.channelId, + }) + .from(channels) + .where( + inArray( + channels.automationId, + rows.map((row) => row.id), + ), + ); + const byAutomation = new Map(); + for (const link of links) { + const list = byAutomation.get(link.automationId) ?? []; + list.push(link.channelId); + byAutomation.set(link.automationId, list); + } + return rows.map((row) => mapAutomationRow(row, byAutomation.get(row.id))); + }, + + findForUser, + + async findById(id: number): Promise { + const drizzle = await client(); + const rows: Row[] = await drizzle + .select() + .from(automations) + .where(eq(automations.id, id)) + .limit(1); + return rows[0] ? mapEngineRow(rows[0]) : null; + }, + + async create(input: { + userId: string; + name: string; + description?: string | null; + enabled?: boolean; + definition: string; + concurrencyPolicy?: string; + maxRunSeconds?: number; + dryRun?: boolean; + channels?: number[]; + }): Promise { + const now = new Date().toISOString(); + const id = await insertId(automations, { + userId: input.userId, + name: input.name, + description: input.description ?? null, + enabled: input.enabled ?? true, + definition: input.definition, + concurrencyPolicy: input.concurrencyPolicy ?? "skip", + maxRunSeconds: input.maxRunSeconds ?? 300, + dryRun: input.dryRun ?? false, + createdAt: now, + updatedAt: now, + }); + await replaceChannels(id, input.userId, input.channels ?? []); + await db.persist(); + const created = await findForUser(id, input.userId); + if (!created) throw new Error("Automation could not be read back"); + return created; + }, + + async update( + id: number, + userId: string, + input: { + name?: string; + description?: string | null; + enabled?: boolean; + definition?: string; + concurrencyPolicy?: string; + maxRunSeconds?: number; + dryRun?: boolean; + channels?: number[]; + }, + ): Promise { + const existing = await findForUser(id, userId); + if (!existing) return null; + + const { channels: channelIds, ...fields } = input; + const drizzle = await client(); + if (Object.keys(fields).length > 0) { + await drizzle + .update(automations) + .set({ ...fields, updatedAt: new Date().toISOString() }) + .where(and(eq(automations.id, id), eq(automations.userId, userId))); + } + if (channelIds) await replaceChannels(id, userId, channelIds); + await db.persist(); + return findForUser(id, userId); + }, + + async delete(id: number, userId: string): Promise { + const existing = await findForUser(id, userId); + if (!existing) return false; + const drizzle = await client(); + await drizzle + .delete(automations) + .where(and(eq(automations.id, id), eq(automations.userId, userId))); + await db.persist(); + return true; + }, + + /** + * Enabled automations owned by one user. Wildcard targets must never + * reach across users. + */ + async listEnabledForUser(userId: string): Promise { + const drizzle = await client(); + const rows: Row[] = await drizzle + .select() + .from(automations) + .where( + and(eq(automations.enabled, true), eq(automations.userId, userId)), + ); + return rows.map(mapEngineRow); + }, + + async listAllEnabled(): Promise { + const drizzle = await client(); + const rows: Row[] = await drizzle + .select() + .from(automations) + .where(eq(automations.enabled, true)); + return rows.map(mapEngineRow); + }, + + getTriggerState, + + async upsertTriggerState(input: { + automationId: number; + stateKey: string; + breachStartedAt?: string | null; + lastFiredAt?: string | null; + lastValue?: number | null; + lastObservedState?: string | null; + }): Promise { + const existing = await getTriggerState( + input.automationId, + input.stateKey, + ); + const updatedAt = new Date().toISOString(); + const drizzle = await client(); + + if (existing) { + const values: Record = { updatedAt }; + if (input.breachStartedAt !== undefined) + values.breachStartedAt = input.breachStartedAt; + if (input.lastFiredAt !== undefined) + values.lastFiredAt = input.lastFiredAt; + if (input.lastValue !== undefined) values.lastValue = input.lastValue; + if (input.lastObservedState !== undefined) + values.lastObservedState = input.lastObservedState; + await drizzle + .update(triggerState) + .set(values) + .where( + and( + eq(triggerState.automationId, input.automationId), + eq(triggerState.stateKey, input.stateKey), + ), + ); + } else { + await drizzle.insert(triggerState).values({ + automationId: input.automationId, + stateKey: input.stateKey, + breachStartedAt: input.breachStartedAt ?? null, + lastFiredAt: input.lastFiredAt ?? null, + lastValue: input.lastValue ?? null, + lastObservedState: input.lastObservedState ?? null, + updatedAt, + }); + } + await db.persist({ lazy: true }); + }, + + async clearBreach(automationId: number, stateKey: string): Promise { + const drizzle = await client(); + await drizzle + .update(triggerState) + .set({ breachStartedAt: null, updatedAt: new Date().toISOString() }) + .where( + and( + eq(triggerState.automationId, automationId), + eq(triggerState.stateKey, stateKey), + ), + ); + await db.persist({ lazy: true }); + }, + + /** Dwell windows the scheduler has to re-check without a fresh sample. */ + async listOpenBreaches(): Promise { + const drizzle = await client(); + const rows: Row[] = await drizzle + .select() + .from(triggerState) + .where(sql`${triggerState.breachStartedAt} IS NOT NULL`); + return rows.map(mapTriggerStateRow); + }, + + async upsertSchedule(input: { + automationId: number; + cron: string | null; + intervalSeconds: number | null; + timezone: string | null; + nextDueAt: string | null; + }): Promise { + const drizzle = await client(); + const existing: Row[] = await drizzle + .select({ id: schedules.id }) + .from(schedules) + .where(eq(schedules.automationId, input.automationId)) + .limit(1); + if (existing[0]) { + await drizzle + .update(schedules) + .set({ + cron: input.cron, + intervalSeconds: input.intervalSeconds, + timezone: input.timezone, + nextDueAt: input.nextDueAt, + }) + .where(eq(schedules.automationId, input.automationId)); + } else { + await drizzle.insert(schedules).values(input); + } + await db.persist(); + }, + + async deleteSchedule(automationId: number): Promise { + const drizzle = await client(); + await drizzle + .delete(schedules) + .where(eq(schedules.automationId, automationId)); + await db.persist(); + }, + + /** Schedules due at or before `now`, joined to their enabled automation. */ + async listDueSchedules(now: string): Promise { + const drizzle = await client(); + return drizzle + .select({ + automationId: schedules.automationId, + cron: schedules.cron, + intervalSeconds: schedules.intervalSeconds, + timezone: schedules.timezone, + nextDueAt: schedules.nextDueAt, + }) + .from(schedules) + .innerJoin(automations, eq(automations.id, schedules.automationId)) + .where( + and(eq(automations.enabled, true), lte(schedules.nextDueAt, now)), + ); + }, + + async markScheduleTicked( + automationId: number, + nextDueAt: string | null, + lastTickAt: string, + ): Promise { + const drizzle = await client(); + await drizzle + .update(schedules) + .set({ nextDueAt, lastTickAt }) + .where(eq(schedules.automationId, automationId)); + await db.persist({ lazy: true }); + }, + + async createRun(input: { + automationId: number; + userId: string; + triggerType: string; + triggerContext?: string | null; + status: string; + dryRun?: boolean; + parentRunId?: number | null; + }): Promise<{ id: number }> { + const id = await insertId(runs, { + automationId: input.automationId, + userId: input.userId, + triggerType: input.triggerType, + triggerContext: input.triggerContext ?? null, + status: input.status, + startedAt: new Date().toISOString(), + dryRun: input.dryRun ?? false, + parentRunId: input.parentRunId ?? null, + }); + await db.persist({ lazy: true }); + return { id }; + }, + + async finishRun( + runId: number, + input: { + status: string; + error?: string | null; + durationMs?: number | null; + }, + ): Promise { + const drizzle = await client(); + await drizzle + .update(runs) + .set({ + status: input.status, + error: input.error ?? null, + finishedAt: new Date().toISOString(), + durationMs: input.durationMs ?? null, + }) + .where(eq(runs.id, runId)); + + const run: Row[] = await drizzle + .select({ automationId: runs.automationId, startedAt: runs.startedAt }) + .from(runs) + .where(eq(runs.id, runId)) + .limit(1); + if (run[0]) { + await drizzle + .update(automations) + .set({ lastRunAt: run[0].startedAt, lastRunStatus: input.status }) + .where(eq(automations.id, run[0].automationId)); + } + await db.persist(); + }, + + async listRuns( + userId: string, + options: { automationId?: number; limit?: number; offset?: number } = {}, + ): Promise { + const limit = Math.min(Math.max(options.limit ?? 50, 1), 200); + const offset = Math.max(options.offset ?? 0, 0); + const where = options.automationId + ? and( + eq(runs.userId, userId), + eq(runs.automationId, options.automationId), + ) + : eq(runs.userId, userId); + + const drizzle = await client(); + const rows: Row[] = await drizzle + .select({ run: runs, automationName: automations.name }) + .from(runs) + .leftJoin(automations, eq(automations.id, runs.automationId)) + .where(where) + .orderBy(desc(runs.startedAt), desc(runs.id)) + .limit(limit) + .offset(offset); + return rows.map((row) => ({ + ...mapRunRow(row.run), + automation_name: row.automationName ?? null, + })); + }, + + async findRunForUser( + runId: number, + userId: string, + ): Promise { + const drizzle = await client(); + const rows: Row[] = await drizzle + .select() + .from(runs) + .where(and(eq(runs.id, runId), eq(runs.userId, userId))) + .limit(1); + return rows[0] ? mapRunRow(rows[0]) : null; + }, + + async createRunStep(input: { + runId: number; + stepIndex: number; + stepId: string; + stepType: string; + status: string; + }): Promise { + const id = await insertId(runSteps, { + ...input, + startedAt: new Date().toISOString(), + }); + await db.persist({ lazy: true }); + return id; + }, + + async finishRunStep( + stepRowId: number, + input: { + status: string; + output?: string | null; + error?: string | null; + truncated?: boolean; + }, + ): Promise { + const drizzle = await client(); + await drizzle + .update(runSteps) + .set({ + status: input.status, + output: input.output ?? null, + error: input.error ?? null, + truncated: input.truncated ?? false, + finishedAt: new Date().toISOString(), + }) + .where(eq(runSteps.id, stepRowId)); + await db.persist({ lazy: true }); + }, + + async listRunSteps(runId: number): Promise { + const drizzle = await client(); + const rows: Row[] = await drizzle + .select() + .from(runSteps) + .where(eq(runSteps.runId, runId)) + .orderBy(asc(runSteps.stepIndex)); + return rows.map(mapRunStepRow); + }, + + /** Trims run history. Called from the scheduler's daily sweep. */ + async pruneRunsOlderThan(days: number): Promise { + const cutoff = new Date(Date.now() - days * 86400000).toISOString(); + const drizzle = await client(); + const old: Row[] = await drizzle + .select({ id: runs.id }) + .from(runs) + .where(lt(runs.startedAt, cutoff)); + if (old.length === 0) return 0; + await drizzle.delete(runs).where(lt(runs.startedAt, cutoff)); + await db.persist(); + return old.length; + }, + + /** Marks runs left behind by a crash so they do not block concurrency. */ + async failStaleRunningRuns(olderThanIso: string): Promise { + const where = and( + eq(runs.status, "running"), + lt(runs.startedAt, olderThanIso), + ); + const drizzle = await client(); + const stale: Row[] = await drizzle + .select({ id: runs.id }) + .from(runs) + .where(where); + if (stale.length === 0) return 0; + await drizzle + .update(runs) + .set({ + status: "failed", + error: "Interrupted by a server restart", + finishedAt: new Date().toISOString(), + }) + .where(where); + await db.persist(); + return stale.length; + }, + }; +} + +function mapAutomationRow(row: Row, channelIds: number[] = []): AutomationRow { + return { + id: row.id, + user_id: row.userId, + name: row.name, + description: row.description ?? null, + enabled: row.enabled ? 1 : 0, + definition: row.definition, + definition_version: row.definitionVersion, + concurrency_policy: row.concurrencyPolicy, + max_run_seconds: row.maxRunSeconds, + dry_run: row.dryRun ? 1 : 0, + last_run_at: row.lastRunAt ?? null, + last_run_status: row.lastRunStatus ?? null, + created_at: row.createdAt, + updated_at: row.updatedAt, + channels: channelIds, + }; +} + +function mapEngineRow(row: Row): AutomationEngineRow { + return { + id: row.id, + userId: row.userId, + name: row.name, + enabled: !!row.enabled, + definition: row.definition, + concurrencyPolicy: row.concurrencyPolicy, + maxRunSeconds: row.maxRunSeconds, + dryRun: !!row.dryRun, + }; +} + +function mapTriggerStateRow(row: Row): TriggerStateRow { + return { + automationId: row.automationId, + stateKey: row.stateKey, + breachStartedAt: row.breachStartedAt ?? null, + lastFiredAt: row.lastFiredAt ?? null, + lastValue: row.lastValue ?? null, + lastObservedState: row.lastObservedState ?? null, + }; +} + +function mapRunRow(row: Row): AutomationRunRow { + return { + id: row.id, + automation_id: row.automationId, + user_id: row.userId, + trigger_type: row.triggerType, + trigger_context: row.triggerContext ?? null, + status: row.status, + started_at: row.startedAt, + finished_at: row.finishedAt ?? null, + duration_ms: row.durationMs ?? null, + error: row.error ?? null, + dry_run: row.dryRun ? 1 : 0, + parent_run_id: row.parentRunId ?? null, + }; +} + +function mapRunStepRow(row: Row): AutomationRunStepRow { + return { + id: row.id, + run_id: row.runId, + step_index: row.stepIndex, + step_id: row.stepId, + step_type: row.stepType, + status: row.status, + started_at: row.startedAt, + finished_at: row.finishedAt ?? null, + output: row.output ?? null, + error: row.error ?? null, + truncated: row.truncated ? 1 : 0, + }; +} diff --git a/plugins/automations/src/backend/requirements.ts b/plugins/automations/src/backend/requirements.ts new file mode 100644 index 000000000..2d1abde83 --- /dev/null +++ b/plugins/automations/src/backend/requirements.ts @@ -0,0 +1,93 @@ +import type { AutomationDefinition, HostSelector, Step } from "../types.js"; +import { pluginOf, type Deps, type ProviderKey } from "./deps.js"; + +/** + * The providers a definition cannot run without. A trigger or step whose + * plugin is off makes the whole automation wait for it, rather than half of + * it running. + */ +export function requiredProviders( + definition: AutomationDefinition | null | undefined, +): ProviderKey[] { + const needed = new Set(); + if (!definition) return []; + + const selector = (value: HostSelector | undefined) => { + if (value?.kind === "fleet") needed.add("fleets"); + }; + + const trigger = definition.trigger; + switch (trigger?.kind) { + case "metric_threshold": + case "health_check": + needed.add("host-metrics"); + selector(trigger.hostSelector); + break; + case "docker_event": + needed.add("docker-events"); + selector(trigger.hostSelector); + break; + case "host_status": + selector(trigger.hostSelector); + break; + case "internal_event": + if (trigger.event === "tunnel_disconnected") needed.add("tunnels"); + selector(trigger.hostSelector); + break; + default: + break; + } + + const walk = (steps: Step[] | undefined) => { + for (const step of steps ?? []) { + if (step.enabled === false) continue; + switch (step.type) { + case "run_snippet": + needed.add("snippets"); + selector(step.hostSelector); + break; + case "run_command": + selector(step.hostSelector); + break; + case "docker": + needed.add("docker"); + selector(step.hostSelector); + break; + case "tunnel": + needed.add("tunnels"); + break; + case "wol": + needed.add("wake-on-lan"); + break; + case "if": + walk(step.then); + walk(step.else); + break; + default: + break; + } + } + }; + walk(definition.steps); + + return [...needed]; +} + +/** Plugin ids the definition needs that are not running, deduplicated. */ +export function missingPlugins( + definition: AutomationDefinition | null | undefined, + deps: Pick, +): string[] { + const missing = requiredProviders(definition) + .filter((key) => !deps.available(key)) + .map(pluginOf); + return [...new Set(missing)]; +} + +export function parseDefinition(raw: string): AutomationDefinition | null { + try { + return JSON.parse(raw) as AutomationDefinition; + } catch { + return null; + } +} diff --git a/plugins/automations/src/backend/routes.ts b/plugins/automations/src/backend/routes.ts index 9c74d6897..99ddc1041 100644 --- a/plugins/automations/src/backend/routes.ts +++ b/plugins/automations/src/backend/routes.ts @@ -1,216 +1,65 @@ import crypto from "node:crypto"; -import express, { type Request, type Response, type Router } from "express"; -import type { AuthenticatedRequest } from "../../../../src/types/index.js"; -import type { - AutomationDefinition, - Step, - Trigger, -} from "../../../../src/types/automations.js"; -import { AUTOMATION_DEFINITION_VERSION } from "../../../../src/types/automations.js"; -import { PermissionManager } from "../../../../src/backend/utils/permission-manager.js"; -import { AuthManager } from "../../../../src/backend/utils/auth-manager.js"; -import { databaseLogger } from "../../../../src/backend/utils/logger.js"; -import { - getAuditUsername, - getRequestMeta, - logAudit, -} from "../../../../src/backend/utils/audit-logger.js"; -import { createCurrentAutomationRepository } from "../../../../src/backend/database/repositories/factory.js"; -import type { AutomationRow } from "../../../../src/backend/database/repositories/automation-repository.js"; -import { AutomationEngine } from "./engine.js"; -import { computeNextDueAt, isValidCron, isValidTimezone } from "./cron.js"; +import type { Request, Response, Router } from "express"; +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import type { AutomationDefinition } from "../types.js"; +import { computeNextDueAt } from "./cron.js"; +import type { Deps } from "./deps.js"; +import type { AutomationEngine } from "./engine.js"; +import type { AutomationRepository, AutomationRow } from "./repository.js"; +import { missingPlugins, parseDefinition } from "./requirements.js"; +import { isNonEmptyString, validateDefinition } from "./validate.js"; -export const router = express.Router(); - -const authManager = AuthManager.getInstance(); -const permissionManager = PermissionManager.getInstance(); -const authenticateJWT = authManager.createAuthMiddleware(); -const requireDataAccess = authManager.createDataAccessMiddleware(); - -const TRIGGER_KINDS = new Set([ - "metric_threshold", - "host_status", - "health_check", - "schedule", - "docker_event", - "internal_event", - "webhook", -]); - -const STEP_TYPES = new Set([ - "notify", - "http", - "run_snippet", - "run_command", - "docker", - "tunnel", - "wol", - "wait", - "set_var", - "if", - "run_automation", - "stop", -]); - -const OPERATORS = new Set([ - ">", - "<", - ">=", - "<=", - "==", - "!=", - "contains", - "not_contains", - "changed", -]); +export interface RouteDeps { + ctx: PluginContext; + repository: AutomationRepository; + engine: Pick; + deps: Deps; +} function parseId(raw: unknown): number | null { const id = typeof raw === "string" ? parseInt(raw, 10) : NaN; return Number.isInteger(id) && id > 0 ? id : null; } -function isNonEmptyString(value: unknown): value is string { - return typeof value === "string" && value.trim().length > 0; +export function hashToken(token: string): string { + return crypto.createHash("sha256").update(token).digest("hex"); +} + +function timingSafeEqual(a: string, b: string): boolean { + const left = Buffer.from(a || "", "utf8"); + const right = Buffer.from(b || "", "utf8"); + if (left.length !== right.length) return false; + return crypto.timingSafeEqual(left, right); } /** - * Validates a definition before it is stored. The engine treats the stored - * blob as trusted, so everything it relies on is checked once here. + * The wire shape of an automation: the definition parsed, the webhook token + * hash blanked, and the plugins it needs that are not running. */ -export function validateDefinition(value: unknown): { - ok: boolean; - error?: string; - definition?: AutomationDefinition; -} { - if (typeof value !== "object" || value === null) { - return { ok: false, error: "Definition must be an object" }; - } - - const candidate = value as Partial; - const trigger = candidate.trigger as Trigger | undefined; - - if (!trigger || !TRIGGER_KINDS.has(trigger.kind)) { - return { ok: false, error: "Unknown or missing trigger kind" }; - } - - if (trigger.kind === "metric_threshold") { - if (!OPERATORS.has(trigger.operator)) { - return { ok: false, error: "Unknown comparison operator" }; - } - if (typeof trigger.value !== "number" || !Number.isFinite(trigger.value)) { - return { ok: false, error: "Threshold value must be a number" }; - } - if (!trigger.metric?.path) { - return { ok: false, error: "Trigger is missing a metric" }; - } - } - - if (trigger.kind === "schedule") { - const hasInterval = - typeof trigger.intervalSeconds === "number" && - trigger.intervalSeconds > 0; - const hasCron = isNonEmptyString(trigger.cron); - if (!hasInterval && !hasCron) { - return { ok: false, error: "Schedule needs an interval or a cron" }; - } - if (hasCron && !isValidCron(trigger.cron as string)) { - return { ok: false, error: "Cron expression is not valid" }; - } - if (hasInterval && (trigger.intervalSeconds as number) < 60) { - return { ok: false, error: "Interval must be at least 60 seconds" }; - } - if ( - isNonEmptyString(trigger.timezone) && - !isValidTimezone(trigger.timezone) - ) { - return { ok: false, error: "Time zone is not valid" }; - } - } - - const steps = candidate.steps; - if (!Array.isArray(steps)) { - return { ok: false, error: "Definition must include a steps array" }; - } - - const seen = new Set(); - const stepError = validateSteps(steps as Step[], seen); - if (stepError) return { ok: false, error: stepError }; - - return { - ok: true, - definition: { - version: candidate.version ?? AUTOMATION_DEFINITION_VERSION, - trigger, - steps: steps as Step[], - }, - }; -} - -function validateSteps(steps: Step[], seen: Set): string | null { - for (const step of steps) { - if (!step || typeof step !== "object") return "Step must be an object"; - if (!isNonEmptyString(step.id)) return "Every step needs an id"; - if (seen.has(step.id)) return `Duplicate step id: ${step.id}`; - seen.add(step.id); - if (!STEP_TYPES.has(step.type)) { - return `Unknown step type: ${step.type}`; - } - - if (step.type === "if") { - if (!step.condition || !OPERATORS.has(step.condition.operator)) { - return "Condition needs a valid operator"; - } - const thenError = validateSteps(step.then ?? [], seen); - if (thenError) return thenError; - const elseError = validateSteps(step.else ?? [], seen); - if (elseError) return elseError; - } - - if (step.type === "http" && !isNonEmptyString(step.url)) { - return "HTTP steps need a URL"; - } - if (step.type === "run_command" && !isNonEmptyString(step.command)) { - return "Command steps need a command"; - } - if (step.type === "wait" && typeof step.seconds !== "number") { - return "Wait steps need a number of seconds"; - } - if (step.type === "set_var" && !isNonEmptyString(step.name)) { - return "Variable steps need a name"; - } - } - return null; -} - -/** Never leak a webhook token hash to the client. */ -function serialize(row: AutomationRow) { - let definition: AutomationDefinition | null = null; - try { - definition = JSON.parse(row.definition) as AutomationDefinition; - } catch { - definition = null; - } - +export function serializeAutomation(row: AutomationRow, deps: Deps) { + let definition: AutomationDefinition | null = parseDefinition(row.definition); if (definition?.trigger?.kind === "webhook") { definition = { ...definition, trigger: { ...definition.trigger, tokenHash: "" }, }; } - - return { ...row, definition }; + return { + ...row, + definition, + missingPlugins: missingPlugins(definition, deps), + }; } -async function syncSchedule( +export async function syncSchedule( + repository: AutomationRepository, automationId: number, definition: AutomationDefinition, ): Promise { - const repository = createCurrentAutomationRepository(); if (definition.trigger?.kind !== "schedule") { await repository.deleteSchedule(automationId); return; } - const trigger = definition.trigger; await repository.upsertSchedule({ automationId, @@ -225,604 +74,545 @@ async function syncSchedule( }); } -/** - * @openapi - * /automations: - * get: - * summary: List the current user's automations - * description: Returns every automation the caller owns, with its parsed definition and linked notification channels. - * tags: - * - Automations - * responses: - * 200: - * description: List of automations. - * 403: - * description: Missing the automations.view permission. - */ -router.get( - "/", - authenticateJWT, - permissionManager.requirePermission("automations.view"), - requireDataAccess, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId; - try { - const rows = await createCurrentAutomationRepository().list(userId); - res.json(rows.map(serialize)); - } catch (error) { - databaseLogger.error("Failed to list automations", error, { - operation: "automation_list_error", - userId, - }); - res.status(500).json({ error: "Failed to list automations" }); - } - }, -); +export function registerRoutes( + router: Router, + { ctx, repository, engine, deps }: RouteDeps, +): void { + const actor = () => ctx.currentActor() as string; + const fail = (res: Response, message: string, error: unknown) => { + ctx.log.error( + message, + error instanceof Error ? error : new Error(String(error)), + ); + res.status(500).json({ error: message }); + }; + const audit = ( + action: string, + id: number, + name: string | undefined, + success = true, + details?: string, + ) => + ctx.audit.record({ + action, + resourceType: "automation", + resourceId: String(id), + resourceName: name, + details, + success, + }); -/** - * @openapi - * /automations/{id}: - * get: - * summary: Fetch a single automation - * tags: - * - Automations - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: The automation. - * 404: - * description: Automation not found. - */ -router.get( - "/:id", - authenticateJWT, - permissionManager.requirePermission("automations.view"), - requireDataAccess, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId; - const id = parseId(req.params.id); - if (id === null) return res.status(400).json({ error: "Invalid id" }); + /** + * @openapi + * /plugin-api/automations/webhook/{token}: + * post: + * summary: Trigger an automation from an external system + * description: Unauthenticated by design; the 32-byte token in the path is the credential and is compared against a stored hash in constant time. + * tags: + * - Automations + * parameters: + * - in: path + * name: token + * required: true + * schema: + * type: string + * responses: + * 202: + * description: The run was accepted. + * 404: + * description: No automation matches that token. + */ + router.post("/webhook/:token", async (req: Request, res: Response) => { + const token = req.params.token; + if (!isNonEmptyString(token) || token.length < 32) { + return res.status(404).json({ error: "Not found" }); + } try { - const row = await createCurrentAutomationRepository().findForUser( - id, - userId, - ); - if (!row) return res.status(404).json({ error: "Automation not found" }); - res.json(serialize(row)); - } catch (error) { - databaseLogger.error("Failed to fetch automation", error, { - operation: "automation_get_error", - userId, + const wanted = hashToken(token); + const match = (await repository.listAllEnabled()).find((row) => { + const definition = parseDefinition(row.definition); + if (definition?.trigger?.kind !== "webhook") return false; + return timingSafeEqual(definition.trigger.tokenHash, wanted); }); - res.status(500).json({ error: "Failed to fetch automation" }); - } - }, -); + if (!match) return res.status(404).json({ error: "Not found" }); -/** - * @openapi - * /automations: - * post: - * summary: Create an automation - * description: Validates the trigger and every step before storing the definition. A schedule trigger also registers its next due time. - * tags: - * - Automations - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * name: - * type: string - * definition: - * type: object - * responses: - * 201: - * description: The created automation. - * 400: - * description: Validation failed. - */ -router.post( - "/", - authenticateJWT, - permissionManager.requirePermission("automations.create"), - requireDataAccess, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId; - const { name, description, enabled, definition, concurrencyPolicy } = - req.body ?? {}; - - if (!isNonEmptyString(name)) { - return res.status(400).json({ error: "Name is required" }); - } - - const validated = validateDefinition(definition); - if (!validated.ok || !validated.definition) { - return res.status(400).json({ error: validated.error }); - } - - // A webhook trigger's token is shown once here and only stored hashed. - let webhookToken: string | undefined; - if (validated.definition.trigger.kind === "webhook") { - webhookToken = crypto.randomBytes(32).toString("hex"); - validated.definition = { - ...validated.definition, - trigger: { - kind: "webhook", - tokenHash: hashToken(webhookToken), + const outcome = await engine.run({ + automationId: match.id, + triggerType: "webhook", + triggerContext: { + body: req.body ?? {}, + receivedAt: new Date().toISOString(), }, - }; - } - - try { - const repository = createCurrentAutomationRepository(); - const created = await repository.create({ - userId, - name: name.trim(), - description: description ?? null, - enabled: enabled !== false, - definition: JSON.stringify(validated.definition), - concurrencyPolicy, - channels: Array.isArray(req.body?.channels) ? req.body.channels : [], }); - - await syncSchedule(created.id, validated.definition); - - const { ipAddress, userAgent } = getRequestMeta(req); - await logAudit({ - userId, - username: await getAuditUsername(userId), - action: "create_automation", - resourceType: "automation", - resourceId: String(created.id), - resourceName: created.name, - ipAddress, - userAgent, - success: true, - }); - - res.status(201).json({ ...serialize(created), webhookToken }); + res.status(202).json({ runId: outcome.runId, status: outcome.status }); } catch (error) { - databaseLogger.error("Failed to create automation", error, { - operation: "automation_create_error", - userId, - }); - res.status(500).json({ error: "Failed to create automation" }); + fail(res, "Failed to run automation", error); } - }, -); + }); -/** - * @openapi - * /automations/{id}: - * put: - * summary: Update an automation - * tags: - * - Automations - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: The updated automation. - * 400: - * description: Validation failed. - * 404: - * description: Automation not found. - */ -router.put( - "/:id", - authenticateJWT, - permissionManager.requirePermission("automations.edit"), - requireDataAccess, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId; - const id = parseId(req.params.id); - if (id === null) return res.status(400).json({ error: "Invalid id" }); - - const repository = createCurrentAutomationRepository(); - const existing = await repository.findForUser(id, userId); - if (!existing) { - return res.status(404).json({ error: "Automation not found" }); - } - - const update: Record = {}; - if (req.body?.name !== undefined) { - if (!isNonEmptyString(req.body.name)) { - return res.status(400).json({ error: "Name cannot be empty" }); + /** + * @openapi + * /plugin-api/automations: + * get: + * summary: List the current user's automations + * description: Returns every automation the caller owns, with its parsed definition, linked notification channels and the plugins it needs that are not running. + * tags: + * - Automations + * responses: + * 200: + * description: List of automations. + * 403: + * description: Missing the automations.view permission. + */ + router.get( + "/", + ctx.rbac.require("view") as never, + async (_req: Request, res: Response) => { + try { + const rows = await repository.list(actor()); + res.json(rows.map((row) => serializeAutomation(row, deps))); + } catch (error) { + fail(res, "Failed to list automations", error); } - update.name = req.body.name.trim(); - } - if (req.body?.description !== undefined) { - update.description = req.body.description; - } - if (req.body?.enabled !== undefined) update.enabled = !!req.body.enabled; - if (req.body?.concurrencyPolicy !== undefined) { - update.concurrencyPolicy = req.body.concurrencyPolicy; - } - if (Array.isArray(req.body?.channels)) update.channels = req.body.channels; + }, + ); - let parsedDefinition: AutomationDefinition | null = null; - if (req.body?.definition !== undefined) { - const validated = validateDefinition(req.body.definition); + /** + * @openapi + * /plugin-api/automations/editor-options: + * get: + * summary: What the automation editor can offer + * description: The caller's hosts, snippets, fleets and notification channels, and which optional plugins are running, so the editor only offers triggers and steps it can run. + * tags: + * - Automations + * responses: + * 200: + * description: Lookup lists and the running providers. + * 403: + * description: Missing the automations.view permission. + */ + router.get( + "/editor-options", + ctx.rbac.require("view") as never, + async (_req: Request, res: Response) => { + const userId = actor(); + const settle = async (fn: () => Promise, fallback: T) => { + try { + return await fn(); + } catch { + return fallback; + } + }; + const snippets = deps.snippets(); + const fleets = deps.fleets(); + + const [hosts, snippetList, fleetList, channels] = await Promise.all([ + settle(() => ctx.hosts.list(), []), + settle( + async () => + typeof snippets.list === "function" ? snippets.list() : [], + [], + ), + settle( + async () => (typeof fleets.list === "function" ? fleets.list() : []), + [], + ), + settle(() => ctx.notify.channels(), []), + ]); + + res.json({ + hosts: hosts + .filter((host) => host.userId === userId) + .map((host) => ({ id: host.id, name: host.name || host.ip })), + snippets: snippetList + .filter((snippet) => !snippet.isNote) + .map((snippet) => ({ id: snippet.id, name: snippet.name })), + fleets: fleetList.map((fleet) => ({ id: fleet.id, name: fleet.name })), + channels: channels.map((channel) => ({ + id: channel.id, + name: channel.name, + })), + providers: deps.providers(), + }); + }, + ); + + /** + * @openapi + * /plugin-api/automations/runs/history: + * get: + * summary: List automation runs + * tags: + * - Automations + * parameters: + * - in: query + * name: automationId + * schema: + * type: integer + * - in: query + * name: limit + * schema: + * type: integer + * - in: query + * name: offset + * schema: + * type: integer + * responses: + * 200: + * description: Recent runs, newest first. + */ + router.get( + "/runs/history", + ctx.rbac.require("view") as never, + async (req: Request, res: Response) => { + try { + res.json( + await repository.listRuns(actor(), { + automationId: parseId(req.query.automationId) ?? undefined, + limit: Number(req.query.limit) || 50, + offset: Number(req.query.offset) || 0, + }), + ); + } catch (error) { + fail(res, "Failed to list runs", error); + } + }, + ); + + /** + * @openapi + * /plugin-api/automations/runs/{runId}/steps: + * get: + * summary: Step-by-step results for a run + * tags: + * - Automations + * parameters: + * - in: path + * name: runId + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: The run's steps in order. + * 404: + * description: Run not found. + */ + router.get( + "/runs/:runId/steps", + ctx.rbac.require("view") as never, + async (req: Request, res: Response) => { + const runId = parseId(req.params.runId); + if (runId === null) return res.status(400).json({ error: "Invalid id" }); + try { + const run = await repository.findRunForUser(runId, actor()); + if (!run) return res.status(404).json({ error: "Run not found" }); + res.json(await repository.listRunSteps(runId)); + } catch (error) { + fail(res, "Failed to list run steps", error); + } + }, + ); + + /** + * @openapi + * /plugin-api/automations/{id}: + * get: + * summary: Fetch a single automation + * tags: + * - Automations + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: The automation. + * 404: + * description: Automation not found. + */ + router.get( + "/:id", + ctx.rbac.require("view") as never, + async (req: Request, res: Response) => { + const id = parseId(req.params.id); + if (id === null) return res.status(400).json({ error: "Invalid id" }); + try { + const row = await repository.findForUser(id, actor()); + if (!row) + return res.status(404).json({ error: "Automation not found" }); + res.json(serializeAutomation(row, deps)); + } catch (error) { + fail(res, "Failed to fetch automation", error); + } + }, + ); + + /** + * @openapi + * /plugin-api/automations: + * post: + * summary: Create an automation + * description: Validates the trigger and every step before storing the definition. A schedule trigger also registers its next due time, and a webhook trigger returns its token once. + * tags: + * - Automations + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * definition: + * type: object + * responses: + * 201: + * description: The created automation. + * 400: + * description: Validation failed. + */ + router.post( + "/", + ctx.rbac.require("create") as never, + async (req: Request, res: Response) => { + const userId = actor(); + const { name, description, enabled, definition, concurrencyPolicy } = + req.body ?? {}; + + if (!isNonEmptyString(name)) { + return res.status(400).json({ error: "Name is required" }); + } + const validated = validateDefinition(definition); if (!validated.ok || !validated.definition) { return res.status(400).json({ error: validated.error }); } - // Keep the stored token hash: the raw token is only ever shown once. + // A webhook trigger's token is shown once here and only stored hashed. + let webhookToken: string | undefined; if (validated.definition.trigger.kind === "webhook") { - const previous = JSON.parse( - existing.definition, - ) as AutomationDefinition; - const previousHash = - previous.trigger?.kind === "webhook" - ? previous.trigger.tokenHash - : ""; + webhookToken = crypto.randomBytes(32).toString("hex"); validated.definition = { ...validated.definition, - trigger: { kind: "webhook", tokenHash: previousHash }, + trigger: { kind: "webhook", tokenHash: hashToken(webhookToken) }, }; } - parsedDefinition = validated.definition; - update.definition = JSON.stringify(validated.definition); - } - - try { - const updated = await repository.update(id, userId, update); - if (!updated) { - return res.status(404).json({ error: "Automation not found" }); - } - - if (parsedDefinition) await syncSchedule(id, parsedDefinition); - - const { ipAddress, userAgent } = getRequestMeta(req); - await logAudit({ - userId, - username: await getAuditUsername(userId), - action: "update_automation", - resourceType: "automation", - resourceId: String(id), - resourceName: updated.name, - ipAddress, - userAgent, - success: true, - }); - - res.json(serialize(updated)); - } catch (error) { - databaseLogger.error("Failed to update automation", error, { - operation: "automation_update_error", - userId, - }); - res.status(500).json({ error: "Failed to update automation" }); - } - }, -); - -/** - * @openapi - * /automations/{id}: - * delete: - * summary: Delete an automation - * tags: - * - Automations - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Deleted. - * 404: - * description: Automation not found. - */ -router.delete( - "/:id", - authenticateJWT, - permissionManager.requirePermission("automations.delete"), - requireDataAccess, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId; - const id = parseId(req.params.id); - if (id === null) return res.status(400).json({ error: "Invalid id" }); - - try { - const deleted = await createCurrentAutomationRepository().delete( - id, - userId, - ); - if (!deleted) { - return res.status(404).json({ error: "Automation not found" }); - } - - const { ipAddress, userAgent } = getRequestMeta(req); - await logAudit({ - userId, - username: await getAuditUsername(userId), - action: "delete_automation", - resourceType: "automation", - resourceId: String(id), - ipAddress, - userAgent, - success: true, - }); - - res.json({ success: true }); - } catch (error) { - databaseLogger.error("Failed to delete automation", error, { - operation: "automation_delete_error", - userId, - }); - res.status(500).json({ error: "Failed to delete automation" }); - } - }, -); - -/** - * @openapi - * /automations/{id}/run: - * post: - * summary: Run an automation now - * description: Runs immediately, as the automation's owner. Pass dryRun to record what each step would do without touching anything outside Termix. - * tags: - * - Automations - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * requestBody: - * content: - * application/json: - * schema: - * type: object - * properties: - * dryRun: - * type: boolean - * responses: - * 200: - * description: The run outcome. - * 404: - * description: Automation not found. - */ -router.post( - "/:id/run", - authenticateJWT, - permissionManager.requirePermission("automations.run"), - requireDataAccess, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId; - const id = parseId(req.params.id); - if (id === null) return res.status(400).json({ error: "Invalid id" }); - - const existing = await createCurrentAutomationRepository().findForUser( - id, - userId, - ); - if (!existing) { - return res.status(404).json({ error: "Automation not found" }); - } - - try { - const outcome = await AutomationEngine.getInstance().run({ - automationId: id, - triggerType: "manual", - triggerContext: { manual: true, requestedBy: userId }, - dryRun: req.body?.dryRun === true, - }); - - const { ipAddress, userAgent } = getRequestMeta(req); - await logAudit({ - userId, - username: await getAuditUsername(userId), - action: "run_automation", - resourceType: "automation", - resourceId: String(id), - resourceName: existing.name, - details: JSON.stringify({ - status: outcome.status, - dryRun: req.body?.dryRun === true, - }), - ipAddress, - userAgent, - success: outcome.status === "success", - }); - - res.json(outcome); - } catch (error) { - databaseLogger.error("Failed to run automation", error, { - operation: "automation_run_error", - userId, - }); - res.status(500).json({ error: "Failed to run automation" }); - } - }, -); - -/** - * @openapi - * /automations/runs: - * get: - * summary: List automation runs - * tags: - * - Automations - * parameters: - * - in: query - * name: automationId - * schema: - * type: integer - * - in: query - * name: limit - * schema: - * type: integer - * responses: - * 200: - * description: Recent runs, newest first. - */ -router.get( - "/runs/history", - authenticateJWT, - permissionManager.requirePermission("automations.view"), - requireDataAccess, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId; - try { - const runs = await createCurrentAutomationRepository().listRuns(userId, { - automationId: parseId(req.query.automationId) ?? undefined, - limit: Number(req.query.limit) || 50, - offset: Number(req.query.offset) || 0, - }); - res.json(runs); - } catch (error) { - databaseLogger.error("Failed to list automation runs", error, { - operation: "automation_runs_error", - userId, - }); - res.status(500).json({ error: "Failed to list runs" }); - } - }, -); - -/** - * @openapi - * /automations/runs/{runId}/steps: - * get: - * summary: Step-by-step results for a run - * tags: - * - Automations - * parameters: - * - in: path - * name: runId - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: The run's steps in order. - * 404: - * description: Run not found. - */ -router.get( - "/runs/:runId/steps", - authenticateJWT, - permissionManager.requirePermission("automations.view"), - requireDataAccess, - async (req: Request, res: Response) => { - const userId = (req as AuthenticatedRequest).userId; - const runId = parseId(req.params.runId); - if (runId === null) return res.status(400).json({ error: "Invalid id" }); - - try { - const repository = createCurrentAutomationRepository(); - const run = await repository.findRunForUser(runId, userId); - if (!run) return res.status(404).json({ error: "Run not found" }); - - res.json(await repository.listRunSteps(runId)); - } catch (error) { - databaseLogger.error("Failed to list run steps", error, { - operation: "automation_run_steps_error", - userId, - }); - res.status(500).json({ error: "Failed to list run steps" }); - } - }, -); - -/** - * @openapi - * /automations/webhook/{token}: - * post: - * summary: Trigger an automation from an external system - * description: Unauthenticated by design; the 32-byte token in the path is the credential and is compared against a stored hash in constant time. - * tags: - * - Automations - * parameters: - * - in: path - * name: token - * required: true - * schema: - * type: string - * responses: - * 202: - * description: The run was accepted. - * 404: - * description: No automation matches that token. - */ -router.post("/webhook/:token", async (req: Request, res: Response) => { - const token = req.params.token; - if (!isNonEmptyString(token) || token.length < 32) { - return res.status(404).json({ error: "Not found" }); - } - - try { - const repository = createCurrentAutomationRepository(); - const candidates = await repository.listAllEnabled(); - const wanted = hashToken(token); - - const match = candidates.find((row) => { try { - const definition = JSON.parse(row.definition) as AutomationDefinition; - if (definition.trigger?.kind !== "webhook") return false; - return timingSafeEqual(definition.trigger.tokenHash, wanted); - } catch { - return false; + const created = await repository.create({ + userId, + name: name.trim(), + description: description ?? null, + enabled: enabled !== false, + definition: JSON.stringify(validated.definition), + concurrencyPolicy, + channels: Array.isArray(req.body?.channels) ? req.body.channels : [], + }); + await syncSchedule(repository, created.id, validated.definition); + await audit("create_automation", created.id, created.name); + res + .status(201) + .json({ ...serializeAutomation(created, deps), webhookToken }); + } catch (error) { + fail(res, "Failed to create automation", error); } - }); + }, + ); - if (!match) return res.status(404).json({ error: "Not found" }); + /** + * @openapi + * /plugin-api/automations/{id}: + * put: + * summary: Update an automation + * tags: + * - Automations + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: The updated automation. + * 400: + * description: Validation failed. + * 404: + * description: Automation not found. + */ + router.put( + "/:id", + ctx.rbac.require("edit") as never, + async (req: Request, res: Response) => { + const userId = actor(); + const id = parseId(req.params.id); + if (id === null) return res.status(400).json({ error: "Invalid id" }); - const outcome = await AutomationEngine.getInstance().run({ - automationId: match.id, - triggerType: "webhook", - triggerContext: { - body: req.body ?? {}, - receivedAt: new Date().toISOString(), - }, - }); + try { + const existing = await repository.findForUser(id, userId); + if (!existing) { + return res.status(404).json({ error: "Automation not found" }); + } - res.status(202).json({ runId: outcome.runId, status: outcome.status }); - } catch (error) { - databaseLogger.error("Webhook automation failed", error, { - operation: "automation_webhook_error", - }); - res.status(500).json({ error: "Failed to run automation" }); - } -}); + const update: Parameters[2] = {}; + if (req.body?.name !== undefined) { + if (!isNonEmptyString(req.body.name)) { + return res.status(400).json({ error: "Name cannot be empty" }); + } + update.name = req.body.name.trim(); + } + if (req.body?.description !== undefined) { + update.description = req.body.description; + } + if (req.body?.enabled !== undefined) + update.enabled = !!req.body.enabled; + if (req.body?.concurrencyPolicy !== undefined) { + update.concurrencyPolicy = req.body.concurrencyPolicy; + } + if (Array.isArray(req.body?.channels)) { + update.channels = req.body.channels; + } -function hashToken(token: string): string { - return crypto.createHash("sha256").update(token).digest("hex"); -} + let parsedDefinition: AutomationDefinition | null = null; + if (req.body?.definition !== undefined) { + const validated = validateDefinition(req.body.definition); + if (!validated.ok || !validated.definition) { + return res.status(400).json({ error: validated.error }); + } + // Keep the stored token hash: the raw token is only shown once. + if (validated.definition.trigger.kind === "webhook") { + const previous = parseDefinition(existing.definition); + validated.definition = { + ...validated.definition, + trigger: { + kind: "webhook", + tokenHash: + previous?.trigger?.kind === "webhook" + ? previous.trigger.tokenHash + : "", + }, + }; + } + parsedDefinition = validated.definition; + update.definition = JSON.stringify(validated.definition); + } -function timingSafeEqual(a: string, b: string): boolean { - const left = Buffer.from(a || "", "utf8"); - const right = Buffer.from(b || "", "utf8"); - if (left.length !== right.length) return false; - return crypto.timingSafeEqual(left, right); -} + const updated = await repository.update(id, userId, update); + if (!updated) { + return res.status(404).json({ error: "Automation not found" }); + } + if (parsedDefinition) { + await syncSchedule(repository, id, parsedDefinition); + } + await audit("update_automation", id, updated.name); + res.json(serializeAutomation(updated, deps)); + } catch (error) { + fail(res, "Failed to update automation", error); + } + }, + ); -/** Called from activate(). Mounts this router at /automations via the shared - * dispatcher. */ -export function startAutomationsService(mountOn: Router): void { - mountOn.use(router); -} + /** + * @openapi + * /plugin-api/automations/{id}: + * delete: + * summary: Delete an automation + * tags: + * - Automations + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Deleted. + * 404: + * description: Automation not found. + */ + router.delete( + "/:id", + ctx.rbac.require("delete") as never, + async (req: Request, res: Response) => { + const id = parseId(req.params.id); + if (id === null) return res.status(400).json({ error: "Invalid id" }); + try { + const deleted = await repository.delete(id, actor()); + if (!deleted) { + return res.status(404).json({ error: "Automation not found" }); + } + await audit("delete_automation", id, undefined); + res.json({ success: true }); + } catch (error) { + fail(res, "Failed to delete automation", error); + } + }, + ); -/** Called from deactivate(). /automations/* falls back to 404 until - * reactivated. */ -export function stopAutomationsService(): void { - // Unmounted by the runtime when the plugin deactivates. + /** + * @openapi + * /plugin-api/automations/{id}/run: + * post: + * summary: Run an automation now + * description: Runs immediately, as the automation's owner. Pass dryRun to record what each step would do without touching anything outside Termix. An automation that needs a plugin that is off is recorded as skipped. + * tags: + * - Automations + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * dryRun: + * type: boolean + * responses: + * 200: + * description: The run outcome. + * 404: + * description: Automation not found. + */ + router.post( + "/:id/run", + ctx.rbac.require("run") as never, + async (req: Request, res: Response) => { + const userId = actor(); + const id = parseId(req.params.id); + if (id === null) return res.status(400).json({ error: "Invalid id" }); + + try { + const existing = await repository.findForUser(id, userId); + if (!existing) { + return res.status(404).json({ error: "Automation not found" }); + } + const dryRun = req.body?.dryRun === true; + const outcome = await engine.run({ + automationId: id, + triggerType: "manual", + triggerContext: { manual: true, requestedBy: userId }, + dryRun, + }); + await audit( + "run_automation", + id, + existing.name, + outcome.status === "success", + JSON.stringify({ status: outcome.status, dryRun }), + ); + res.json(outcome); + } catch (error) { + fail(res, "Failed to run automation", error); + } + }, + ); } diff --git a/plugins/automations/src/backend/scheduler.ts b/plugins/automations/src/backend/scheduler.ts index 5cb3a618c..d535fcd11 100644 --- a/plugins/automations/src/backend/scheduler.ts +++ b/plugins/automations/src/backend/scheduler.ts @@ -1,217 +1,158 @@ -import type { AutomationDefinition } from "../../../../src/types/automations.js"; -import { createCurrentAutomationRepository } from "../../../../src/backend/database/repositories/factory.js"; -import { DataCrypto } from "../../../../src/backend/utils/data-crypto.js"; -import { statsLogger } from "../../../../src/backend/utils/logger.js"; +import type { PluginLogger } from "@termix/plugin-sdk/backend"; +import type { AutomationDefinition } from "../types.js"; import { computeNextDueAt } from "./cron.js"; import { hasDwelled, isCoolingDown } from "./conditions.js"; -import { reconcileDockerWatch, resetDockerWatcher } from "./docker-watcher.js"; -import { AutomationEngine } from "./engine.js"; -import { reconcileHeadlessViewers } from "./headless-viewer.js"; -import { - subscribeAutomationTriggers, - unsubscribeAutomationTriggers, -} from "./triggers.js"; +import type { AutomationEngine } from "./engine.js"; +import type { AutomationRepository } from "./repository.js"; /** - * The one timer the automations feature owns. - * - * Every other periodic job in the backend is its own module-level setInterval; - * this deliberately is not one per automation. A single tick handles due - * schedules, dwell windows that need re-checking without a fresh sample, the - * synthetic viewer heartbeat, and history pruning. + * The one tick the automations plugin runs. It handles due schedules, dwell + * windows that need re-checking without a fresh sample, the watchers that + * keep metrics and Docker events flowing for watched hosts, and history + * pruning. activate() drives it through ctx.schedule. */ -const TICK_MS = 15_000; -const STARTUP_DELAY_MS = 30_000; +export const TICK_MS = 15_000; +export const STARTUP_DELAY_MS = 30_000; const PRUNE_INTERVAL_MS = 24 * 60 * 60 * 1000; const RUN_RETENTION_DAYS = 30; /** A "running" row older than this belongs to a process that is gone. */ const STALE_RUN_MS = 6 * 60 * 60 * 1000; -let tickTimer: NodeJS.Timeout | null = null; -let startupTimer: NodeJS.Timeout | null = null; -let lastPruneAt = 0; -let ticking = false; - -export function startAutomationScheduler(): void { - if (tickTimer) return; - - // Event-driven triggers come through the bus; the interval below only drives - // the schedule-based ones. - subscribeAutomationTriggers(); - - startupTimer = setTimeout(() => { - void tick(); - }, STARTUP_DELAY_MS); - startupTimer.unref?.(); - - tickTimer = setInterval(() => { - void tick(); - }, TICK_MS); - tickTimer.unref?.(); +export interface SchedulerDeps { + repository: AutomationRepository; + engine: Pick; + log: PluginLogger; + /** Watchers brought in line with the enabled automations every tick. */ + reconcile: Array<(now: number) => Promise>; } -export function stopAutomationScheduler(): void { - if (tickTimer) clearInterval(tickTimer); - if (startupTimer) clearTimeout(startupTimer); - tickTimer = null; - startupTimer = null; - unsubscribeAutomationTriggers(); - resetDockerWatcher(); -} +export function createScheduler(deps: SchedulerDeps) { + const { repository, engine, log } = deps; + let lastPruneAt = 0; + let ticking = false; -/** Exposed for tests; the interval calls this. */ -export async function tick(now: Date = new Date()): Promise { - // A slow tick must not overlap the next one. - if (ticking) return; - ticking = true; + const errorText = (error: unknown) => + error instanceof Error ? error.message : String(error); - try { - await reconcileHeadlessViewers().catch(() => undefined); - await runDueSchedules(now); - await recheckOpenBreaches(now); - await reconcileDockerWatch(now.getTime()).catch(() => undefined); - await pruneIfDue(now); - } catch (error) { - statsLogger.warn("Automation scheduler tick failed", { - operation: "automation_scheduler_tick_error", - error: error instanceof Error ? error.message : String(error), - }); - } finally { - ticking = false; - } -} + async function runDueSchedules(now: Date): Promise { + const due = await repository.listDueSchedules(now.toISOString()); -async function runDueSchedules(now: Date): Promise { - const repository = createCurrentAutomationRepository(); - const due = await repository.listDueSchedules(now.toISOString()); - - for (const schedule of due) { - const automation = await repository.findById(schedule.automationId); - if (!automation) continue; - - // Background work can only touch a user's data while their key resolves. - if (!canAccess(automation.userId)) { - statsLogger.warn("Skipping scheduled automation: data key unavailable", { - operation: "automation_schedule_locked", - automationId: automation.id, - }); - continue; - } - - const nextDueAt = computeNextDueAt( - { - cron: schedule.cron, - intervalSeconds: schedule.intervalSeconds, - timezone: schedule.timezone, - }, - now, - ); - await repository.markScheduleTicked( - schedule.automationId, - nextDueAt, - now.toISOString(), - ); - - AutomationEngine.getInstance() - .run({ - automationId: schedule.automationId, - triggerType: "schedule", - triggerContext: { scheduledFor: now.toISOString() }, - }) - .catch((error) => { - statsLogger.warn("Scheduled automation failed to start", { - operation: "automation_schedule_error", - automationId: schedule.automationId, - error: error instanceof Error ? error.message : String(error), - }); - }); - } -} - -/** - * Fires sustained breaches whose window has elapsed. - * - * Without this a dwell window only completes when another sample happens to - * arrive, so a breach that starts just before polling stops would never fire. - */ -async function recheckOpenBreaches(now: Date): Promise { - const repository = createCurrentAutomationRepository(); - const open = await repository.listOpenBreaches(); - const nowMs = now.getTime(); - - for (const state of open) { - const automation = await repository.findById(state.automationId); - if (!automation || !automation.enabled) continue; - if (!canAccess(automation.userId)) continue; - - let definition: AutomationDefinition; - try { - definition = JSON.parse(automation.definition) as AutomationDefinition; - } catch { - continue; - } - - const trigger = definition.trigger; - if (trigger?.kind !== "metric_threshold") continue; - if (!trigger.forSeconds) continue; - if (!hasDwelled(state.breachStartedAt, trigger.forSeconds, nowMs)) continue; - if (isCoolingDown(state.lastFiredAt, trigger.cooldownMinutes, nowMs)) { - continue; - } - - const hostId = Number(state.stateKey.split(":")[0]); - await repository.upsertTriggerState({ - automationId: automation.id, - stateKey: state.stateKey, - lastFiredAt: now.toISOString(), - }); - - AutomationEngine.getInstance() - .run({ - automationId: automation.id, - triggerType: "metric_threshold", - triggerContext: { - hostId, - value: state.lastValue, - threshold: trigger.value, - metric: trigger.metric.path, - sustained: true, + for (const schedule of due) { + const nextDueAt = computeNextDueAt( + { + cron: schedule.cron, + intervalSeconds: schedule.intervalSeconds, + timezone: schedule.timezone, }, - triggerHostId: Number.isFinite(hostId) ? hostId : undefined, - }) - .catch(() => undefined); - } -} + now, + ); + await repository.markScheduleTicked( + schedule.automationId, + nextDueAt, + now.toISOString(), + ); -async function pruneIfDue(now: Date): Promise { - if (now.getTime() - lastPruneAt < PRUNE_INTERVAL_MS) return; - lastPruneAt = now.getTime(); - - const repository = createCurrentAutomationRepository(); - try { - await repository.failStaleRunningRuns( - new Date(now.getTime() - STALE_RUN_MS).toISOString(), - ); - const deleted = await repository.pruneRunsOlderThan(RUN_RETENTION_DAYS); - if (deleted > 0) { - statsLogger.info(`Pruned ${deleted} old automation run(s)`, { - operation: "automation_run_prune", - deleted, - }); + void engine + .run({ + automationId: schedule.automationId, + triggerType: "schedule", + triggerContext: { scheduledFor: now.toISOString() }, + }) + .catch((error: unknown) => + log.warn( + `Scheduled automation ${schedule.automationId} failed to start: ${errorText(error)}`, + ), + ); } - } catch (error) { - statsLogger.warn("Automation run pruning failed", { - operation: "automation_run_prune_error", - error: error instanceof Error ? error.message : String(error), - }); } -} -function canAccess(userId: string): boolean { - try { - return DataCrypto.canUserAccessData(userId); - } catch { - return false; + /** + * Fires sustained breaches whose window has elapsed. Without this a dwell + * window only completes when another sample happens to arrive. + */ + async function recheckOpenBreaches(now: Date): Promise { + const open = await repository.listOpenBreaches(); + const nowMs = now.getTime(); + + for (const state of open) { + const automation = await repository.findById(state.automationId); + if (!automation || !automation.enabled) continue; + + let definition: AutomationDefinition; + try { + definition = JSON.parse(automation.definition) as AutomationDefinition; + } catch { + continue; + } + + const trigger = definition.trigger; + if (trigger?.kind !== "metric_threshold") continue; + if (!trigger.forSeconds) continue; + if (!hasDwelled(state.breachStartedAt, trigger.forSeconds, nowMs)) { + continue; + } + if (isCoolingDown(state.lastFiredAt, trigger.cooldownMinutes, nowMs)) { + continue; + } + + const hostId = Number(state.stateKey.split(":")[0]); + await repository.upsertTriggerState({ + automationId: automation.id, + stateKey: state.stateKey, + lastFiredAt: now.toISOString(), + }); + + void engine + .run({ + automationId: automation.id, + triggerType: "metric_threshold", + triggerContext: { + hostId, + value: state.lastValue, + threshold: trigger.value, + metric: trigger.metric.path, + sustained: true, + }, + triggerHostId: Number.isFinite(hostId) ? hostId : undefined, + }) + .catch(() => undefined); + } } + + async function pruneIfDue(now: Date): Promise { + if (now.getTime() - lastPruneAt < PRUNE_INTERVAL_MS) return; + lastPruneAt = now.getTime(); + try { + await repository.failStaleRunningRuns( + new Date(now.getTime() - STALE_RUN_MS).toISOString(), + ); + const deleted = await repository.pruneRunsOlderThan(RUN_RETENTION_DAYS); + if (deleted > 0) log.info(`Pruned ${deleted} old automation run(s)`); + } catch (error) { + log.warn(`Automation run pruning failed: ${errorText(error)}`); + } + } + + return { + async tick(now: Date = new Date()): Promise { + // A slow tick must not overlap the next one. + if (ticking) return; + ticking = true; + try { + for (const reconcile of deps.reconcile) { + await reconcile(now.getTime()).catch((error: unknown) => + log.warn(`Automation watcher failed: ${errorText(error)}`), + ); + } + await runDueSchedules(now); + await recheckOpenBreaches(now); + await pruneIfDue(now); + } catch (error) { + log.warn(`Automation scheduler tick failed: ${errorText(error)}`); + } finally { + ticking = false; + } + }, + }; } diff --git a/plugins/automations/src/backend/service.ts b/plugins/automations/src/backend/service.ts new file mode 100644 index 000000000..48179f9cf --- /dev/null +++ b/plugins/automations/src/backend/service.ts @@ -0,0 +1,139 @@ +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import type { AutomationDefinition, RunStatus } from "../types.js"; +import type { Deps } from "./deps.js"; +import type { AutomationEngine } from "./engine.js"; +import type { AutomationRepository } from "./repository.js"; +import { missingPlugins, parseDefinition } from "./requirements.js"; +import { syncSchedule } from "./routes.js"; +import { validateDefinition } from "./validate.js"; + +export interface AutomationSummary { + id: number; + name: string; + description: string | null; + enabled: boolean; + triggerKind: string | null; + lastRunAt: string | null; + lastRunStatus: string | null; + missingPlugins: string[]; +} + +/** + * The "automations.access" service, version 1. Runs as the caller's user, + * gated on automations.view; create and run also need automations.create and + * automations.run. + */ +export interface AutomationsAccessV1 { + list: () => Promise; + get: ( + id: number, + ) => Promise< + (AutomationSummary & { definition: AutomationDefinition | null }) | null + >; + /** + * Validates the definition the same way the create route does. Starts + * disabled unless `enabled` is set, so an automation nobody watched run + * does not start firing on its own. + */ + create: (input: { + name: string; + description?: string | null; + definition: unknown; + enabled?: boolean; + }) => Promise<{ id: number; name: string }>; + run: ( + id: number, + options?: { dryRun?: boolean }, + ) => Promise<{ runId: number | null; status: RunStatus; error?: string }>; +} + +export function createAutomationsService( + ctx: PluginContext, + repository: AutomationRepository, + engine: Pick, + deps: Deps, +): AutomationsAccessV1 { + const actor = (method: string) => { + const userId = ctx.currentActor(); + if (!userId) throw new Error(`automations.access.${method} needs an actor`); + return userId; + }; + const summarize = ( + row: Awaited>[number], + ) => { + const definition = parseDefinition(row.definition); + if (definition?.trigger?.kind === "webhook") { + definition.trigger = { ...definition.trigger, tokenHash: "" }; + } + return { + summary: { + id: row.id, + name: row.name, + description: row.description, + enabled: !!row.enabled, + triggerKind: definition?.trigger?.kind ?? null, + lastRunAt: row.last_run_at, + lastRunStatus: row.last_run_status, + missingPlugins: missingPlugins(definition, deps), + }, + definition, + }; + }; + + return { + list: async () => { + const rows = await repository.list(actor("list")); + return rows.map((row) => summarize(row).summary); + }, + + get: async (id) => { + const row = await repository.findForUser(id, actor("get")); + if (!row) return null; + const { summary, definition } = summarize(row); + return { ...summary, definition }; + }, + + create: async (input) => { + const userId = actor("create"); + if (!(await ctx.rbac.has("create"))) { + throw new Error('Creating automations needs "automations.create"'); + } + const name = typeof input.name === "string" ? input.name.trim() : ""; + if (!name) throw new Error("Name is required"); + const validated = validateDefinition(input.definition); + if (!validated.ok || !validated.definition) { + throw new Error( + validated.error ?? "The automation definition is invalid", + ); + } + if (validated.definition.trigger.kind === "webhook") { + // A webhook's token is only ever shown by the create route. + throw new Error("Webhook automations are created from the editor"); + } + const created = await repository.create({ + userId, + name, + description: input.description ?? null, + definition: JSON.stringify(validated.definition), + enabled: input.enabled ?? false, + }); + await syncSchedule(repository, created.id, validated.definition); + return { id: created.id, name: created.name }; + }, + + run: async (id, options = {}) => { + const userId = actor("run"); + if (!(await ctx.rbac.has("run"))) { + throw new Error('Running automations needs "automations.run"'); + } + const existing = await repository.findForUser(id, userId); + if (!existing) throw new Error("Automation not found"); + return engine.run({ + automationId: id, + triggerType: "manual", + triggerContext: { manual: true, requestedBy: userId }, + dryRun: options.dryRun === true, + }); + }, + }; +} diff --git a/plugins/automations/src/backend/snippets.ts b/plugins/automations/src/backend/snippets.ts deleted file mode 100644 index e86dceb99..000000000 --- a/plugins/automations/src/backend/snippets.ts +++ /dev/null @@ -1,91 +0,0 @@ -import type { PluginServices } from "@termix/plugin-sdk/backend"; - -let current: PluginServices | null = null; - -/** Set in activate, cleared on deactivate. */ -export function setPluginServices(services: PluginServices | null): void { - current = services; -} - -/** Another plugin's snippets, as ctx.services.get("snippets.access") returns them. */ -interface SnippetsAccess { - get: (id: number) => Promise<{ - id: number; - name: string; - content: string; - isNote: boolean; - } | null>; - resolveCommand: ( - id: number, - vars: { ip?: string; username?: string; port?: number; name?: string }, - inputValues?: Record, - ) => Promise; -} - -/** - * A snippet's command, resolved against a target host and run as userId. - * Null when the snippets plugin is off, the user may not use it, or the - * snippet does not exist. Optional: the run_snippet step fails with a clear - * message without it, rather than the plugin failing to activate. - */ -export async function resolveSnippetCommandFor( - userId: string, - id: number, - vars: { ip?: string; username?: string; port?: number; name?: string }, - inputValues?: Record, -): Promise { - if (!current) return null; - try { - return await current - .get("snippets.access", { userId }) - .resolveCommand(id, vars, inputValues); - } catch { - return null; - } -} - -export async function getSnippet( - userId: string, - id: number, -): Promise<{ - id: number; - name: string; - content: string; - isNote: boolean; -} | null> { - if (!current) return null; - try { - return await current - .get("snippets.access", { userId }) - .get(id); - } catch { - return null; - } -} - -/** Another plugin's fleets, as ctx.services.get("fleets.access") returns them. */ -interface FleetsAccess { - members: ( - fleetId: number, - ) => Promise>; -} - -/** - * A fleet's effective member host ids, for a "fleet" target selector. Empty - * when the fleets plugin is off, the user may not use it, or the fleet does - * not exist - the step then runs against no hosts rather than failing. - */ -export async function fleetMemberIds( - userId: string, - fleetId: number, -): Promise { - if (!current) return []; - try { - const members = await current - .get("fleets.access", { userId }) - .members(fleetId); - return members.map((member) => member.id); - } catch { - return []; - } -} diff --git a/plugins/automations/src/backend/ssh.ts b/plugins/automations/src/backend/ssh.ts deleted file mode 100644 index b7a221543..000000000 --- a/plugins/automations/src/backend/ssh.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Client } from "ssh2"; -import type { - PluginSsh, - PluginSshConnection, - PluginSshConnectOptions, - PluginSshHost, -} from "@termix/plugin-sdk/backend"; - -let current: PluginSsh | null = null; - -/** Set in activate, cleared on deactivate. */ -export function setPluginSsh(ssh: PluginSsh | null): void { - current = ssh; -} - -export function pluginSsh(): PluginSsh { - if (!current) throw new Error("The plugin is not active"); - return current; -} - -/** withConnection with ssh2's Client type filled in. */ -export function withSshConnection( - host: number | PluginSshHost, - options: PluginSshConnectOptions & { pool: string }, - fn: (client: Client) => Promise, -): Promise { - return pluginSsh().withConnection(host, options, fn); -} - -/** connect with ssh2's Client type filled in. */ -export function connectSsh( - host: number | PluginSshHost, - options?: PluginSshConnectOptions, -): Promise> { - return pluginSsh().connect(host, options); -} diff --git a/plugins/automations/src/backend/tables.ts b/plugins/automations/src/backend/tables.ts new file mode 100644 index 000000000..20291cbce --- /dev/null +++ b/plugins/automations/src/backend/tables.ts @@ -0,0 +1,197 @@ +import { + adoptLegacyTable, + boolean, + defineTable, + id, + integer, + real, + refUser, + text, + timestamp, + varchar, +} from "@termix/plugin-sdk/db"; + +/* + * Adopted from core's automation tables, so column and index names are the + * legacy ones and the rename carries every row across. + * + * The links between these tables, and a channel link to core's + * notification_channels, are plain integers here: the SDK only references + * users and ssh_data. The adoption migrations keep the legacy FOREIGN KEY + * clauses by hand, so deleting an automation still cascades. + */ + +export const automations = adoptLegacyTable( + "automations", + defineTable( + "automations", + { + id: id(), + userId: refUser(), + name: text().notNull(), + description: text(), + enabled: boolean().notNull().default(true), + // The whole trigger and steps graph, read and written as a unit. + definition: text().notNull(), + definitionVersion: integer().notNull().default(1), + concurrencyPolicy: text().notNull().default("skip"), + maxRunSeconds: integer().notNull().default(300), + dryRun: boolean().notNull().default(false), + lastRunAt: text(), + lastRunStatus: text(), + createdAt: timestamp().notNull().defaultNow(), + updatedAt: timestamp().notNull().defaultNow(), + }, + { + indexes: [ + { name: "idx_automations_user", columns: ["userId", "enabled"] }, + ], + }, + ), +); + +/** + * Durable per-target trigger state, so cooldowns and dwell windows survive a + * restart. state_key scopes it to what the trigger watches ("", + * ":/data", ":"). + */ +export const triggerState = adoptLegacyTable( + "automation_trigger_state", + defineTable( + "trigger_state", + { + id: id(), + automationId: integer().notNull(), + stateKey: varchar().notNull(), + breachStartedAt: text(), + lastFiredAt: text(), + lastValue: real(), + lastObservedState: text(), + updatedAt: timestamp().notNull().defaultNow(), + }, + { + uniques: [ + { + name: "idx_automation_trigger_state_key", + columns: ["automationId", "stateKey"], + }, + ], + }, + ), +); + +export const schedules = adoptLegacyTable( + "automation_schedules", + defineTable( + "schedules", + { + id: id(), + automationId: integer().notNull(), + cron: text(), + intervalSeconds: integer(), + timezone: text(), + nextDueAt: timestamp(), + lastTickAt: text(), + }, + { + uniques: [ + { + name: "idx_automation_schedules_automation", + columns: ["automationId"], + }, + ], + indexes: [ + { name: "idx_automation_schedules_due", columns: ["nextDueAt"] }, + ], + }, + ), +); + +export const runs = adoptLegacyTable( + "automation_runs", + defineTable( + "runs", + { + id: id(), + automationId: integer().notNull(), + userId: refUser(), + triggerType: text().notNull(), + triggerContext: text(), + status: text().notNull(), + startedAt: timestamp().notNull().defaultNow(), + finishedAt: text(), + durationMs: integer(), + error: text(), + dryRun: boolean().notNull().default(false), + // Set when one automation invoked another, so a chain can be traced. + parentRunId: integer(), + }, + { + indexes: [ + { + name: "idx_automation_runs_automation", + columns: ["automationId", "startedAt"], + }, + { name: "idx_automation_runs_user", columns: ["userId", "startedAt"] }, + ], + }, + ), +); + +export const runSteps = adoptLegacyTable( + "automation_run_steps", + defineTable( + "run_steps", + { + id: id(), + runId: integer().notNull(), + stepIndex: integer().notNull(), + stepId: text().notNull(), + stepType: text().notNull(), + status: text().notNull(), + startedAt: timestamp().notNull().defaultNow(), + finishedAt: text(), + output: text(), + error: text(), + truncated: boolean().notNull().default(false), + }, + { + indexes: [ + { + name: "idx_automation_run_steps_run", + columns: ["runId", "stepIndex"], + }, + ], + }, + ), +); + +/** Which of core's notification channels an automation is linked to. */ +export const channels = adoptLegacyTable( + "automation_channels", + defineTable( + "channels", + { + id: id(), + automationId: integer().notNull(), + channelId: integer().notNull(), + }, + { + uniques: [ + { + name: "idx_automation_channels_pair", + columns: ["automationId", "channelId"], + }, + ], + }, + ), +); + +export const tables = [ + automations, + triggerState, + schedules, + runs, + runSteps, + channels, +]; diff --git a/plugins/automations/src/backend/triggers.ts b/plugins/automations/src/backend/triggers.ts index 00bed9ee0..4aad1b1d9 100644 --- a/plugins/automations/src/backend/triggers.ts +++ b/plugins/automations/src/backend/triggers.ts @@ -1,11 +1,9 @@ +import type { PluginEvents, PluginLogger } from "@termix/plugin-sdk/backend"; +import type { AutomationDefinition, HostSelector, Trigger } from "../types.js"; import type { - AutomationDefinition, - HostSelector, - Trigger, -} from "../../../../src/types/automations.js"; -import { createCurrentAutomationRepository } from "../../../../src/backend/database/repositories/factory.js"; -import type { AutomationEngineRow } from "../../../../src/backend/database/repositories/automation-repository.js"; -import { statsLogger } from "../../../../src/backend/utils/logger.js"; + AutomationEngineRow, + AutomationRepository, +} from "./repository.js"; import { compare, extractMetricValue, @@ -15,11 +13,7 @@ import { severityForValue, type MetricsSnapshot, } from "./conditions.js"; -import { AutomationEngine } from "./engine.js"; -import { - pluginEvents, - TOPICS, -} from "../../../../src/backend/plugins/events.js"; +import { TOPIC_AUTOMATION_FAILED, type AutomationEngine } from "./engine.js"; /** * Matches events against automation triggers and decides what fires. @@ -68,377 +62,404 @@ interface LoadedAutomation { definition: AutomationDefinition; } -async function loadEnabledFor(userId: string): Promise { - try { - const rows = - await createCurrentAutomationRepository().listEnabledForUser(userId); - const loaded: LoadedAutomation[] = []; - for (const row of rows) { - try { - loaded.push({ - row, - definition: JSON.parse(row.definition) as AutomationDefinition, - }); - } catch { - // A malformed definition should not stop the others from evaluating. +export type Triggers = ReturnType; + +/** Built once per activation. */ +export function createTriggers( + repository: AutomationRepository, + engine: Pick, + log: PluginLogger, +) { + async function loadEnabledFor(userId: string): Promise { + try { + const rows = await repository.listEnabledForUser(userId); + const loaded: LoadedAutomation[] = []; + for (const row of rows) { + try { + loaded.push({ + row, + definition: JSON.parse(row.definition) as AutomationDefinition, + }); + } catch { + // A malformed definition should not stop the others from evaluating. + } } + return loaded; + } catch { + return []; } - return loaded; - } catch { - return []; } -} -/** Whether a selector covers a host. Ownership is checked by the caller. */ -function selectorCoversHost( - selector: HostSelector | undefined, - hostId: number, -): boolean { - if (!selector) return true; - switch (selector.kind) { - case "all": - case "trigger": - return true; - case "host": - return selector.hostId === hostId; - case "hosts": - return selector.hostIds.includes(hostId); - case "fleet": - // Fleet membership is resolved at execution time; evaluate optimistically - // so a fleet-scoped trigger still reaches the engine. - return true; - default: - return false; + /** Whether a selector covers a host. Ownership is checked by the caller. */ + function selectorCoversHost( + selector: HostSelector | undefined, + hostId: number, + ): boolean { + if (!selector) return true; + switch (selector.kind) { + case "all": + case "trigger": + return true; + case "host": + return selector.hostId === hostId; + case "hosts": + return selector.hostIds.includes(hostId); + case "fleet": + // Fleet membership is resolved at execution time; evaluate optimistically + // so a fleet-scoped trigger still reaches the engine. + return true; + default: + return false; + } } -} -async function fire( - automation: AutomationEngineRow, - stateKey: string, - triggerType: string, - triggerContext: Record, - hostId?: number, -): Promise { - const repository = createCurrentAutomationRepository(); - await repository.upsertTriggerState({ - automationId: automation.id, - stateKey, - lastFiredAt: new Date().toISOString(), - }); - - AutomationEngine.getInstance() - .run({ + async function fire( + automation: AutomationEngineRow, + stateKey: string, + triggerType: string, + triggerContext: Record, + hostId?: number, + ): Promise { + await repository.upsertTriggerState({ automationId: automation.id, - triggerType, - triggerContext, - triggerHostId: hostId, - }) - .catch((error) => { - statsLogger.warn("Automation run failed to start", { - operation: "automation_trigger_error", + stateKey, + lastFiredAt: new Date().toISOString(), + }); + + engine + .run({ automationId: automation.id, - error: error instanceof Error ? error.message : String(error), + triggerType, + triggerContext, + triggerHostId: hostId, + }) + .catch((error: unknown) => { + log.warn( + `Automation ${automation.id} failed to start: ${ + error instanceof Error ? error.message : String(error) + }`, + ); }); - }); -} - -/** - * Metric samples. Called for every polled host, including hosts polled only - * because an automation asked for them. - */ -export async function onMetrics(event: MetricEvent): Promise { - const automations = await loadEnabledFor(event.ownerUserId); - const repository = createCurrentAutomationRepository(); - const now = Date.now(); - - for (const { row, definition } of automations) { - const trigger = definition.trigger; - if (trigger?.kind !== "metric_threshold") continue; - if (!selectorCoversHost(trigger.hostSelector, event.hostId)) continue; - - const value = extractMetricValue(event.metrics, trigger.metric); - if (value === null) continue; - - const stateKey = metricStateKey(event.hostId, trigger.metric); - const state = await repository.getTriggerState(row.id, stateKey); - const breaching = compare(value, trigger.operator, trigger.value); - - if (!breaching) { - if (state?.breachStartedAt) { - await repository.clearBreach(row.id, stateKey); - } - continue; - } - - // Open the dwell window on the first breaching sample. - if (!state?.breachStartedAt) { - await repository.upsertTriggerState({ - automationId: row.id, - stateKey, - breachStartedAt: new Date(now).toISOString(), - lastValue: value, - }); - if (trigger.forSeconds) continue; - } - - const breachStartedAt = - state?.breachStartedAt ?? new Date(now).toISOString(); - if (!hasDwelled(breachStartedAt, trigger.forSeconds, now)) continue; - if (isCoolingDown(state?.lastFiredAt, trigger.cooldownMinutes, now)) - continue; - - await fire( - row, - stateKey, - "metric_threshold", - { - hostId: event.hostId, - value, - threshold: trigger.value, - operator: trigger.operator, - metric: trigger.metric.path, - mount: "mount" in trigger.metric ? trigger.metric.mount : undefined, - severity: severityForValue(value, trigger.severity), - }, - event.hostId, - ); } -} -/** Host reachability transitions. Only edges fire, never steady state. */ -export async function onStatus(event: StatusEvent): Promise { - const automations = await loadEnabledFor(event.ownerUserId); - const repository = createCurrentAutomationRepository(); - const now = Date.now(); - const observed = event.online ? "online" : "offline"; + /** + * Metric samples. Called for every polled host, including hosts polled only + * because an automation asked for them. + */ + async function onMetrics(event: MetricEvent): Promise { + const automations = await loadEnabledFor(event.ownerUserId); + const now = Date.now(); - for (const { row, definition } of automations) { - const trigger = definition.trigger; - if (trigger?.kind !== "host_status") continue; - if (!selectorCoversHost(trigger.hostSelector, event.hostId)) continue; + for (const { row, definition } of automations) { + const trigger = definition.trigger; + if (trigger?.kind !== "metric_threshold") continue; + if (!selectorCoversHost(trigger.hostSelector, event.hostId)) continue; - const stateKey = String(event.hostId); - const state = await repository.getTriggerState(row.id, stateKey); + const value = extractMetricValue(event.metrics, trigger.metric); + if (value === null) continue; - if (state?.lastObservedState === observed) continue; + const stateKey = metricStateKey(event.hostId, trigger.metric); + const state = await repository.getTriggerState(row.id, stateKey); + const breaching = compare(value, trigger.operator, trigger.value); - await repository.upsertTriggerState({ - automationId: row.id, - stateKey, - lastObservedState: observed, - }); - - // The first observation establishes a baseline rather than firing, so a - // restart does not announce every host as though it just changed. - if (!state?.lastObservedState) continue; - if (trigger.to !== observed) continue; - if (isCoolingDown(state?.lastFiredAt, trigger.cooldownMinutes, now)) - continue; - - await fire( - row, - stateKey, - "host_status", - { hostId: event.hostId, status: observed }, - event.hostId, - ); - } -} - -export async function onHealthCheck(event: HealthEvent): Promise { - const automations = await loadEnabledFor(event.userId); - const repository = createCurrentAutomationRepository(); - const now = Date.now(); - const observed = event.ok ? "recovered" : "failing"; - - for (const { row, definition } of automations) { - const trigger = definition.trigger; - if (trigger?.kind !== "health_check") continue; - if (!selectorCoversHost(trigger.hostSelector, event.hostId)) continue; - if (trigger.checkId && trigger.checkId !== event.checkId) continue; - - const stateKey = `${event.hostId}:${event.checkId}`; - const state = await repository.getTriggerState(row.id, stateKey); - - if (state?.lastObservedState === observed) continue; - - await repository.upsertTriggerState({ - automationId: row.id, - stateKey, - lastObservedState: observed, - }); - - if (!state?.lastObservedState) continue; - if (trigger.to !== observed) continue; - if (isCoolingDown(state?.lastFiredAt, trigger.cooldownMinutes, now)) - continue; - - await fire( - row, - stateKey, - "health_check", - { - hostId: event.hostId, - checkId: event.checkId, - state: observed, - detail: event.detail, - }, - event.hostId, - ); - } -} - -export async function onDockerEvent(event: DockerEvent): Promise { - const automations = await loadEnabledFor(event.ownerUserId); - const repository = createCurrentAutomationRepository(); - const now = Date.now(); - - for (const { row, definition } of automations) { - const trigger = definition.trigger; - if (trigger?.kind !== "docker_event") continue; - if (!selectorCoversHost(trigger.hostSelector, event.hostId)) continue; - if (trigger.container && trigger.container !== event.container) continue; - if (trigger.event !== event.event) continue; - - const stateKey = `${event.hostId}:${event.container}`; - const state = await repository.getTriggerState(row.id, stateKey); - if (isCoolingDown(state?.lastFiredAt, trigger.cooldownMinutes, now)) - continue; - - await fire( - row, - stateKey, - "docker_event", - { - hostId: event.hostId, - container: event.container, - event: event.event, - }, - event.hostId, - ); - } -} - -export async function onInternalEvent(event: InternalEvent): Promise { - const automations = await loadEnabledFor(event.userId); - const repository = createCurrentAutomationRepository(); - const now = Date.now(); - - for (const { row, definition } of automations) { - const trigger = definition.trigger; - if (trigger?.kind !== "internal_event") continue; - if (trigger.event !== event.event) continue; - if ( - event.hostId !== undefined && - !selectorCoversHost(trigger.hostSelector, event.hostId) - ) { - continue; - } - - const stateKey = event.hostId ? String(event.hostId) : "global"; - const state = await repository.getTriggerState(row.id, stateKey); - if (isCoolingDown(state?.lastFiredAt, trigger.cooldownMinutes, now)) - continue; - - await fire( - row, - stateKey, - "internal_event", - { event: event.event, hostId: event.hostId, ...(event.details ?? {}) }, - event.hostId, - ); - } -} - -let unsubscribers: Array<() => void> = []; - -/** - * Subscribes the engine to the event bus. - * - * The hosts modules publish onto the bus and know nothing about automations; - * this is the other half of that arrangement, and it is what keeps the edge - * one-directional (automations may import repositories, hosts modules must not - * import automations). - * - * Handlers are async and the bus is fire-and-forget, so a rejection here is - * logged by the bus rather than surfacing to whichever host feature emitted. - */ -export function subscribeAutomationTriggers(): void { - if (unsubscribers.length > 0) return; - - unsubscribers = [ - pluginEvents.on("plugin.host-metrics.snapshot", (payload) => - onMetrics(payload as MetricEvent), - ), - pluginEvents.on(TOPICS.hostStatus, (payload) => - onStatus(payload as StatusEvent), - ), - pluginEvents.on("plugin.host-metrics.health-check", (payload) => - onHealthCheck(payload as HealthEvent), - ), - pluginEvents.on(TOPICS.internalEvent, (payload) => - onInternalEvent(payload as InternalEvent), - ), - // An SSH login seen by the terminal, which host-metrics used to relay. - pluginEvents.on(TOPICS.hostLogin, (payload) => { - const login = payload as { - hostId: number; - userId: string; - sshUser?: string; - fromIp?: string; - }; - if (!login?.userId) return; - return onInternalEvent({ - event: "user_login", - userId: login.userId, - hostId: login.hostId, - details: { sshUser: login.sshUser, fromIp: login.fromIp }, - }); - }), - ]; -} - -export function unsubscribeAutomationTriggers(): void { - for (const unsubscribe of unsubscribers) unsubscribe(); - unsubscribers = []; -} - -/** - * Hosts that an enabled automation watches, so the poller knows to collect - * metrics for them even when nobody is looking. - */ -export async function listAutomationWatchedHosts(): Promise< - Map -> { - const watched = new Map(); - - try { - const rows = await createCurrentAutomationRepository().listAllEnabled(); - for (const row of rows) { - let definition: AutomationDefinition; - try { - definition = JSON.parse(row.definition) as AutomationDefinition; - } catch { + if (!breaching) { + if (state?.breachStartedAt) { + await repository.clearBreach(row.id, stateKey); + } continue; } - const trigger: Trigger | undefined = definition.trigger; - // Only metric thresholds need heavy collection; status triggers are - // already served by the cheap reachability probe. - if (trigger?.kind !== "metric_threshold") continue; - - const selector = trigger.hostSelector; - if (selector?.kind === "host") { - watched.set(selector.hostId, row.userId); - } else if (selector?.kind === "hosts") { - for (const hostId of selector.hostIds) watched.set(hostId, row.userId); + // Open the dwell window on the first breaching sample. + if (!state?.breachStartedAt) { + await repository.upsertTriggerState({ + automationId: row.id, + stateKey, + breachStartedAt: new Date(now).toISOString(), + lastValue: value, + }); + if (trigger.forSeconds) continue; } - // Fleet and "all" selectors are resolved by the scheduler, which can - // expand them without blocking this call. + + const breachStartedAt = + state?.breachStartedAt ?? new Date(now).toISOString(); + if (!hasDwelled(breachStartedAt, trigger.forSeconds, now)) continue; + if (isCoolingDown(state?.lastFiredAt, trigger.cooldownMinutes, now)) + continue; + + await fire( + row, + stateKey, + "metric_threshold", + { + hostId: event.hostId, + value, + threshold: trigger.value, + operator: trigger.operator, + metric: trigger.metric.path, + mount: "mount" in trigger.metric ? trigger.metric.mount : undefined, + severity: severityForValue(value, trigger.severity), + }, + event.hostId, + ); + } + } + + /** Host reachability transitions. Only edges fire, never steady state. */ + async function onStatus(event: StatusEvent): Promise { + const automations = await loadEnabledFor(event.ownerUserId); + const now = Date.now(); + const observed = event.online ? "online" : "offline"; + + for (const { row, definition } of automations) { + const trigger = definition.trigger; + if (trigger?.kind !== "host_status") continue; + if (!selectorCoversHost(trigger.hostSelector, event.hostId)) continue; + + const stateKey = String(event.hostId); + const state = await repository.getTriggerState(row.id, stateKey); + + if (state?.lastObservedState === observed) continue; + + await repository.upsertTriggerState({ + automationId: row.id, + stateKey, + lastObservedState: observed, + }); + + // The first observation establishes a baseline rather than firing, so a + // restart does not announce every host as though it just changed. + if (!state?.lastObservedState) continue; + if (trigger.to !== observed) continue; + if (isCoolingDown(state?.lastFiredAt, trigger.cooldownMinutes, now)) + continue; + + await fire( + row, + stateKey, + "host_status", + { hostId: event.hostId, status: observed }, + event.hostId, + ); + } + } + + async function onHealthCheck(event: HealthEvent): Promise { + const automations = await loadEnabledFor(event.userId); + const now = Date.now(); + const observed = event.ok ? "recovered" : "failing"; + + for (const { row, definition } of automations) { + const trigger = definition.trigger; + if (trigger?.kind !== "health_check") continue; + if (!selectorCoversHost(trigger.hostSelector, event.hostId)) continue; + if (trigger.checkId && trigger.checkId !== event.checkId) continue; + + const stateKey = `${event.hostId}:${event.checkId}`; + const state = await repository.getTriggerState(row.id, stateKey); + + if (state?.lastObservedState === observed) continue; + + await repository.upsertTriggerState({ + automationId: row.id, + stateKey, + lastObservedState: observed, + }); + + if (!state?.lastObservedState) continue; + if (trigger.to !== observed) continue; + if (isCoolingDown(state?.lastFiredAt, trigger.cooldownMinutes, now)) + continue; + + await fire( + row, + stateKey, + "health_check", + { + hostId: event.hostId, + checkId: event.checkId, + state: observed, + detail: event.detail, + }, + event.hostId, + ); + } + } + + async function onDockerEvent(event: DockerEvent): Promise { + const automations = await loadEnabledFor(event.ownerUserId); + const now = Date.now(); + + for (const { row, definition } of automations) { + const trigger = definition.trigger; + if (trigger?.kind !== "docker_event") continue; + if (!selectorCoversHost(trigger.hostSelector, event.hostId)) continue; + if (trigger.container && trigger.container !== event.container) continue; + if (trigger.event !== event.event) continue; + + const stateKey = `${event.hostId}:${event.container}`; + const state = await repository.getTriggerState(row.id, stateKey); + if (isCoolingDown(state?.lastFiredAt, trigger.cooldownMinutes, now)) + continue; + + await fire( + row, + stateKey, + "docker_event", + { + hostId: event.hostId, + container: event.container, + event: event.event, + }, + event.hostId, + ); + } + } + + async function onInternalEvent(event: InternalEvent): Promise { + const automations = await loadEnabledFor(event.userId); + const now = Date.now(); + + for (const { row, definition } of automations) { + const trigger = definition.trigger; + if (trigger?.kind !== "internal_event") continue; + if (trigger.event !== event.event) continue; + if ( + event.hostId !== undefined && + !selectorCoversHost(trigger.hostSelector, event.hostId) + ) { + continue; + } + + const stateKey = event.hostId ? String(event.hostId) : "global"; + const state = await repository.getTriggerState(row.id, stateKey); + if (isCoolingDown(state?.lastFiredAt, trigger.cooldownMinutes, now)) + continue; + + await fire( + row, + stateKey, + "internal_event", + { event: event.event, hostId: event.hostId, ...(event.details ?? {}) }, + event.hostId, + ); + } + } + + /** + * Hosts an enabled automation watches with the given trigger kind, with + * the owning user. Fleet and "all" selectors are not expanded: collecting + * for every host a user owns is far too costly. + */ + async function watchedHosts( + kind: "metric_threshold" | "docker_event", + ): Promise> { + const watched = new Map(); + try { + for (const row of await repository.listAllEnabled()) { + let definition: AutomationDefinition; + try { + definition = JSON.parse(row.definition) as AutomationDefinition; + } catch { + continue; + } + const trigger: Trigger | undefined = definition.trigger; + if (trigger?.kind !== kind) continue; + const selector = trigger.hostSelector; + if (selector?.kind === "host") { + watched.set(selector.hostId, row.userId); + } else if (selector?.kind === "hosts") { + for (const hostId of selector.hostIds) + watched.set(hostId, row.userId); + } + } + } catch { + return watched; } - } catch { return watched; } - return watched; + /** + * Subscribes to the topics the triggers read. Every listener is disposed + * with the plugin. Handlers are async and the bus is fire-and-forget, so a + * rejection is logged here rather than reaching whoever emitted. + */ + function subscribe(events: PluginEvents): void { + const on = ( + topic: string, + handler: (payload: T) => Promise | undefined, + ) => + events.on(topic, (payload) => { + void Promise.resolve(handler(payload as T)).catch((error: unknown) => + log.warn( + `Automation trigger for ${topic} failed: ${ + error instanceof Error ? error.message : String(error) + }`, + ), + ); + }); + + on("plugin.host-metrics.snapshot", onMetrics); + on("plugin.host-metrics.health-check", onHealthCheck); + on("host.status", onStatus); + on("internal.event", (event) => + event?.userId ? onInternalEvent(event) : undefined, + ); + // An SSH login seen by the terminal. + on<{ hostId: number; userId: string; sshUser?: string; fromIp?: string }>( + "host.login", + (login) => + login?.userId + ? onInternalEvent({ + event: "user_login", + userId: login.userId, + hostId: login.hostId, + details: { sshUser: login.sshUser, fromIp: login.fromIp }, + }) + : undefined, + ); + // Nothing arrives here while the tunnels plugin is off, which is the + // whole of that optional dependency. + on<{ userId?: string; hostId?: number; tunnelName?: string }>( + "plugin.tunnels.tunnel_disconnected", + (event) => + event?.userId + ? onInternalEvent({ + event: "tunnel_disconnected", + userId: event.userId, + hostId: event.hostId, + details: { tunnelName: event.tunnelName }, + }) + : undefined, + ); + on<{ + userId?: string; + automationId?: number; + automationName?: string; + runId?: number; + error?: string | null; + }>(TOPIC_AUTOMATION_FAILED, (event) => + event?.userId + ? onInternalEvent({ + event: "automation_failed", + userId: event.userId, + details: { + automationId: event.automationId, + automationName: event.automationName, + runId: event.runId, + error: event.error ?? null, + }, + }) + : undefined, + ); + } + + return { + onMetrics, + onStatus, + onHealthCheck, + onDockerEvent, + onInternalEvent, + watchedHosts, + subscribe, + }; } diff --git a/plugins/automations/src/backend/tunnels.ts b/plugins/automations/src/backend/tunnels.ts deleted file mode 100644 index 1716edcdb..000000000 --- a/plugins/automations/src/backend/tunnels.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { PluginServices } from "@termix/plugin-sdk/backend"; - -let current: PluginServices | null = null; - -/** Set in activate, cleared on deactivate. */ -export function setTunnelServices(services: PluginServices | null): void { - current = services; -} - -/** The tunnels plugin, as ctx.services.get("tunnels.access") returns it. */ -interface TunnelsAccess { - start: (name: string) => Promise; - stop: (name: string) => Promise; -} - -export interface TunnelActionResult { - ok: boolean; - /** True when the tunnels plugin is off or the user may not use it. */ - unavailable?: boolean; - error?: string; -} - -/** - * Connects or disconnects a saved tunnel as userId. Optional: without the - * tunnels plugin the step fails with a clear message rather than the plugin - * failing to activate. - */ -export async function runTunnelAction( - userId: string, - action: "connect" | "disconnect", - name: string, -): Promise { - if (!current) { - return { - ok: false, - unavailable: true, - error: "The tunnels plugin is not available", - }; - } - - let tunnels: TunnelsAccess; - try { - tunnels = current.get("tunnels.access", { userId }); - } catch { - return { - ok: false, - unavailable: true, - error: "The tunnels plugin is not available", - }; - } - - try { - if (action === "connect") await tunnels.start(name); - else await tunnels.stop(name); - return { ok: true }; - } catch (error) { - return { - ok: false, - unavailable: false, - error: error instanceof Error ? error.message : String(error), - }; - } -} diff --git a/plugins/automations/src/backend/validate.ts b/plugins/automations/src/backend/validate.ts new file mode 100644 index 000000000..2d06d7da9 --- /dev/null +++ b/plugins/automations/src/backend/validate.ts @@ -0,0 +1,153 @@ +import type { AutomationDefinition, Step, Trigger } from "../types.js"; +import { AUTOMATION_DEFINITION_VERSION } from "../types.js"; +import { isValidCron, isValidTimezone } from "./cron.js"; + +const TRIGGER_KINDS = new Set([ + "metric_threshold", + "host_status", + "health_check", + "schedule", + "docker_event", + "internal_event", + "webhook", +]); + +const STEP_TYPES = new Set([ + "notify", + "http", + "run_snippet", + "run_command", + "docker", + "tunnel", + "wol", + "wait", + "set_var", + "if", + "run_automation", + "stop", +]); + +const OPERATORS = new Set([ + ">", + "<", + ">=", + "<=", + "==", + "!=", + "contains", + "not_contains", + "changed", +]); + +export function isNonEmptyString(value: unknown): value is string { + return typeof value === "string" && value.trim().length > 0; +} + +/** + * Validates a definition before it is stored. The engine treats the stored + * blob as trusted, so everything it relies on is checked once here. + */ +export function validateDefinition(value: unknown): { + ok: boolean; + error?: string; + definition?: AutomationDefinition; +} { + if (typeof value !== "object" || value === null) { + return { ok: false, error: "Definition must be an object" }; + } + + const candidate = value as Partial; + const trigger = candidate.trigger as Trigger | undefined; + + if (!trigger || !TRIGGER_KINDS.has(trigger.kind)) { + return { ok: false, error: "Unknown or missing trigger kind" }; + } + + if (trigger.kind === "metric_threshold") { + if (!OPERATORS.has(trigger.operator)) { + return { ok: false, error: "Unknown comparison operator" }; + } + if (typeof trigger.value !== "number" || !Number.isFinite(trigger.value)) { + return { ok: false, error: "Threshold value must be a number" }; + } + if (!trigger.metric?.path) { + return { ok: false, error: "Trigger is missing a metric" }; + } + } + + if (trigger.kind === "schedule") { + const hasInterval = + typeof trigger.intervalSeconds === "number" && + trigger.intervalSeconds > 0; + const hasCron = isNonEmptyString(trigger.cron); + if (!hasInterval && !hasCron) { + return { ok: false, error: "Schedule needs an interval or a cron" }; + } + if (hasCron && !isValidCron(trigger.cron as string)) { + return { ok: false, error: "Cron expression is not valid" }; + } + if (hasInterval && (trigger.intervalSeconds as number) < 60) { + return { ok: false, error: "Interval must be at least 60 seconds" }; + } + if ( + isNonEmptyString(trigger.timezone) && + !isValidTimezone(trigger.timezone) + ) { + return { ok: false, error: "Time zone is not valid" }; + } + } + + const steps = candidate.steps; + if (!Array.isArray(steps)) { + return { ok: false, error: "Definition must include a steps array" }; + } + + const seen = new Set(); + const stepError = validateSteps(steps as Step[], seen); + if (stepError) return { ok: false, error: stepError }; + + return { + ok: true, + definition: { + version: candidate.version ?? AUTOMATION_DEFINITION_VERSION, + trigger, + steps: steps as Step[], + }, + }; +} + +function validateSteps(steps: Step[], seen: Set): string | null { + for (const step of steps) { + if (!step || typeof step !== "object") return "Step must be an object"; + if (!isNonEmptyString(step.id)) return "Every step needs an id"; + if (seen.has(step.id)) return `Duplicate step id: ${step.id}`; + seen.add(step.id); + if (!STEP_TYPES.has(step.type)) { + return `Unknown step type: ${step.type}`; + } + + if (step.type === "if") { + if (!step.condition || !OPERATORS.has(step.condition.operator)) { + return "Condition needs a valid operator"; + } + const thenError = validateSteps(step.then ?? [], seen); + if (thenError) return thenError; + const elseError = validateSteps(step.else ?? [], seen); + if (elseError) return elseError; + } + + if (step.type === "http" && !isNonEmptyString(step.url)) { + return "HTTP steps need a URL"; + } + if (step.type === "run_command" && !isNonEmptyString(step.command)) { + return "Command steps need a command"; + } + if (step.type === "wait" && typeof step.seconds !== "number") { + return "Wait steps need a number of seconds"; + } + if (step.type === "set_var" && !isNonEmptyString(step.name)) { + return "Variable steps need a name"; + } + } + return null; +} diff --git a/plugins/automations/src/frontend/AutomationsPanel.tsx b/plugins/automations/src/frontend/AutomationsPanel.tsx index ce5a315ef..8613d88b4 100644 --- a/plugins/automations/src/frontend/AutomationsPanel.tsx +++ b/plugins/automations/src/frontend/AutomationsPanel.tsx @@ -1,4 +1,3 @@ -import { getErrorMessage } from "@/lib/error-message.js"; import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "@termix/plugin-sdk/frontend"; import { toast } from "sonner"; @@ -12,30 +11,22 @@ import { Trash2, Workflow, } from "lucide-react"; -import { Button } from "@/components/button"; -import { Badge } from "@/components/badge"; import { - createAutomation, - deleteAutomation, - listAutomationRunSteps, - listAutomationRuns, - listAutomations, - runAutomation, - updateAutomation, + Badge, + Button, + NotificationChannelDialog, + deleteNotificationChannel, + getBasePath, + getNotificationChannels, + testNotificationChannel, + type NotificationChannel, +} from "@termix/plugin-sdk/ui"; +import { + useAutomationsApi, type AutomationRow, type AutomationRunRow, type AutomationRunStepRow, } from "./automations-api"; -import { - deleteNotificationChannel, - getNotificationChannels, - testNotificationChannel, - type NotificationChannel, -} from "@/api/notification-channels-api"; -import { NotificationChannelDialog } from "@/sidebar/NotificationChannelDialog"; -import { getSnippets } from "@/api/snippets-api"; -import { getSSHHosts } from "@/api/ssh-host-management-api"; -import { invokeAction } from "@termix/plugin-sdk/frontend"; import { AutomationEditor, @@ -47,6 +38,10 @@ import { type AutomationEditorOptions, } from "./automations/editor-types"; +function getErrorMessage(error: unknown, fallback: string): string { + return error instanceof Error ? error.message : fallback; +} + type PanelTab = "automations" | "runs" | "channels"; const STATUS_CLASS: Record = { @@ -58,25 +53,20 @@ const STATUS_CLASS: Record = { cancelled: "text-muted-foreground", }; -function timeAgo(iso: string | null): string { +type Translate = (key: string, options?: Record) => string; + +function timeAgo(iso: string | null, t: Translate): string { if (!iso) return ""; const ms = Date.now() - new Date(iso).getTime(); if (!Number.isFinite(ms)) return ""; + const base = "newUi.sidebar.automations.ago"; const sec = Math.floor(ms / 1000); - if (sec < 60) return `${sec}s ago`; + if (sec < 60) return t(`${base}.seconds`, { count: sec }); const min = Math.floor(sec / 60); - if (min < 60) return `${min}m ago`; + if (min < 60) return t(`${base}.minutes`, { count: min }); const hr = Math.floor(min / 60); - if (hr < 24) return `${hr}h ago`; - return `${Math.floor(hr / 24)}d ago`; -} - -/** Fleets from the fleets plugin, or none when it is not running. */ -async function listFleets(): Promise<{ id: number; name: string }[]> { - const result = await invokeAction("fleets.list"); - return Array.isArray(result) - ? (result as { id: number; name: string }[]) - : []; + if (hr < 24) return t(`${base}.hours`, { count: hr }); + return t(`${base}.days`, { count: Math.floor(hr / 24) }); } export function AutomationsPanel({ @@ -87,6 +77,7 @@ export function AutomationsPanel({ onEditingChange?: (editing: boolean) => void; }) { const { t } = useTranslation(); + const api = useAutomationsApi(); const base = "newUi.sidebar.automations"; const [tab, setTab] = useState("automations"); @@ -113,60 +104,34 @@ export function AutomationsPanel({ const loadAutomations = useCallback(async () => { setLoading(true); try { - setAutomations(await listAutomations()); + setAutomations(await api.list()); } catch { // A failed load leaves the previous list on screen. } finally { setLoading(false); } - }, []); + }, [api]); const loadRuns = useCallback(async () => { try { - setRuns(await listAutomationRuns({ limit: 100 })); + setRuns(await api.runs({ limit: 100 })); } catch { // Non-fatal. } - }, []); + }, [api]); const loadOptions = useCallback(async () => { - const [hosts, snippets, channelsResult, fleets] = await Promise.allSettled([ - getSSHHosts({ includeStatus: false }), - getSnippets(), + const [editorOptions, channelsResult] = await Promise.allSettled([ + api.editorOptions(), getNotificationChannels(), - listFleets(), ]); - if (channelsResult.status === "fulfilled") { setChannels(channelsResult.value); } - - setOptions({ - hosts: - hosts.status === "fulfilled" - ? hosts.value.map((host) => ({ - id: host.id, - name: host.name || host.ip, - })) - : [], - snippets: - snippets.status === "fulfilled" - ? snippets.value.map((s) => ({ - id: s.id, - // SnippetRow only types its id; the rest is narrowed by callers. - name: typeof s.name === "string" ? s.name : `#${s.id}`, - })) - : [], - channels: - channelsResult.status === "fulfilled" - ? channelsResult.value.map((c) => ({ id: c.id, name: c.name })) - : [], - fleets: - fleets.status === "fulfilled" - ? fleets.value.map((f) => ({ id: f.id, name: f.name })) - : [], - }); - }, []); + if (editorOptions.status === "fulfilled") { + setOptions(editorOptions.value); + } + }, [api]); useEffect(() => { if (!active) return; @@ -222,7 +187,7 @@ export function AutomationsPanel({ setSaving(true); try { if (editingId === null) { - const created = await createAutomation({ + const created = await api.create({ name: draft.name.trim(), description: draft.description || null, enabled: draft.enabled, @@ -236,7 +201,7 @@ export function AutomationsPanel({ setEditorOpen(false); } } else { - await updateAutomation(editingId, { + await api.update(editingId, { name: draft.name.trim(), description: draft.description || null, enabled: draft.enabled, @@ -257,7 +222,7 @@ export function AutomationsPanel({ async function remove(row: AutomationRow) { if (!confirm(t(`${base}.deleteConfirm`))) return; try { - await deleteAutomation(row.id); + await api.remove(row.id); toast.success(t(`${base}.deleted`)); await loadAutomations(); } catch (error) { @@ -267,7 +232,7 @@ export function AutomationsPanel({ async function run(row: AutomationRow, dryRun: boolean) { try { - const outcome = await runAutomation(row.id, { dryRun }); + const outcome = await api.run(row.id, { dryRun }); toast.success(t(`${base}.runFinished`, { status: outcome.status })); await loadAutomations(); if (tab === "runs") await loadRuns(); @@ -304,7 +269,7 @@ export function AutomationsPanel({ } setExpandedRun(runId); try { - setRunSteps(await listAutomationRunSteps(runId)); + setRunSteps(await api.runSteps(runId)); } catch { setRunSteps([]); } @@ -314,7 +279,7 @@ export function AutomationsPanel({ // host manager does, rather than opening a dialog over the app. if (editorOpen) { const webhookUrl = webhookToken - ? `${window.location.origin}/automations/webhook/${webhookToken}` + ? `${window.location.origin}${getBasePath()}/plugin-api/automations/webhook/${webhookToken}` : ""; return ( @@ -432,7 +397,7 @@ export function AutomationsPanel({ rel="noreferrer" className="ml-auto shrink-0 text-[10px] text-accent-brand hover:underline" > - {t("hosts.docsLink")} + {t(`${base}.docsLink`)} @@ -475,9 +440,20 @@ export function AutomationsPanel({ > {row.name} + {!!row.missingPlugins?.length && ( + + {t(`${base}.needsPlugin`, { + plugin: row.missingPlugins.join(", "), + })} + + )} {!row.enabled && ( - off + {t(`${base}.offBadge`)} )} @@ -490,7 +466,7 @@ export function AutomationsPanel({ ) : ""} {row.last_run_status - ? ` · ${timeAgo(row.last_run_at)}` + ? ` · ${timeAgo(row.last_run_at, t)}` : ""}