diff --git a/app/client/modules/auth/routes/download-recovery-key.tsx b/app/client/modules/auth/routes/download-recovery-key.tsx
index 5ba88ec0..d188597c 100644
--- a/app/client/modules/auth/routes/download-recovery-key.tsx
+++ b/app/client/modules/auth/routes/download-recovery-key.tsx
@@ -62,7 +62,9 @@ export function DownloadRecoveryKeyPage({ passwordAuthSupported, hasPassword, us
}
setBlockedMessage(null);
- downloadResticPassword.mutate({ body: { password: passwordAuthSupported ? password : "" } });
+ downloadResticPassword.mutate({
+ body: { password: passwordAuthSupported ? password : "" },
+ });
};
const handleSkip = () => {
diff --git a/app/client/modules/settings/routes/settings.tsx b/app/client/modules/settings/routes/settings.tsx
index 66b738af..45dd439c 100644
--- a/app/client/modules/settings/routes/settings.tsx
+++ b/app/client/modules/settings/routes/settings.tsx
@@ -271,10 +271,12 @@ export function SettingsPage({
-
+ {permissions.hasRuntimeFeature("ssoManagement") && (
+
+ )}
@@ -488,7 +490,12 @@ export function SettingsPage({
-
+ {permissions.hasRuntimeFeature("apiKeys") && (
+
+ )}
diff --git a/app/drizzle/20260615163731_empty_vulcan/migration.sql b/app/drizzle/20260615163731_empty_vulcan/migration.sql
new file mode 100644
index 00000000..7255931e
--- /dev/null
+++ b/app/drizzle/20260615163731_empty_vulcan/migration.sql
@@ -0,0 +1 @@
+ALTER TABLE `sessions_table` ADD `auth_source` text DEFAULT 'browser-session' NOT NULL;
\ No newline at end of file
diff --git a/app/drizzle/20260615163731_empty_vulcan/snapshot.json b/app/drizzle/20260615163731_empty_vulcan/snapshot.json
new file mode 100644
index 00000000..f98f1248
--- /dev/null
+++ b/app/drizzle/20260615163731_empty_vulcan/snapshot.json
@@ -0,0 +1,3509 @@
+{
+ "version": "7",
+ "dialect": "sqlite",
+ "id": "0ad8fc87-d06c-416b-a98b-f79b79795955",
+ "prevIds": ["dc6150bc-d98b-4635-ae1c-ae23e5995eba"],
+ "ddl": [
+ {
+ "name": "account",
+ "entityType": "tables"
+ },
+ {
+ "name": "agents_table",
+ "entityType": "tables"
+ },
+ {
+ "name": "apikey",
+ "entityType": "tables"
+ },
+ {
+ "name": "app_metadata",
+ "entityType": "tables"
+ },
+ {
+ "name": "backup_schedule_mirrors_table",
+ "entityType": "tables"
+ },
+ {
+ "name": "backup_schedule_notifications_table",
+ "entityType": "tables"
+ },
+ {
+ "name": "backup_schedules_table",
+ "entityType": "tables"
+ },
+ {
+ "name": "invitation",
+ "entityType": "tables"
+ },
+ {
+ "name": "member",
+ "entityType": "tables"
+ },
+ {
+ "name": "notification_destinations_table",
+ "entityType": "tables"
+ },
+ {
+ "name": "organization",
+ "entityType": "tables"
+ },
+ {
+ "name": "passkey",
+ "entityType": "tables"
+ },
+ {
+ "name": "repositories_table",
+ "entityType": "tables"
+ },
+ {
+ "name": "repository_lock_waiters",
+ "entityType": "tables"
+ },
+ {
+ "name": "repository_locks",
+ "entityType": "tables"
+ },
+ {
+ "name": "sessions_table",
+ "entityType": "tables"
+ },
+ {
+ "name": "sso_provider",
+ "entityType": "tables"
+ },
+ {
+ "name": "tasks",
+ "entityType": "tables"
+ },
+ {
+ "name": "two_factor",
+ "entityType": "tables"
+ },
+ {
+ "name": "users_table",
+ "entityType": "tables"
+ },
+ {
+ "name": "verification",
+ "entityType": "tables"
+ },
+ {
+ "name": "volumes_table",
+ "entityType": "tables"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "account_id",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "provider_id",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "access_token",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "refresh_token",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id_token",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "access_token_expires_at",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "refresh_token_expires_at",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "scope",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "password",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "agents_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "organization_id",
+ "entityType": "columns",
+ "table": "agents_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "agents_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "kind",
+ "entityType": "columns",
+ "table": "agents_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'offline'",
+ "generated": null,
+ "name": "status",
+ "entityType": "columns",
+ "table": "agents_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'{}'",
+ "generated": null,
+ "name": "capabilities",
+ "entityType": "columns",
+ "table": "agents_table"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "last_seen_at",
+ "entityType": "columns",
+ "table": "agents_table"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "last_ready_at",
+ "entityType": "columns",
+ "table": "agents_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "agents_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "table": "agents_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "apikey"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'default'",
+ "generated": null,
+ "name": "config_id",
+ "entityType": "columns",
+ "table": "apikey"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "apikey"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "start",
+ "entityType": "columns",
+ "table": "apikey"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "reference_id",
+ "entityType": "columns",
+ "table": "apikey"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "prefix",
+ "entityType": "columns",
+ "table": "apikey"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "key",
+ "entityType": "columns",
+ "table": "apikey"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "refill_interval",
+ "entityType": "columns",
+ "table": "apikey"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "refill_amount",
+ "entityType": "columns",
+ "table": "apikey"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "last_refill_at",
+ "entityType": "columns",
+ "table": "apikey"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": "true",
+ "generated": null,
+ "name": "enabled",
+ "entityType": "columns",
+ "table": "apikey"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "rate_limit_enabled",
+ "entityType": "columns",
+ "table": "apikey"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "rate_limit_time_window",
+ "entityType": "columns",
+ "table": "apikey"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "rate_limit_max",
+ "entityType": "columns",
+ "table": "apikey"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": "0",
+ "generated": null,
+ "name": "request_count",
+ "entityType": "columns",
+ "table": "apikey"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "remaining",
+ "entityType": "columns",
+ "table": "apikey"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "last_request",
+ "entityType": "columns",
+ "table": "apikey"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "expires_at",
+ "entityType": "columns",
+ "table": "apikey"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "apikey"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "table": "apikey"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "permissions",
+ "entityType": "columns",
+ "table": "apikey"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "metadata",
+ "entityType": "columns",
+ "table": "apikey"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "key",
+ "entityType": "columns",
+ "table": "app_metadata"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "value",
+ "entityType": "columns",
+ "table": "app_metadata"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "app_metadata"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "table": "app_metadata"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": true,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "backup_schedule_mirrors_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "schedule_id",
+ "entityType": "columns",
+ "table": "backup_schedule_mirrors_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "repository_id",
+ "entityType": "columns",
+ "table": "backup_schedule_mirrors_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "true",
+ "generated": null,
+ "name": "enabled",
+ "entityType": "columns",
+ "table": "backup_schedule_mirrors_table"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "last_copy_at",
+ "entityType": "columns",
+ "table": "backup_schedule_mirrors_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "last_copy_status",
+ "entityType": "columns",
+ "table": "backup_schedule_mirrors_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "last_copy_error",
+ "entityType": "columns",
+ "table": "backup_schedule_mirrors_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "backup_schedule_mirrors_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "schedule_id",
+ "entityType": "columns",
+ "table": "backup_schedule_notifications_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "destination_id",
+ "entityType": "columns",
+ "table": "backup_schedule_notifications_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "notify_on_start",
+ "entityType": "columns",
+ "table": "backup_schedule_notifications_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "notify_on_success",
+ "entityType": "columns",
+ "table": "backup_schedule_notifications_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "true",
+ "generated": null,
+ "name": "notify_on_warning",
+ "entityType": "columns",
+ "table": "backup_schedule_notifications_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "true",
+ "generated": null,
+ "name": "notify_on_failure",
+ "entityType": "columns",
+ "table": "backup_schedule_notifications_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "backup_schedule_notifications_table"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": true,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "short_id",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "volume_id",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "repository_id",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "true",
+ "generated": null,
+ "name": "enabled",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "cron_expression",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "retention_policy",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'[]'",
+ "generated": null,
+ "name": "exclude_patterns",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'[]'",
+ "generated": null,
+ "name": "exclude_if_present",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'[]'",
+ "generated": null,
+ "name": "include_paths",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'[]'",
+ "generated": null,
+ "name": "include_patterns",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "last_backup_at",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "last_backup_status",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "last_backup_error",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "next_backup_at",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "one_file_system",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'[]'",
+ "generated": null,
+ "name": "custom_restic_params",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "backup_webhooks",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "0",
+ "generated": null,
+ "name": "sort_order",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "0",
+ "generated": null,
+ "name": "failure_retry_count",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "2",
+ "generated": null,
+ "name": "max_retries",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "900000",
+ "generated": null,
+ "name": "retry_delay",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "organization_id",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "invitation"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "organization_id",
+ "entityType": "columns",
+ "table": "invitation"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "email",
+ "entityType": "columns",
+ "table": "invitation"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "role",
+ "entityType": "columns",
+ "table": "invitation"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'pending'",
+ "generated": null,
+ "name": "status",
+ "entityType": "columns",
+ "table": "invitation"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "expires_at",
+ "entityType": "columns",
+ "table": "invitation"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "invitation"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "inviter_id",
+ "entityType": "columns",
+ "table": "invitation"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "member"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "organization_id",
+ "entityType": "columns",
+ "table": "member"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "table": "member"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'member'",
+ "generated": null,
+ "name": "role",
+ "entityType": "columns",
+ "table": "member"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "member"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": true,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "notification_destinations_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "notification_destinations_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "true",
+ "generated": null,
+ "name": "enabled",
+ "entityType": "columns",
+ "table": "notification_destinations_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'unknown'",
+ "generated": null,
+ "name": "status",
+ "entityType": "columns",
+ "table": "notification_destinations_table"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "last_checked",
+ "entityType": "columns",
+ "table": "notification_destinations_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "last_error",
+ "entityType": "columns",
+ "table": "notification_destinations_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "type",
+ "entityType": "columns",
+ "table": "notification_destinations_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "config",
+ "entityType": "columns",
+ "table": "notification_destinations_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "notification_destinations_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "table": "notification_destinations_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "organization_id",
+ "entityType": "columns",
+ "table": "notification_destinations_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "organization"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "organization"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "slug",
+ "entityType": "columns",
+ "table": "organization"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "logo",
+ "entityType": "columns",
+ "table": "organization"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "organization"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "metadata",
+ "entityType": "columns",
+ "table": "organization"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "passkey"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "passkey"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "public_key",
+ "entityType": "columns",
+ "table": "passkey"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "table": "passkey"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "credential_id",
+ "entityType": "columns",
+ "table": "passkey"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "counter",
+ "entityType": "columns",
+ "table": "passkey"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "device_type",
+ "entityType": "columns",
+ "table": "passkey"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "backed_up",
+ "entityType": "columns",
+ "table": "passkey"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "transports",
+ "entityType": "columns",
+ "table": "passkey"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "passkey"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "aaguid",
+ "entityType": "columns",
+ "table": "passkey"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "short_id",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "provisioning_id",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "type",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "config",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'auto'",
+ "generated": null,
+ "name": "compression_mode",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'unknown'",
+ "generated": null,
+ "name": "status",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "last_checked",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "last_error",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "doctor_result",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "stats",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "stats_updated_at",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "upload_limit_enabled",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "real",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "1",
+ "generated": null,
+ "name": "upload_limit_value",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'Mbps'",
+ "generated": null,
+ "name": "upload_limit_unit",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "download_limit_enabled",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "real",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "1",
+ "generated": null,
+ "name": "download_limit_value",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'Mbps'",
+ "generated": null,
+ "name": "download_limit_unit",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "organization_id",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "repository_lock_waiters"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "repository_id",
+ "entityType": "columns",
+ "table": "repository_lock_waiters"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "type",
+ "entityType": "columns",
+ "table": "repository_lock_waiters"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "operation",
+ "entityType": "columns",
+ "table": "repository_lock_waiters"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "owner_id",
+ "entityType": "columns",
+ "table": "repository_lock_waiters"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "requested_at",
+ "entityType": "columns",
+ "table": "repository_lock_waiters"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "expires_at",
+ "entityType": "columns",
+ "table": "repository_lock_waiters"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "heartbeat_at",
+ "entityType": "columns",
+ "table": "repository_lock_waiters"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "repository_locks"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "repository_id",
+ "entityType": "columns",
+ "table": "repository_locks"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "type",
+ "entityType": "columns",
+ "table": "repository_locks"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "operation",
+ "entityType": "columns",
+ "table": "repository_locks"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "owner_id",
+ "entityType": "columns",
+ "table": "repository_locks"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "acquired_at",
+ "entityType": "columns",
+ "table": "repository_locks"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "expires_at",
+ "entityType": "columns",
+ "table": "repository_locks"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "heartbeat_at",
+ "entityType": "columns",
+ "table": "repository_locks"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "sessions_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "table": "sessions_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "token",
+ "entityType": "columns",
+ "table": "sessions_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "expires_at",
+ "entityType": "columns",
+ "table": "sessions_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "sessions_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "table": "sessions_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "ip_address",
+ "entityType": "columns",
+ "table": "sessions_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "user_agent",
+ "entityType": "columns",
+ "table": "sessions_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "impersonated_by",
+ "entityType": "columns",
+ "table": "sessions_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "active_organization_id",
+ "entityType": "columns",
+ "table": "sessions_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'browser-session'",
+ "generated": null,
+ "name": "auth_source",
+ "entityType": "columns",
+ "table": "sessions_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "sso_provider"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "provider_id",
+ "entityType": "columns",
+ "table": "sso_provider"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "organization_id",
+ "entityType": "columns",
+ "table": "sso_provider"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "table": "sso_provider"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "issuer",
+ "entityType": "columns",
+ "table": "sso_provider"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "domain",
+ "entityType": "columns",
+ "table": "sso_provider"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "auto_link_matching_emails",
+ "entityType": "columns",
+ "table": "sso_provider"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "oidc_config",
+ "entityType": "columns",
+ "table": "sso_provider"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "saml_config",
+ "entityType": "columns",
+ "table": "sso_provider"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "sso_provider"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "table": "sso_provider"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "tasks"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "organization_id",
+ "entityType": "columns",
+ "table": "tasks"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "kind",
+ "entityType": "columns",
+ "table": "tasks"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "status",
+ "entityType": "columns",
+ "table": "tasks"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "resource_type",
+ "entityType": "columns",
+ "table": "tasks"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "resource_id",
+ "entityType": "columns",
+ "table": "tasks"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "target_agent_id",
+ "entityType": "columns",
+ "table": "tasks"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "input",
+ "entityType": "columns",
+ "table": "tasks"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "progress",
+ "entityType": "columns",
+ "table": "tasks"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "result",
+ "entityType": "columns",
+ "table": "tasks"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "error",
+ "entityType": "columns",
+ "table": "tasks"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "cancellation_requested",
+ "entityType": "columns",
+ "table": "tasks"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "tasks"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "started_at",
+ "entityType": "columns",
+ "table": "tasks"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "table": "tasks"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "finished_at",
+ "entityType": "columns",
+ "table": "tasks"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "two_factor"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "secret",
+ "entityType": "columns",
+ "table": "two_factor"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "backup_codes",
+ "entityType": "columns",
+ "table": "two_factor"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "table": "two_factor"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "true",
+ "generated": null,
+ "name": "verified",
+ "entityType": "columns",
+ "table": "two_factor"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "username",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "password_hash",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "has_downloaded_restic_password",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'MM/DD/YYYY'",
+ "generated": null,
+ "name": "date_format",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'12h'",
+ "generated": null,
+ "name": "time_format",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "email",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "email_verified",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "image",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "display_username",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "two_factor_enabled",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'user'",
+ "generated": null,
+ "name": "role",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "banned",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "ban_reason",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "ban_expires",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "verification"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "identifier",
+ "entityType": "columns",
+ "table": "verification"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "value",
+ "entityType": "columns",
+ "table": "verification"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "expires_at",
+ "entityType": "columns",
+ "table": "verification"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "verification"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "table": "verification"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": true,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "short_id",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "provisioning_id",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "type",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'unmounted'",
+ "generated": null,
+ "name": "status",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "last_error",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "last_health_check",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "config",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "true",
+ "generated": null,
+ "name": "auto_remount",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'local'",
+ "generated": null,
+ "name": "agent_id",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "organization_id",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "columns": ["user_id"],
+ "tableTo": "users_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "account_user_id_users_table_id_fk",
+ "entityType": "fks",
+ "table": "account"
+ },
+ {
+ "columns": ["organization_id"],
+ "tableTo": "organization",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_agents_table_organization_id_organization_id_fk",
+ "entityType": "fks",
+ "table": "agents_table"
+ },
+ {
+ "columns": ["reference_id"],
+ "tableTo": "users_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_apikey_reference_id_users_table_id_fk",
+ "entityType": "fks",
+ "table": "apikey"
+ },
+ {
+ "columns": ["schedule_id"],
+ "tableTo": "backup_schedules_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk",
+ "entityType": "fks",
+ "table": "backup_schedule_mirrors_table"
+ },
+ {
+ "columns": ["repository_id"],
+ "tableTo": "repositories_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk",
+ "entityType": "fks",
+ "table": "backup_schedule_mirrors_table"
+ },
+ {
+ "columns": ["schedule_id"],
+ "tableTo": "backup_schedules_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk",
+ "entityType": "fks",
+ "table": "backup_schedule_notifications_table"
+ },
+ {
+ "columns": ["destination_id"],
+ "tableTo": "notification_destinations_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk",
+ "entityType": "fks",
+ "table": "backup_schedule_notifications_table"
+ },
+ {
+ "columns": ["volume_id"],
+ "tableTo": "volumes_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "backup_schedules_table_volume_id_volumes_table_id_fk",
+ "entityType": "fks",
+ "table": "backup_schedules_table"
+ },
+ {
+ "columns": ["repository_id"],
+ "tableTo": "repositories_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "backup_schedules_table_repository_id_repositories_table_id_fk",
+ "entityType": "fks",
+ "table": "backup_schedules_table"
+ },
+ {
+ "columns": ["organization_id"],
+ "tableTo": "organization",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "backup_schedules_table_organization_id_organization_id_fk",
+ "entityType": "fks",
+ "table": "backup_schedules_table"
+ },
+ {
+ "columns": ["organization_id"],
+ "tableTo": "organization",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "invitation_organization_id_organization_id_fk",
+ "entityType": "fks",
+ "table": "invitation"
+ },
+ {
+ "columns": ["inviter_id"],
+ "tableTo": "users_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "invitation_inviter_id_users_table_id_fk",
+ "entityType": "fks",
+ "table": "invitation"
+ },
+ {
+ "columns": ["organization_id"],
+ "tableTo": "organization",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "member_organization_id_organization_id_fk",
+ "entityType": "fks",
+ "table": "member"
+ },
+ {
+ "columns": ["user_id"],
+ "tableTo": "users_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "member_user_id_users_table_id_fk",
+ "entityType": "fks",
+ "table": "member"
+ },
+ {
+ "columns": ["organization_id"],
+ "tableTo": "organization",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "notification_destinations_table_organization_id_organization_id_fk",
+ "entityType": "fks",
+ "table": "notification_destinations_table"
+ },
+ {
+ "columns": ["user_id"],
+ "tableTo": "users_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_passkey_user_id_users_table_id_fk",
+ "entityType": "fks",
+ "table": "passkey"
+ },
+ {
+ "columns": ["organization_id"],
+ "tableTo": "organization",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "repositories_table_organization_id_organization_id_fk",
+ "entityType": "fks",
+ "table": "repositories_table"
+ },
+ {
+ "columns": ["user_id"],
+ "tableTo": "users_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "sessions_table_user_id_users_table_id_fk",
+ "entityType": "fks",
+ "table": "sessions_table"
+ },
+ {
+ "columns": ["organization_id"],
+ "tableTo": "organization",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_sso_provider_organization_id_organization_id_fk",
+ "entityType": "fks",
+ "table": "sso_provider"
+ },
+ {
+ "columns": ["user_id"],
+ "tableTo": "users_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "SET NULL",
+ "nameExplicit": false,
+ "name": "fk_sso_provider_user_id_users_table_id_fk",
+ "entityType": "fks",
+ "table": "sso_provider"
+ },
+ {
+ "columns": ["organization_id"],
+ "tableTo": "organization",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_tasks_organization_id_organization_id_fk",
+ "entityType": "fks",
+ "table": "tasks"
+ },
+ {
+ "columns": ["user_id"],
+ "tableTo": "users_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "two_factor_user_id_users_table_id_fk",
+ "entityType": "fks",
+ "table": "two_factor"
+ },
+ {
+ "columns": ["organization_id"],
+ "tableTo": "organization",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "volumes_table_organization_id_organization_id_fk",
+ "entityType": "fks",
+ "table": "volumes_table"
+ },
+ {
+ "columns": ["schedule_id", "destination_id"],
+ "nameExplicit": false,
+ "name": "backup_schedule_notifications_table_schedule_id_destination_id_pk",
+ "entityType": "pks",
+ "table": "backup_schedule_notifications_table"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "account_pk",
+ "table": "account",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "agents_table_pk",
+ "table": "agents_table",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "apikey_pk",
+ "table": "apikey",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["key"],
+ "nameExplicit": false,
+ "name": "app_metadata_pk",
+ "table": "app_metadata",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "backup_schedule_mirrors_table_pk",
+ "table": "backup_schedule_mirrors_table",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "backup_schedules_table_pk",
+ "table": "backup_schedules_table",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "invitation_pk",
+ "table": "invitation",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "member_pk",
+ "table": "member",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "notification_destinations_table_pk",
+ "table": "notification_destinations_table",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "organization_pk",
+ "table": "organization",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "passkey_pk",
+ "table": "passkey",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "repositories_table_pk",
+ "table": "repositories_table",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "repository_lock_waiters_pk",
+ "table": "repository_lock_waiters",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "repository_locks_pk",
+ "table": "repository_locks",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "sessions_table_pk",
+ "table": "sessions_table",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "sso_provider_pk",
+ "table": "sso_provider",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "tasks_pk",
+ "table": "tasks",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "two_factor_pk",
+ "table": "two_factor",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "users_table_pk",
+ "table": "users_table",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "verification_pk",
+ "table": "verification",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "volumes_table_pk",
+ "table": "volumes_table",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ {
+ "value": "user_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "account_userId_idx",
+ "entityType": "indexes",
+ "table": "account"
+ },
+ {
+ "columns": [
+ {
+ "value": "organization_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "agents_table_organization_id_idx",
+ "entityType": "indexes",
+ "table": "agents_table"
+ },
+ {
+ "columns": [
+ {
+ "value": "status",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "agents_table_status_idx",
+ "entityType": "indexes",
+ "table": "agents_table"
+ },
+ {
+ "columns": [
+ {
+ "value": "config_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "apikey_configId_idx",
+ "entityType": "indexes",
+ "table": "apikey"
+ },
+ {
+ "columns": [
+ {
+ "value": "reference_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "apikey_referenceId_idx",
+ "entityType": "indexes",
+ "table": "apikey"
+ },
+ {
+ "columns": [
+ {
+ "value": "key",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "apikey_key_unique",
+ "entityType": "indexes",
+ "table": "apikey"
+ },
+ {
+ "columns": [
+ {
+ "value": "organization_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "invitation_organizationId_idx",
+ "entityType": "indexes",
+ "table": "invitation"
+ },
+ {
+ "columns": [
+ {
+ "value": "email",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "invitation_email_idx",
+ "entityType": "indexes",
+ "table": "invitation"
+ },
+ {
+ "columns": [
+ {
+ "value": "organization_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "member_organizationId_idx",
+ "entityType": "indexes",
+ "table": "member"
+ },
+ {
+ "columns": [
+ {
+ "value": "user_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "member_userId_idx",
+ "entityType": "indexes",
+ "table": "member"
+ },
+ {
+ "columns": [
+ {
+ "value": "organization_id",
+ "isExpression": false
+ },
+ {
+ "value": "user_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "member_org_user_uidx",
+ "entityType": "indexes",
+ "table": "member"
+ },
+ {
+ "columns": [
+ {
+ "value": "slug",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "organization_slug_uidx",
+ "entityType": "indexes",
+ "table": "organization"
+ },
+ {
+ "columns": [
+ {
+ "value": "user_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "passkey_userId_idx",
+ "entityType": "indexes",
+ "table": "passkey"
+ },
+ {
+ "columns": [
+ {
+ "value": "credential_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "passkey_credentialID_idx",
+ "entityType": "indexes",
+ "table": "passkey"
+ },
+ {
+ "columns": [
+ {
+ "value": "organization_id",
+ "isExpression": false
+ },
+ {
+ "value": "provisioning_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "repositories_table_org_provisioning_id_uidx",
+ "entityType": "indexes",
+ "table": "repositories_table"
+ },
+ {
+ "columns": [
+ {
+ "value": "repository_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "repository_lock_waiters_repository_id_idx",
+ "entityType": "indexes",
+ "table": "repository_lock_waiters"
+ },
+ {
+ "columns": [
+ {
+ "value": "expires_at",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "repository_lock_waiters_expires_at_idx",
+ "entityType": "indexes",
+ "table": "repository_lock_waiters"
+ },
+ {
+ "columns": [
+ {
+ "value": "owner_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "repository_lock_waiters_owner_id_idx",
+ "entityType": "indexes",
+ "table": "repository_lock_waiters"
+ },
+ {
+ "columns": [
+ {
+ "value": "repository_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "repository_locks_repository_id_idx",
+ "entityType": "indexes",
+ "table": "repository_locks"
+ },
+ {
+ "columns": [
+ {
+ "value": "expires_at",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "repository_locks_expires_at_idx",
+ "entityType": "indexes",
+ "table": "repository_locks"
+ },
+ {
+ "columns": [
+ {
+ "value": "owner_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "repository_locks_owner_id_idx",
+ "entityType": "indexes",
+ "table": "repository_locks"
+ },
+ {
+ "columns": [
+ {
+ "value": "user_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "sessionsTable_userId_idx",
+ "entityType": "indexes",
+ "table": "sessions_table"
+ },
+ {
+ "columns": [
+ {
+ "value": "organization_id",
+ "isExpression": false
+ },
+ {
+ "value": "kind",
+ "isExpression": false
+ },
+ {
+ "value": "resource_type",
+ "isExpression": false
+ },
+ {
+ "value": "resource_id",
+ "isExpression": false
+ },
+ {
+ "value": "status",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "tasks_org_kind_resource_status_idx",
+ "entityType": "indexes",
+ "table": "tasks"
+ },
+ {
+ "columns": [
+ {
+ "value": "organization_id",
+ "isExpression": false
+ },
+ {
+ "value": "status",
+ "isExpression": false
+ },
+ {
+ "value": "updated_at",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "tasks_org_status_updated_at_idx",
+ "entityType": "indexes",
+ "table": "tasks"
+ },
+ {
+ "columns": [
+ {
+ "value": "secret",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "twoFactor_secret_idx",
+ "entityType": "indexes",
+ "table": "two_factor"
+ },
+ {
+ "columns": [
+ {
+ "value": "user_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "twoFactor_userId_idx",
+ "entityType": "indexes",
+ "table": "two_factor"
+ },
+ {
+ "columns": [
+ {
+ "value": "identifier",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "verification_identifier_idx",
+ "entityType": "indexes",
+ "table": "verification"
+ },
+ {
+ "columns": [
+ {
+ "value": "agent_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "volumes_table_agent_id_idx",
+ "entityType": "indexes",
+ "table": "volumes_table"
+ },
+ {
+ "columns": [
+ {
+ "value": "organization_id",
+ "isExpression": false
+ },
+ {
+ "value": "provisioning_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "volumes_table_org_provisioning_id_uidx",
+ "entityType": "indexes",
+ "table": "volumes_table"
+ },
+ {
+ "columns": ["schedule_id", "repository_id"],
+ "nameExplicit": false,
+ "name": "backup_schedule_mirrors_table_schedule_id_repository_id_unique",
+ "entityType": "uniques",
+ "table": "backup_schedule_mirrors_table"
+ },
+ {
+ "columns": ["name", "organization_id"],
+ "nameExplicit": false,
+ "name": "volumes_table_name_organization_id_unique",
+ "entityType": "uniques",
+ "table": "volumes_table"
+ },
+ {
+ "columns": ["short_id"],
+ "nameExplicit": false,
+ "name": "backup_schedules_table_short_id_unique",
+ "entityType": "uniques",
+ "table": "backup_schedules_table"
+ },
+ {
+ "columns": ["short_id"],
+ "nameExplicit": false,
+ "name": "repositories_table_short_id_unique",
+ "entityType": "uniques",
+ "table": "repositories_table"
+ },
+ {
+ "columns": ["token"],
+ "nameExplicit": false,
+ "name": "sessions_table_token_unique",
+ "entityType": "uniques",
+ "table": "sessions_table"
+ },
+ {
+ "columns": ["provider_id"],
+ "nameExplicit": false,
+ "name": "sso_provider_provider_id_unique",
+ "entityType": "uniques",
+ "table": "sso_provider"
+ },
+ {
+ "columns": ["username"],
+ "nameExplicit": false,
+ "name": "users_table_username_unique",
+ "entityType": "uniques",
+ "table": "users_table"
+ },
+ {
+ "columns": ["email"],
+ "nameExplicit": false,
+ "name": "users_table_email_unique",
+ "entityType": "uniques",
+ "table": "users_table"
+ },
+ {
+ "columns": ["short_id"],
+ "nameExplicit": false,
+ "name": "volumes_table_short_id_unique",
+ "entityType": "uniques",
+ "table": "volumes_table"
+ }
+ ],
+ "renames": []
+}
diff --git a/app/lib/__tests__/permission-policy.test.ts b/app/lib/__tests__/permission-policy.test.ts
index a590a8c0..a0dd41ca 100644
--- a/app/lib/__tests__/permission-policy.test.ts
+++ b/app/lib/__tests__/permission-policy.test.ts
@@ -46,6 +46,40 @@ describe("permissions", () => {
evaluatePermission("ssoProvider.create", {
runtime: "server",
orgRole: "owner",
+ authSource: "desktop-session",
+ }),
+ ).toEqual({ allowed: false, reason: "authSource" });
+
+ expect(
+ evaluatePermission("ssoProvider.create", {
+ runtime: "server",
+ orgRole: "owner",
+ authSource: "api-key",
+ }),
+ ).toEqual({ allowed: false, reason: "authSource" });
+ });
+
+ test("allows recovery-key download for browser and desktop sessions but not API keys", () => {
+ expect(
+ evaluatePermission("recoveryKey.download", {
+ runtime: "desktop",
+ orgRole: "owner",
+ authSource: "desktop-session",
+ }).allowed,
+ ).toBe(true);
+
+ expect(
+ evaluatePermission("recoveryKey.download", {
+ runtime: "server",
+ orgRole: "owner",
+ authSource: "browser-session",
+ }).allowed,
+ ).toBe(true);
+
+ expect(
+ evaluatePermission("recoveryKey.download", {
+ runtime: "desktop",
+ orgRole: "owner",
authSource: "api-key",
}),
).toEqual({ allowed: false, reason: "authSource" });
@@ -80,5 +114,9 @@ describe("permissions", () => {
test("models runtime features independently from user roles", () => {
expect(hasRuntimeFeature("server", "remoteVolumeBackends")).toBe(true);
expect(hasRuntimeFeature("desktop", "remoteVolumeBackends")).toBe(false);
+ expect(hasRuntimeFeature("server", "apiKeys")).toBe(true);
+ expect(hasRuntimeFeature("desktop", "apiKeys")).toBe(false);
+ expect(hasRuntimeFeature("server", "passwordAuthentication")).toBe(true);
+ expect(hasRuntimeFeature("desktop", "passwordAuthentication")).toBe(false);
});
});
diff --git a/app/lib/permission-policy.ts b/app/lib/permission-policy.ts
index ba4aa800..55bd93b2 100644
--- a/app/lib/permission-policy.ts
+++ b/app/lib/permission-policy.ts
@@ -1,11 +1,13 @@
export type Runtime = "server" | "desktop";
-export type AuthSource = "browser-session" | "api-key";
+export type AuthSource = "browser-session" | "desktop-session" | "api-key";
export type RuntimeFeature =
| "instanceAdministration"
| "organizationAdministration"
| "ssoManagement"
- | "remoteVolumeBackends";
+ | "remoteVolumeBackends"
+ | "apiKeys"
+ | "passwordAuthentication";
export const RUNTIME_FEATURES = {
server: {
@@ -13,12 +15,16 @@ export const RUNTIME_FEATURES = {
organizationAdministration: true,
ssoManagement: true,
remoteVolumeBackends: true,
+ apiKeys: true,
+ passwordAuthentication: true,
},
desktop: {
instanceAdministration: false,
organizationAdministration: false,
ssoManagement: false,
remoteVolumeBackends: false,
+ apiKeys: false,
+ passwordAuthentication: false,
},
} as const satisfies Record>;
@@ -65,7 +71,7 @@ const PERMISSIONS = {
},
"recoveryKey.download": {
orgRoles: ["owner", "admin"],
- authSources: ["browser-session"],
+ authSources: ["browser-session", "desktop-session"],
},
} as const satisfies Record;
diff --git a/app/middleware/auth.ts b/app/middleware/auth.ts
index 3de945c8..439a2a1e 100644
--- a/app/middleware/auth.ts
+++ b/app/middleware/auth.ts
@@ -5,6 +5,7 @@ import { getCookie, getRequestHeaders } from "@tanstack/react-start/server";
import { authService } from "~/server/modules/auth/auth.service";
import { isAuthRoute } from "~/lib/auth-routes";
import { RECOVERY_KEY_DOWNLOAD_SKIPPED_COOKIE_NAME } from "~/lib/recovery-key-skip";
+import { invalidateAuthSession, isSessionAuthSourceAllowed } from "~/server/modules/auth/helpers";
export const authMiddleware = createMiddleware().server(async ({ next, request }) => {
const headers = getRequestHeaders();
@@ -21,6 +22,12 @@ export const authMiddleware = createMiddleware().server(async ({ next, request }
}
if (session?.user?.id) {
+ if (!isSessionAuthSourceAllowed(session.session.authSource)) {
+ await invalidateAuthSession(session.session.token);
+
+ throw redirect({ to: "/login" });
+ }
+
const hasSkippedRecoveryKeyDownload = getCookie(RECOVERY_KEY_DOWNLOAD_SKIPPED_COOKIE_NAME) === session.user.id;
if (
diff --git a/app/routes/(dashboard)/settings/index.tsx b/app/routes/(dashboard)/settings/index.tsx
index e18db934..14cdc1e3 100644
--- a/app/routes/(dashboard)/settings/index.tsx
+++ b/app/routes/(dashboard)/settings/index.tsx
@@ -21,10 +21,12 @@ export const Route = createFileRoute("/(dashboard)/settings/")({
queryKey: ["organization-context"],
queryFn: () => getOrganizationContext(),
});
- const userInvitationsPromise = context.queryClient.ensureQueryData({ ...getUserSsoInvitationsOptions() });
- const [authContext, userInvitations] = await Promise.all([authContextPromise, userInvitationsPromise]);
- await orgContextPromise;
+ const authContext = await authContextPromise;
+ const userInvitationsPromise = context.features.ssoManagement
+ ? context.queryClient.ensureQueryData({ ...getUserSsoInvitationsOptions() })
+ : Promise.resolve([]);
+ const [userInvitations] = await Promise.all([userInvitationsPromise, orgContextPromise]);
const shouldPrefetchOrgQueries = context.permissions["organizationSettings.view"];
diff --git a/app/server/app.ts b/app/server/app.ts
index 37035c28..60982d32 100644
--- a/app/server/app.ts
+++ b/app/server/app.ts
@@ -22,6 +22,7 @@ import { logger } from "@zerobyte/core/node";
import { config } from "./core/config";
import { auth } from "~/server/lib/auth";
import { db } from "./db/db";
+import { invalidateAuthSession, isSessionAuthSourceAllowed } from "./modules/auth/helpers";
const requestLogger = async (c: Context, next: Next) => {
const method = c.req.method;
@@ -98,7 +99,7 @@ export const createApp = () => {
.route("/api/v1/desktop", desktopController)
.route("/api/v1/events", eventsController);
- app.on(["POST", "GET"], "/api/auth/*", (c) => {
+ app.on(["POST", "GET"], "/api/auth/*", async (c) => {
const pathname = new URL(c.req.url).pathname;
if (pathname.startsWith("/api/auth/api-key/")) {
return c.json({ message: "API key management is only supported through API v1 routes" }, 404);
@@ -108,6 +109,13 @@ export const createApp = () => {
return c.json({ message: "API key authentication is only supported for API v1 routes" }, 401);
}
+ const session = await auth.api.getSession({ headers: c.req.raw.headers });
+ if (session && !isSessionAuthSourceAllowed(session.session.authSource)) {
+ await invalidateAuthSession(session.session.token, c);
+
+ return c.json({ message: "Invalid or expired session" }, 401);
+ }
+
return auth.handler(c.req.raw);
});
app.get("/api/v1/openapi.json", generalDescriptor(app));
diff --git a/app/server/db/schema.ts b/app/server/db/schema.ts
index 1081064d..b9caf9f3 100644
--- a/app/server/db/schema.ts
+++ b/app/server/db/schema.ts
@@ -15,6 +15,8 @@ import type { NotificationConfig, NotificationType } from "~/schemas/notificatio
import type { ShortId } from "~/server/utils/branded";
import { LOCAL_AGENT_ID } from "../modules/agents/constants";
+type SessionAuthSource = "browser-session" | "desktop-session";
+
/**
* Users Table
*/
@@ -65,6 +67,7 @@ export const sessionsTable = sqliteTable(
userAgent: text("user_agent"),
impersonatedBy: text("impersonated_by"),
activeOrganizationId: text("active_organization_id"),
+ authSource: text("auth_source").$type().notNull().default("browser-session"),
},
(table) => [index("sessionsTable_userId_idx").on(table.userId)],
);
diff --git a/app/server/lib/auth.ts b/app/server/lib/auth.ts
index 9273e6db..2cf72cec 100644
--- a/app/server/lib/auth.ts
+++ b/app/server/lib/auth.ts
@@ -155,6 +155,14 @@ export const auth = betterAuth({
},
session: {
modelName: "sessionsTable",
+ additionalFields: {
+ authSource: {
+ type: "string",
+ returned: true,
+ input: false,
+ defaultValue: "browser-session",
+ },
+ },
},
plugins: [
username({
diff --git a/app/server/lib/functions/current-permissions.ts b/app/server/lib/functions/current-permissions.ts
index c333a8f4..645c086d 100644
--- a/app/server/lib/functions/current-permissions.ts
+++ b/app/server/lib/functions/current-permissions.ts
@@ -3,6 +3,7 @@ import { getRequestHeaders } from "@tanstack/react-start/server";
import type { Permission, RuntimeFeature } from "~/lib/permission-policy";
import { resolvePermissions } from "~/server/core/request-context";
import { auth } from "~/server/lib/auth";
+import { getSessionAuthSource } from "~/server/modules/auth/helpers";
export const currentPermissionsQueryKey = ["current-permissions"] as const;
@@ -27,7 +28,7 @@ export const getCurrentPermissions = createServerFn({ method: "GET" }).handler(
const { permissions, features } = resolvePermissions({
instanceRole: session?.user?.role,
orgRole: activeMember?.role,
- authSource: session?.user ? ("browser-session" as const) : null,
+ authSource: session?.user ? getSessionAuthSource(session.session.authSource) : null,
});
return { permissions, features };
diff --git a/app/server/modules/api-keys/api-keys.controller.ts b/app/server/modules/api-keys/api-keys.controller.ts
index ad23e2d9..38ff70cc 100644
--- a/app/server/modules/api-keys/api-keys.controller.ts
+++ b/app/server/modules/api-keys/api-keys.controller.ts
@@ -9,21 +9,29 @@ import {
type ListApiKeysDto,
} from "./api-keys.dto";
import { MAX_API_KEYS_PER_USER, countActiveApiKeys, hasApiKey, listApiKeys } from "./api-keys.service";
-import { requireAuth, requireBrowserSession } from "../auth/auth.middleware";
+import { requireAuth, requireBrowserSession, requireRuntimeFeature } from "../auth/auth.middleware";
import { auth } from "~/server/lib/auth";
import { isPasswordAuthSupported, userHasPassword, verifyUserPassword } from "../auth/helpers";
export const apiKeysController = new Hono()
- .get("/api-keys", requireAuth, requireBrowserSession, getApiKeysDto, async (c) => {
- const user = c.get("user");
- const organizationId = c.get("organizationId");
- const apiKeys = await listApiKeys(user.id, organizationId);
+ .get(
+ "/api-keys",
+ requireAuth,
+ requireRuntimeFeature("apiKeys"),
+ requireBrowserSession,
+ getApiKeysDto,
+ async (c) => {
+ const user = c.get("user");
+ const organizationId = c.get("organizationId");
+ const apiKeys = await listApiKeys(user.id, organizationId);
- return c.json({ apiKeys, limit: MAX_API_KEYS_PER_USER });
- })
+ return c.json({ apiKeys, limit: MAX_API_KEYS_PER_USER });
+ },
+ )
.post(
"/api-keys",
requireAuth,
+ requireRuntimeFeature("apiKeys"),
requireBrowserSession,
createApiKeyDto,
validator("json", createApiKeyBody),
@@ -69,20 +77,27 @@ export const apiKeysController = new Hono()
});
},
)
- .delete("/api-keys/:keyId", requireAuth, requireBrowserSession, deleteApiKeyDto, async (c) => {
- const user = c.get("user");
- const organizationId = c.get("organizationId");
- const keyId = c.req.param("keyId");
+ .delete(
+ "/api-keys/:keyId",
+ requireAuth,
+ requireRuntimeFeature("apiKeys"),
+ requireBrowserSession,
+ deleteApiKeyDto,
+ async (c) => {
+ const user = c.get("user");
+ const organizationId = c.get("organizationId");
+ const keyId = c.req.param("keyId");
- const belongsToUserOrganization = await hasApiKey(user.id, organizationId, keyId);
- if (!belongsToUserOrganization) {
- return c.json({ message: "API key not found" }, 404);
- }
+ const belongsToUserOrganization = await hasApiKey(user.id, organizationId, keyId);
+ if (!belongsToUserOrganization) {
+ return c.json({ message: "API key not found" }, 404);
+ }
- await auth.api.deleteApiKey({
- headers: c.req.raw.headers,
- body: { keyId },
- });
+ await auth.api.deleteApiKey({
+ headers: c.req.raw.headers,
+ body: { keyId },
+ });
- return c.json({ success: true });
- });
+ return c.json({ success: true });
+ },
+ );
diff --git a/app/server/modules/auth/__tests__/auth.api-keys.test.ts b/app/server/modules/auth/__tests__/auth.api-keys.test.ts
index e4dd4eb2..58f903a0 100644
--- a/app/server/modules/auth/__tests__/auth.api-keys.test.ts
+++ b/app/server/modules/auth/__tests__/auth.api-keys.test.ts
@@ -74,6 +74,16 @@ async function createStoredApiKey(session: TestSession, organizationId = session
});
}
+async function createDesktopRuntimeSession() {
+ config.runtime = "desktop";
+ const session = await createTestSession();
+ await db
+ .update(sessionsTable)
+ .set({ authSource: "desktop-session" })
+ .where(eq(sessionsTable.token, session.session.token));
+ return session;
+}
+
describe("API keys", () => {
test("creates and lists API keys for the current organization after password confirmation", async () => {
const session = await createTestSession();
@@ -165,7 +175,7 @@ describe("API keys", () => {
});
});
- test("creates API keys without password confirmation when password auth is unsupported", async () => {
+ test("rejects browser sessions in desktop runtime", async () => {
config.runtime = "desktop";
const session = await createTestSession();
@@ -178,13 +188,21 @@ describe("API keys", () => {
body: JSON.stringify({ name: "Desktop key", password: "" }),
});
- expect(res.status).toBe(200);
- expect(await res.json()).toEqual(
- expect.objectContaining({
- name: "Desktop key",
- key: expect.stringMatching(/^zb_/),
- }),
- );
+ expect(res.status).toBe(401);
+ expect(await res.json()).toEqual({
+ message: "Invalid or expired session",
+ });
+ });
+
+ test("does not expose API key endpoints when the runtime feature is unavailable", async () => {
+ const session = await createDesktopRuntimeSession();
+
+ const res = await app.request("/api/v1/auth/api-keys", {
+ headers: session.headers,
+ });
+
+ expect(res.status).toBe(403);
+ expect(await res.json()).toEqual({ message: "Not available in desktop mode" });
});
test("enforces the per-user API key limit", async () => {
@@ -323,6 +341,33 @@ describe("API keys", () => {
}
});
+ test("does not expose SSO invitation browser flow routes when the runtime feature is unavailable", async () => {
+ const session = await createDesktopRuntimeSession();
+
+ const routes = [
+ { method: "GET", path: "/api/v1/auth/sso-invitations" },
+ {
+ method: "POST",
+ path: "/api/v1/auth/sso-invitations/test-invitation/verify",
+ body: { providerId: "test-provider" },
+ },
+ ];
+
+ for (const route of routes) {
+ const res = await app.request(route.path, {
+ method: route.method,
+ headers: {
+ ...session.headers,
+ "Content-Type": "application/json",
+ },
+ body: route.body ? JSON.stringify(route.body) : undefined,
+ });
+
+ expect(res.status).toBe(403);
+ expect(await res.json()).toEqual({ message: "Not available in desktop mode" });
+ }
+ });
+
test("does not allow API keys to mutate SSO admin resources", async () => {
const session = await createTestSessionWithOrgAdmin();
await addPassword(session);
diff --git a/app/server/modules/auth/__tests__/auth.helpers.test.ts b/app/server/modules/auth/__tests__/auth.helpers.test.ts
new file mode 100644
index 00000000..a128de7b
--- /dev/null
+++ b/app/server/modules/auth/__tests__/auth.helpers.test.ts
@@ -0,0 +1,21 @@
+import { beforeEach, describe, expect, test } from "vitest";
+import { config } from "~/server/core/config";
+import { isPasswordAuthSupported, isSessionAuthSourceAllowed } from "../helpers";
+
+describe("auth helpers", () => {
+ beforeEach(() => {
+ config.runtime = "server";
+ });
+
+ test("allows only the runtime session source and models password auth as a runtime feature", () => {
+ expect(isPasswordAuthSupported()).toBe(true);
+ expect(isSessionAuthSourceAllowed("browser-session")).toBe(true);
+ expect(isSessionAuthSourceAllowed("desktop-session")).toBe(false);
+
+ config.runtime = "desktop";
+
+ expect(isPasswordAuthSupported()).toBe(false);
+ expect(isSessionAuthSourceAllowed("browser-session")).toBe(false);
+ expect(isSessionAuthSourceAllowed("desktop-session")).toBe(true);
+ });
+});
diff --git a/app/server/modules/auth/auth.middleware.ts b/app/server/modules/auth/auth.middleware.ts
index 0039589d..02196e19 100644
--- a/app/server/modules/auth/auth.middleware.ts
+++ b/app/server/modules/auth/auth.middleware.ts
@@ -1,12 +1,12 @@
import { createMiddleware } from "hono/factory";
import { auth } from "~/server/lib/auth";
import { db } from "~/server/db/db";
-import { getPermission, withContext } from "~/server/core/request-context";
+import { getPermission, hasFeature, withContext } from "~/server/core/request-context";
import { getApiKeyOrganizationId } from "../api-keys/api-keys.service";
-import type { Permission } from "~/lib/permission-policy";
+import type { AuthSource, Permission, RuntimeFeature } from "~/lib/permission-policy";
+import { getSessionAuthSource, invalidateAuthSession, isSessionAuthSourceAllowed } from "./helpers";
const API_KEY_HEADER = "x-api-key";
-type AuthSource = "browser-session" | "api-key";
type AuthenticatedUser = {
id: string;
email: string;
@@ -31,7 +31,7 @@ declare module "hono" {
*/
export const requireAuth = createMiddleware(async (c, next) => {
const apiKeyValue = c.req.header(API_KEY_HEADER);
- const authSource: AuthSource = apiKeyValue ? "api-key" : "browser-session";
+ let authSource: AuthSource = apiKeyValue ? "api-key" : "browser-session";
let user: AuthenticatedUser | undefined;
let activeOrganizationId: string | null | undefined;
@@ -53,8 +53,15 @@ export const requireAuth = createMiddleware(async (c, next) => {
const sess = await auth.api.getSession({ headers: c.req.raw.headers });
if (sess) {
+ if (!isSessionAuthSourceAllowed(sess.session.authSource)) {
+ await invalidateAuthSession(sess.session.token, c);
+
+ return c.json({ message: "Invalid or expired session" }, 401);
+ }
+
user = sess.user;
activeOrganizationId = sess.session.activeOrganizationId;
+ authSource = getSessionAuthSource(sess.session.authSource);
}
}
@@ -100,6 +107,14 @@ export const requireAuth = createMiddleware(async (c, next) => {
});
export const requireBrowserSession = createMiddleware(async (c, next) => {
+ if (c.get("authSource") !== "browser-session") {
+ return c.json({ message: "Browser session required" }, 401);
+ }
+
+ await next();
+});
+
+export const requireUserSession = createMiddleware(async (c, next) => {
if (c.get("authSource") === "api-key") {
return c.json({ message: "Browser session required" }, 401);
}
@@ -107,6 +122,15 @@ export const requireBrowserSession = createMiddleware(async (c, next) => {
await next();
});
+export const requireRuntimeFeature = (feature: RuntimeFeature) =>
+ createMiddleware(async (c, next) => {
+ if (!hasFeature(feature)) {
+ return c.json({ message: "Not available in desktop mode" }, 403);
+ }
+
+ await next();
+ });
+
export const requirePermission = (permission: Permission) =>
createMiddleware(async (c, next) => {
const result = getPermission(permission);
@@ -142,7 +166,7 @@ export const requireOrgAdmin = createMiddleware(async (c, next) => {
});
export const requireAdmin = createMiddleware(async (c, next) => {
- if (c.get("authSource") === "api-key") {
+ if (c.get("authSource") !== "browser-session") {
return c.json({ message: "Browser session required" }, 401);
}
diff --git a/app/server/modules/auth/helpers.ts b/app/server/modules/auth/helpers.ts
index 1d7092cb..4e5ec48a 100644
--- a/app/server/modules/auth/helpers.ts
+++ b/app/server/modules/auth/helpers.ts
@@ -1,15 +1,38 @@
import { eq } from "drizzle-orm";
import { verifyPassword } from "better-auth/crypto";
+import type { Context } from "hono";
+import { deleteCookie } from "hono/cookie";
+import { hasRuntimeFeature } from "~/lib/permission-policy";
import { config } from "~/server/core/config";
import { db } from "~/server/db/db";
import { passkey, usersTable } from "~/server/db/schema";
+import { auth } from "~/server/lib/auth";
type PasswordVerificationBody = {
userId: string;
password: string;
};
-export const isPasswordAuthSupported = () => config.runtime !== "desktop";
+type SessionAuthSource = "browser-session" | "desktop-session";
+
+export const getSessionAuthSource = (authSource: string | null | undefined): SessionAuthSource =>
+ authSource === "desktop-session" ? "desktop-session" : "browser-session";
+
+export const isSessionAuthSourceAllowed = (authSource: string | null | undefined) =>
+ getSessionAuthSource(authSource) === (config.runtime === "desktop" ? "desktop-session" : "browser-session");
+
+export const invalidateAuthSession = async (token: string, c?: Context) => {
+ const authContext = await auth.$context;
+ await authContext.internalAdapter.deleteSession(token);
+
+ if (c) {
+ for (const cookie of Object.values(authContext.authCookies)) {
+ deleteCookie(c, cookie.name, cookie.attributes);
+ }
+ }
+};
+
+export const isPasswordAuthSupported = () => hasRuntimeFeature(config.runtime, "passwordAuthentication");
export const verifyUserPassword = async ({ password, userId }: PasswordVerificationBody) => {
const userAccount = await db.query.account.findFirst({
diff --git a/app/server/modules/desktop/__tests__/desktop.controller.test.ts b/app/server/modules/desktop/__tests__/desktop.controller.test.ts
index 8b04a9f6..84f5419d 100644
--- a/app/server/modules/desktop/__tests__/desktop.controller.test.ts
+++ b/app/server/modules/desktop/__tests__/desktop.controller.test.ts
@@ -1,11 +1,13 @@
import { afterEach, describe, expect, test } from "vitest";
import { eq } from "drizzle-orm";
+import { hashPassword } from "better-auth/crypto";
import { createApp } from "~/server/app";
import { config } from "~/server/core/config";
import { db } from "~/server/db/db";
-import { usersTable } from "~/server/db/schema";
+import { account, usersTable } from "~/server/db/schema";
+import { createTestSession } from "~/test/helpers/auth";
import { DESKTOP_LAUNCH_SECRET_HEADER } from "../desktop.service";
-import { DESKTOP_USER_EMAIL } from "../bootstrap";
+import { DESKTOP_USER_EMAIL } from "../constants";
const app = createApp();
const launchSecret = "s".repeat(32);
@@ -20,6 +22,32 @@ const useDesktopRuntime = () => {
config.desktop.launchSecret = launchSecret;
};
+const createDesktopSessionCookie = async () => {
+ useDesktopRuntime();
+
+ const res = await app.request("/api/v1/desktop/session", {
+ method: "POST",
+ headers: {
+ [DESKTOP_LAUNCH_SECRET_HEADER]: launchSecret,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ dateFormat: "DD/MM/YYYY", timeFormat: "24h" }),
+ });
+ const cookie = res.headers.get("set-cookie")?.split(";")[0];
+ const body = (await res.clone().json()) as { token: string };
+
+ expect(res.status).toBe(200);
+ expect(cookie).toBeTruthy();
+
+ return { cookie: cookie ?? "", token: body.token };
+};
+
+const expectSessionCookieCleared = (res: Response) => {
+ const setCookie = res.headers.get("set-cookie");
+ expect(setCookie).toContain("zerobyte.session_token=");
+ expect(setCookie).toContain("Max-Age=0");
+};
+
describe("desktopController", () => {
test("rejects desktop session requests without the launch secret", async () => {
useDesktopRuntime();
@@ -46,7 +74,7 @@ describe("desktopController", () => {
expect(res.status).toBe(400);
});
- test("creates a normal session cookie when the launch secret is valid", async () => {
+ test("creates a desktop-scoped session cookie when the launch secret is valid", async () => {
useDesktopRuntime();
await db
.update(usersTable)
@@ -64,10 +92,112 @@ describe("desktopController", () => {
expect(res.status).toBe(200);
expect(res.headers.get("set-cookie")).toContain("zerobyte.session_token");
+ const body = (await res.clone().json()) as { token: string };
const desktopUser = await db.query.usersTable.findFirst({
where: { email: DESKTOP_USER_EMAIL },
});
+ const desktopAuthSession = await db.query.sessionsTable.findFirst({
+ where: { token: body.token },
+ });
expect(desktopUser?.hasDownloadedResticPassword).toBe(false);
+ expect(desktopAuthSession?.authSource).toBe("desktop-session");
+ });
+
+ test("rejects reserved desktop users that do not have the derived desktop credential", async () => {
+ useDesktopRuntime();
+ await db.delete(usersTable).where(eq(usersTable.email, DESKTOP_USER_EMAIL));
+
+ const userId = crypto.randomUUID();
+ try {
+ await db.insert(usersTable).values({
+ id: userId,
+ username: `desktop-collision-${crypto.randomUUID()}`,
+ name: "Desktop Collision",
+ email: DESKTOP_USER_EMAIL,
+ });
+ await db.insert(account).values({
+ id: crypto.randomUUID(),
+ accountId: DESKTOP_USER_EMAIL,
+ providerId: "credential",
+ userId,
+ password: await hashPassword("wrong-password"),
+ });
+
+ const res = await app.request("/api/v1/desktop/session", {
+ method: "POST",
+ headers: {
+ [DESKTOP_LAUNCH_SECRET_HEADER]: launchSecret,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ dateFormat: "DD/MM/YYYY", timeFormat: "24h" }),
+ });
+
+ expect(res.status).toBe(401);
+ expect(await db.query.sessionsTable.findFirst({ where: { userId } })).toBeUndefined();
+ } finally {
+ await db.delete(usersTable).where(eq(usersTable.email, DESKTOP_USER_EMAIL));
+ }
+ });
+
+ test("does not treat desktop sessions as browser sessions for admin routes", async () => {
+ const v1Session = await createDesktopSessionCookie();
+ config.runtime = "server";
+
+ const adminRes = await app.request("/api/v1/auth/admin-users", {
+ headers: {
+ Cookie: v1Session.cookie,
+ },
+ });
+
+ expect(adminRes.status).toBe(401);
+ expectSessionCookieCleared(adminRes);
+ expect(await db.query.sessionsTable.findFirst({ where: { token: v1Session.token } })).toBeUndefined();
+
+ const directSession = await createDesktopSessionCookie();
+ config.runtime = "server";
+ const directSessionRes = await app.request("/api/auth/get-session", {
+ headers: {
+ Cookie: directSession.cookie,
+ },
+ });
+
+ expect(directSessionRes.status).toBe(401);
+ expectSessionCookieCleared(directSessionRes);
+ expect(await db.query.sessionsTable.findFirst({ where: { token: directSession.token } })).toBeUndefined();
+
+ const betterAuthAdminSession = await createDesktopSessionCookie();
+ config.runtime = "server";
+ const betterAuthAdminRes = await app.request("/api/auth/admin/list-users", {
+ headers: {
+ Cookie: betterAuthAdminSession.cookie,
+ },
+ });
+
+ expect(betterAuthAdminRes.status).toBe(401);
+ expectSessionCookieCleared(betterAuthAdminRes);
+ expect(
+ await db.query.sessionsTable.findFirst({ where: { token: betterAuthAdminSession.token } }),
+ ).toBeUndefined();
+ });
+
+ test("does not allow browser sessions to self-mark as desktop sessions", async () => {
+ const session = await createTestSession();
+
+ const res = await app.request("/api/auth/update-session", {
+ method: "POST",
+ headers: {
+ ...session.headers,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ authSource: "desktop-session" }),
+ });
+
+ expect(res.status).toBe(400);
+
+ const storedSession = await db.query.sessionsTable.findFirst({
+ where: { token: session.session.token },
+ });
+ expect(storedSession?.authSource).toBe("browser-session");
});
});
diff --git a/app/server/modules/desktop/bootstrap.ts b/app/server/modules/desktop/bootstrap.ts
index 6aec98c5..0bec9e1b 100644
--- a/app/server/modules/desktop/bootstrap.ts
+++ b/app/server/modules/desktop/bootstrap.ts
@@ -1,16 +1,14 @@
import { eq } from "drizzle-orm";
+import { UnauthorizedError } from "http-errors-enhanced";
import type { DateFormatPreference, TimeFormatPreference } from "~/lib/datetime";
import { config } from "~/server/core/config";
import { db } from "~/server/db/db";
import { usersTable } from "~/server/db/schema";
+import { verifyUserPassword } from "~/server/modules/auth/helpers";
import { ensureDefaultOrg } from "~/server/lib/auth/helpers/create-default-org";
import { auth } from "~/server/lib/auth";
import { cryptoUtils } from "~/server/utils/crypto";
-
-export const DESKTOP_USER_EMAIL = "desktop@zerobyte.local";
-export const DESKTOP_USERNAME = "desktop-admin";
-
-export const getDesktopUserPassword = () => cryptoUtils.deriveSecret("zerobyte:desktop-user-password");
+import { DESKTOP_USER_EMAIL, DESKTOP_USERNAME } from "./constants";
type DesktopDateTimePreferences = {
dateFormat: DateFormatPreference;
@@ -22,8 +20,8 @@ export const ensureDesktopIdentity = async ({ dateFormat, timeFormat }: DesktopD
return;
}
- const password = await getDesktopUserPassword();
let user = await db.query.usersTable.findFirst({ where: { email: DESKTOP_USER_EMAIL } });
+ const password = await cryptoUtils.deriveSecret("zerobyte:desktop-user-password");
if (!user) {
await auth.api.signUpEmail({
@@ -40,6 +38,8 @@ export const ensureDesktopIdentity = async ({ dateFormat, timeFormat }: DesktopD
});
user = await db.query.usersTable.findFirst({ where: { email: DESKTOP_USER_EMAIL } });
+ } else if (!(await verifyUserPassword({ userId: user.id, password }))) {
+ throw new UnauthorizedError("Reserved desktop user is not trusted");
}
if (!user) {
@@ -52,4 +52,11 @@ export const ensureDesktopIdentity = async ({ dateFormat, timeFormat }: DesktopD
.update(usersTable)
.set({ role: "admin", emailVerified: true, updatedAt: new Date() })
.where(eq(usersTable.id, user.id));
+
+ const desktopUser = await db.query.usersTable.findFirst({ where: { id: user.id } });
+ if (!desktopUser) {
+ throw new Error("Failed to load desktop user");
+ }
+
+ return desktopUser;
};
diff --git a/app/server/modules/desktop/constants.ts b/app/server/modules/desktop/constants.ts
new file mode 100644
index 00000000..bce1cebf
--- /dev/null
+++ b/app/server/modules/desktop/constants.ts
@@ -0,0 +1,2 @@
+export const DESKTOP_USER_EMAIL = "desktop@zerobyte.local";
+export const DESKTOP_USERNAME = "desktop-admin";
diff --git a/app/server/modules/desktop/desktop.controller.ts b/app/server/modules/desktop/desktop.controller.ts
index 2fab87a5..412f89b5 100644
--- a/app/server/modules/desktop/desktop.controller.ts
+++ b/app/server/modules/desktop/desktop.controller.ts
@@ -1,6 +1,8 @@
import { Hono } from "hono";
import { createMiddleware } from "hono/factory";
+import { setSignedCookie } from "hono/cookie";
import { validator } from "hono-openapi";
+import { auth } from "~/server/lib/auth";
import { DESKTOP_LAUNCH_SECRET_HEADER, desktopService, verifyDesktopLaunchSecret } from "./desktop.service";
import { createDesktopSessionBody, createDesktopSessionDto } from "./desktop.dto";
@@ -15,6 +17,19 @@ export const desktopController = new Hono().post(
createDesktopSessionDto,
validator("json", createDesktopSessionBody),
async (c) => {
- return desktopService.createDesktopSessionResponse(c.req.valid("json"));
+ const { session, user } = await desktopService.createDesktopSession(c.req.valid("json"));
+ const authContext = await auth.$context;
+
+ await setSignedCookie(c, authContext.authCookies.sessionToken.name, session.token, authContext.secret, {
+ ...authContext.authCookies.sessionToken.attributes,
+ maxAge: authContext.sessionConfig.expiresIn,
+ });
+
+ return c.json({
+ redirect: false,
+ token: session.token,
+ url: undefined,
+ user,
+ });
},
);
diff --git a/app/server/modules/desktop/desktop.service.ts b/app/server/modules/desktop/desktop.service.ts
index fcb3003f..f2db174d 100644
--- a/app/server/modules/desktop/desktop.service.ts
+++ b/app/server/modules/desktop/desktop.service.ts
@@ -1,7 +1,7 @@
import { BadRequestError, UnauthorizedError } from "http-errors-enhanced";
import { config } from "~/server/core/config";
import { auth } from "~/server/lib/auth";
-import { DESKTOP_USER_EMAIL, ensureDesktopIdentity, getDesktopUserPassword } from "~/server/modules/desktop/bootstrap";
+import { ensureDesktopIdentity } from "~/server/modules/desktop/bootstrap";
import { cryptoUtils } from "~/server/utils/crypto";
import type { CreateDesktopSessionBody } from "./desktop.dto";
@@ -26,21 +26,23 @@ export const verifyDesktopLaunchSecret = (secret: string | undefined) => {
}
};
-const createDesktopSessionResponse = async (body: CreateDesktopSessionBody) => {
+const createDesktopSession = async (body: CreateDesktopSessionBody) => {
assertDesktopRuntime();
- await ensureDesktopIdentity(body);
- const password = await getDesktopUserPassword();
- return auth.api.signInEmail({
- body: {
- email: DESKTOP_USER_EMAIL,
- password,
- rememberMe: true,
- },
- asResponse: true,
- });
+ const user = await ensureDesktopIdentity(body);
+ if (!user) {
+ throw new Error("Failed to bootstrap desktop user");
+ }
+
+ const ctx = await auth.$context;
+ const session = await ctx.internalAdapter.createSession(user.id, false, { authSource: "desktop-session" }, true);
+ if (!session) {
+ throw new Error("Failed to create desktop session");
+ }
+
+ return { session, user };
};
export const desktopService = {
- createDesktopSessionResponse,
+ createDesktopSession,
};
diff --git a/app/server/modules/repositories/__tests__/repositories.controller.test.ts b/app/server/modules/repositories/__tests__/repositories.controller.test.ts
index 7de1322a..9a123cd4 100644
--- a/app/server/modules/repositories/__tests__/repositories.controller.test.ts
+++ b/app/server/modules/repositories/__tests__/repositories.controller.test.ts
@@ -3,12 +3,16 @@ import crypto from "node:crypto";
import { PassThrough } from "node:stream";
import { createApp } from "~/server/app";
import { db } from "~/server/db/db";
-import { repositoriesTable } from "~/server/db/schema";
+import { member, repositoriesTable, sessionsTable } from "~/server/db/schema";
import { generateShortId } from "~/server/utils/id";
import { createTestSession, getAuthHeaders } from "~/test/helpers/auth";
import type { RepositoryConfig } from "@zerobyte/core/restic";
import { restic } from "~/server/core/restic";
import { Effect } from "effect";
+import { systemService } from "~/server/modules/system/system.service";
+import { repositoriesService } from "../repositories.service";
+import { eq } from "drizzle-orm";
+import { config } from "~/server/core/config";
const app = createApp();
@@ -23,6 +27,7 @@ beforeEach(() => {
});
afterEach(() => {
+ config.runtime = "server";
vi.restoreAllMocks();
});
@@ -151,6 +156,31 @@ describe("repositories security", () => {
const body = await res.json();
expect(body.message).toBe("Invalid or expired session");
});
+
+ test("POST /api/v1/repositories/:shortId/exec should allow desktop sessions", async () => {
+ config.runtime = "desktop";
+ const desktopSession = await createTestSession();
+ await db.update(member).set({ role: "admin" }).where(eq(member.userId, desktopSession.user.id));
+ await db
+ .update(sessionsTable)
+ .set({ authSource: "desktop-session" })
+ .where(eq(sessionsTable.token, desktopSession.session.token));
+ vi.spyOn(systemService, "isDevPanelEnabled").mockReturnValue(true);
+ const execSpy = vi.spyOn(repositoriesService, "execResticCommand").mockResolvedValue({ exitCode: 0 });
+
+ const res = await app.request("/api/v1/repositories/test-repo/exec", {
+ method: "POST",
+ headers: {
+ ...desktopSession.headers,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ command: "version" }),
+ });
+
+ expect(res.status).toBe(200);
+ await res.text();
+ expect(execSpy).toHaveBeenCalled();
+ });
});
describe("information disclosure", () => {
diff --git a/app/server/modules/repositories/repositories.controller.ts b/app/server/modules/repositories/repositories.controller.ts
index c6fe2297..66f656d5 100644
--- a/app/server/modules/repositories/repositories.controller.ts
+++ b/app/server/modules/repositories/repositories.controller.ts
@@ -54,7 +54,7 @@ import {
} from "./repositories.dto";
import { repositoriesService } from "./repositories.service";
import { getRcloneRemoteInfo, listRcloneRemotes } from "../../utils/rclone";
-import { requireAuth, requireBrowserSession, requireOrgAdmin } from "../auth/auth.middleware";
+import { requireAuth, requireOrgAdmin, requireUserSession } from "../auth/auth.middleware";
import { toMessage } from "~/server/utils/errors";
import { requireDevPanel } from "../auth/dev-panel.middleware";
import { getSnapshotDuration } from "../../utils/snapshots";
@@ -291,7 +291,7 @@ export const repositoriesController = new Hono()
})
.post(
"/:shortId/exec",
- requireBrowserSession,
+ requireUserSession,
requireDevPanel,
requireOrgAdmin,
devPanelExecDto,
diff --git a/app/server/modules/sso/__tests__/sso.registration.test.ts b/app/server/modules/sso/__tests__/sso.registration.test.ts
index f41d43b8..b4903ccc 100644
--- a/app/server/modules/sso/__tests__/sso.registration.test.ts
+++ b/app/server/modules/sso/__tests__/sso.registration.test.ts
@@ -2,7 +2,16 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { eq } from "drizzle-orm";
import { createApp } from "~/server/app";
import { db } from "~/server/db/db";
-import { account, invitation, member, organization, ssoProvider, usersTable, verification } from "~/server/db/schema";
+import {
+ account,
+ invitation,
+ member,
+ organization,
+ sessionsTable,
+ ssoProvider,
+ usersTable,
+ verification,
+} from "~/server/db/schema";
import { createTestSession, createTestSessionWithOrgAdmin } from "~/test/helpers/auth";
import { SSO_INVITATION_INTENT_COOKIE, ssoService } from "../sso.service";
import { config } from "~/server/core/config";
@@ -84,7 +93,11 @@ describe("SSO provider registration authorization", () => {
test("rejects provider registration when SSO management is unavailable", async () => {
config.runtime = "desktop";
- const { headers, organizationId } = await createTestSession();
+ const { headers, organizationId, session } = await createTestSession();
+ await db
+ .update(sessionsTable)
+ .set({ authSource: "desktop-session" })
+ .where(eq(sessionsTable.token, session.token));
const response = await app.request(ssoRegisterUrl, {
method: "POST",
diff --git a/app/server/modules/sso/middlewares/authorize-registration.ts b/app/server/modules/sso/middlewares/authorize-registration.ts
index 29833c43..b4400ea7 100644
--- a/app/server/modules/sso/middlewares/authorize-registration.ts
+++ b/app/server/modules/sso/middlewares/authorize-registration.ts
@@ -45,7 +45,7 @@ export const authorizeSsoRegistration = async (ctx: AuthMiddlewareContext) => {
organizationId,
userId: session.user.id,
orgRole: membership.role,
- authSource: "browser-session",
+ authSource: session.session.authSource,
},
() => getPermission("ssoProvider.create"),
);
diff --git a/app/server/modules/sso/sso.controller.ts b/app/server/modules/sso/sso.controller.ts
index 91d0badc..011bc46d 100644
--- a/app/server/modules/sso/sso.controller.ts
+++ b/app/server/modules/sso/sso.controller.ts
@@ -15,7 +15,7 @@ import {
updateSsoProviderAutoLinkingDto,
} from "./sso.dto";
import { SSO_INVITATION_INTENT_COOKIE, ssoService } from "./sso.service";
-import { requireAuth, requireBrowserSession, requirePermission } from "../auth/auth.middleware";
+import { requireAuth, requireBrowserSession, requirePermission, requireRuntimeFeature } from "../auth/auth.middleware";
import { auth } from "~/server/lib/auth";
import { mapAuthErrorToCode } from "./sso.errors";
import { config } from "~/server/core/config";
@@ -66,15 +66,23 @@ export const ssoController = new Hono()
})),
});
})
- .get("/sso-invitations", requireAuth, requireBrowserSession, getUserSsoInvitationsDto, async (c) => {
- const user = c.get("user");
- const invitations = await ssoService.listPendingInvitationsForUser(user.email);
+ .get(
+ "/sso-invitations",
+ requireAuth,
+ requireRuntimeFeature("ssoManagement"),
+ requireBrowserSession,
+ getUserSsoInvitationsDto,
+ async (c) => {
+ const user = c.get("user");
+ const invitations = await ssoService.listPendingInvitationsForUser(user.email);
- return c.json(invitations);
- })
+ return c.json(invitations);
+ },
+ )
.post(
"/sso-invitations/:invitationId/verify",
requireAuth,
+ requireRuntimeFeature("ssoManagement"),
requireBrowserSession,
startInvitationSsoVerificationDto,
validator("json", startInvitationSsoVerificationBody),
diff --git a/app/server/modules/system/__tests__/system.controller.test.ts b/app/server/modules/system/__tests__/system.controller.test.ts
index e567f027..44c22096 100644
--- a/app/server/modules/system/__tests__/system.controller.test.ts
+++ b/app/server/modules/system/__tests__/system.controller.test.ts
@@ -4,7 +4,7 @@ import { createTestSession, createTestSessionWithGlobalAdmin, getAuthHeaders } f
import { systemService } from "../system.service";
import * as authHelpers from "~/server/modules/auth/helpers";
import { db } from "~/server/db/db";
-import { organization, usersTable } from "~/server/db/schema";
+import { organization, sessionsTable, usersTable } from "~/server/db/schema";
import { eq } from "drizzle-orm";
import { cryptoUtils } from "~/server/utils/crypto";
import { config } from "~/server/core/config";
@@ -14,6 +14,15 @@ const app = createApp();
let session: Awaited>;
let globalAdminSession: Awaited>;
+const createDesktopTestSession = async () => {
+ const desktopAuthSession = await createTestSession();
+ await db
+ .update(sessionsTable)
+ .set({ authSource: "desktop-session" })
+ .where(eq(sessionsTable.token, desktopAuthSession.session.token));
+ return desktopAuthSession;
+};
+
beforeAll(async () => {
session = await createTestSession();
globalAdminSession = await createTestSessionWithGlobalAdmin();
@@ -51,10 +60,11 @@ describe("system security", () => {
test("returns desktop runtime and effective backend lists in desktop mode", async () => {
config.runtime = "desktop";
+ const desktopAuthSession = await createDesktopTestSession();
try {
const res = await app.request("/api/v1/system/info", {
- headers: session.headers,
+ headers: desktopAuthSession.headers,
});
expect(res.status).toBe(200);
@@ -93,7 +103,9 @@ describe("system security", () => {
describe("registration-status endpoint", () => {
test("GET /api/v1/system/registration-status should be accessible with valid session", async () => {
- const res = await app.request("/api/v1/system/registration-status", { headers: session.headers });
+ const res = await app.request("/api/v1/system/registration-status", {
+ headers: session.headers,
+ });
expect(res.status).toBe(200);
const body = await res.json();
expect(typeof body.enabled).toBe("boolean");
@@ -185,8 +197,9 @@ describe("system security", () => {
expect(await res.text()).toBe(resticPassword);
});
- test("should download restic password without password re-authentication in desktop mode", async () => {
+ test("should download restic password without password re-authentication for desktop sessions", async () => {
config.runtime = "desktop";
+ const desktopAuthSession = await createDesktopTestSession();
const { cryptoUtils: actualCryptoUtils } =
await vi.importActual("~/server/utils/crypto");
const resticPassword = "desktop-restic-password";
@@ -196,17 +209,17 @@ describe("system security", () => {
await db
.update(organization)
.set({ metadata: { resticPassword: encryptedResticPassword } })
- .where(eq(organization.id, session.organizationId));
+ .where(eq(organization.id, desktopAuthSession.organizationId));
await db
.update(usersTable)
.set({ hasDownloadedResticPassword: false })
- .where(eq(usersTable.id, session.user.id));
+ .where(eq(usersTable.id, desktopAuthSession.user.id));
vi.spyOn(cryptoUtils, "resolveSecret").mockImplementationOnce(actualCryptoUtils.resolveSecret);
const res = await app.request("/api/v1/system/restic-password", {
method: "POST",
headers: {
- ...session.headers,
+ ...desktopAuthSession.headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
@@ -218,10 +231,55 @@ describe("system security", () => {
expect(await res.text()).toBe(resticPassword);
expect(verifyPasswordSpy).not.toHaveBeenCalled();
- const updatedUser = await db.query.usersTable.findFirst({ where: { id: session.user.id } });
+ const updatedUser = await db.query.usersTable.findFirst({
+ where: { id: desktopAuthSession.user.id },
+ });
expect(updatedUser?.hasDownloadedResticPassword).toBe(true);
});
+ test("rejects browser sessions in desktop mode", async () => {
+ config.runtime = "desktop";
+ const browserSession = await createTestSession();
+ const verifyPasswordSpy = vi.spyOn(authHelpers, "verifyUserPassword").mockResolvedValueOnce(false);
+
+ const res = await app.request("/api/v1/system/restic-password", {
+ method: "POST",
+ headers: {
+ ...browserSession.headers,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ password: "wrong-password",
+ }),
+ });
+
+ expect(res.status).toBe(401);
+ expect(verifyPasswordSpy).not.toHaveBeenCalled();
+ const body = await res.json();
+ expect(body.message).toBe("Invalid or expired session");
+ });
+
+ test("rejects desktop sessions outside desktop mode", async () => {
+ const desktopAuthSession = await createDesktopTestSession();
+ const verifyPasswordSpy = vi.spyOn(authHelpers, "verifyUserPassword").mockResolvedValueOnce(false);
+
+ const res = await app.request("/api/v1/system/restic-password", {
+ method: "POST",
+ headers: {
+ ...desktopAuthSession.headers,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ password: "wrong-password",
+ }),
+ });
+
+ expect(res.status).toBe(401);
+ expect(verifyPasswordSpy).not.toHaveBeenCalled();
+ const body = await res.json();
+ expect(body.message).toBe("Invalid or expired session");
+ });
+
test("should return 400 for invalid payload on restic-password", async () => {
const res = await app.request("/api/v1/system/restic-password", {
method: "POST",
diff --git a/app/server/modules/system/system.controller.ts b/app/server/modules/system/system.controller.ts
index c9fb41cc..819b4d91 100644
--- a/app/server/modules/system/system.controller.ts
+++ b/app/server/modules/system/system.controller.ts
@@ -19,7 +19,7 @@ import { requireAuth, requirePermission } from "../auth/auth.middleware";
import { db } from "../../db/db";
import { usersTable } from "../../db/schema";
import { eq } from "drizzle-orm";
-import { isPasswordAuthSupported, userHasPassword, verifyUserPassword } from "../auth/helpers";
+import { userHasPassword, verifyUserPassword } from "../auth/helpers";
import { cryptoUtils } from "../../utils/crypto";
import { getOrganizationId } from "~/server/core/request-context";
@@ -63,20 +63,25 @@ export const systemController = new Hono()
const user = c.get("user");
const organizationId = getOrganizationId();
const body = c.req.valid("json");
- if (isPasswordAuthSupported()) {
+ if (c.get("authSource") !== "desktop-session") {
const hasPassword = await userHasPassword(user.id);
if (!hasPassword) {
return c.json({ message: "A local password is required to download the recovery key" }, 403);
}
- const isPasswordValid = await verifyUserPassword({ password: body.password, userId: user.id });
+ const isPasswordValid = await verifyUserPassword({
+ password: body.password,
+ userId: user.id,
+ });
if (!isPasswordValid) {
return c.json({ message: "Invalid password" }, 401);
}
}
try {
- const org = await db.query.organization.findFirst({ where: { id: organizationId } });
+ const org = await db.query.organization.findFirst({
+ where: { id: organizationId },
+ });
if (!org?.metadata?.resticPassword) {
return c.json({ message: "Organization Restic password not found" }, 404);