from __future__ import annotations import os import stat from dataclasses import dataclass from pathlib import Path from typing import Any def _env_value(name: str, default: str | None) -> str | None: value = os.getenv(name) if value is None: return default value = value.strip() return value or default def _env_bool(name: str, default: bool = False) -> bool: value = os.getenv(name) if value is None: return default return value.strip().lower() in {"1", "true", "yes", "on"} def _secret_value(name: str, file_name: str) -> str | None: value = _env_value(name, None) if value: return value path = _env_value(file_name, None) if not path: return None try: with open(path, encoding="utf-8") as secret_file: return secret_file.read().strip() or None except OSError: return None def _private_secret_file(path: str | None) -> str | None: """Read a small owner-private regular secret file without following links.""" if not path: return None descriptor: int | None = None try: source = Path(path) before = source.lstat() if not source.is_absolute() or source.is_symlink() or not stat.S_ISREG(before.st_mode) or before.st_size > 512: return None descriptor = os.open( source, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_CLOEXEC", 0), ) opened = os.fstat(descriptor) if not stat.S_ISREG(opened.st_mode) or opened.st_size > 512: return None if os.name != "nt" and ( opened.st_uid != os.geteuid() or stat.S_IMODE(opened.st_mode) & 0o077 ): return None value = os.read(descriptor, 513) if len(value) > 512: return None return value.decode("utf-8").strip() or None except (OSError, UnicodeDecodeError): return None finally: if descriptor is not None: os.close(descriptor) def _env_list(name: str, default: list[str] | None = None) -> list[str]: value = os.getenv(name) if value is None: return list(default or []) return [item.strip() for item in value.split(",") if item.strip()] @dataclass class Settings: app_title: str = "Unraid AppOps Gateway" bind_host: str = "0.0.0.0" port: int = 1216 db_path: str = "/data/appops.db" data_dir: str = "/data" environment: str = "development" log_level: str = "info" static_dir: str = "dist" gpu_metrics_url: str = "" frontend_origin: str = "http://localhost:5173" voice_token: str | None = None voice_token_file: str | None = None operator_proxy_auth_enabled: bool = False operator_proxy_trusted_networks: list[str] | None = None auth_public_url: str = "" auth_admin_subjects: list[str] | None = None auth_admin_emails: list[str] | None = None authentik_issuer: str = "" authentik_entra_source: str = "" authentik_client_id: str = "" authentik_client_secret: str | None = None entra_tenant_id: str = "" entra_client_id: str = "" entra_client_secret: str | None = None deployment_mode: str = "private" verify_tls: bool = True secret_key: str | None = None secret_key_file: str | None = None secret_key_source: str = "missing" secret_rotation_enabled: bool = False discovery_interval_seconds: int = 0 discovery_history_runs: int = 0 notification_interval_seconds: int = 0 connector_health_interval_seconds: int = 0 observability_persist_interval_seconds: int = 0 restore_drill_interval_seconds: int = 0 mcp_canary_interval_seconds: int = 0 backup_retention: int = 20 mutation_rate_limit_per_minute: int = 120 mcp_enabled: bool = False mcp_token: str | None = None mcp_token_file: str | None = None mcp_allowed_hosts: list[str] | None = None runner_statistics_token_file: str | None = None n8n_enabled: bool = False n8n_token: str | None = None n8n_token_file: str | None = None apis: dict[str, str | bool | None] = None # populated in __post_init__ def __post_init__(self) -> None: self.app_title = _env_value("APP_OPS_APP_TITLE", self.app_title) or self.app_title self.bind_host = _env_value("APPOPS_BIND_HOST", self.bind_host) or self.bind_host self.port = int(_env_value("APPOPS_PORT", str(self.port)) or str(self.port)) self.db_path = _env_value("APPOPS_DB_PATH", self.db_path) or self.db_path self.data_dir = _env_value("APPOPS_DATA_DIR", self.data_dir) or self.data_dir self.environment = _env_value("APPOPS_ENV", self.environment) or self.environment default_discovery_interval = 120 if self.environment == "production" else self.discovery_interval_seconds self.discovery_interval_seconds = max(0, int(_env_value("APPOPS_DISCOVERY_INTERVAL_SECONDS", str(default_discovery_interval)) or "0")) default_discovery_history = 48 if self.environment == "production" else self.discovery_history_runs self.discovery_history_runs = max(0, int(_env_value("APPOPS_DISCOVERY_HISTORY_RUNS", str(default_discovery_history)) or "0")) production_default = self.environment == "production" self.notification_interval_seconds = max(0, int(_env_value("APPOPS_NOTIFICATION_INTERVAL_SECONDS", "300" if production_default else str(self.notification_interval_seconds)) or "0")) self.connector_health_interval_seconds = max(0, int(_env_value("APPOPS_CONNECTOR_HEALTH_INTERVAL_SECONDS", "300" if production_default else str(self.connector_health_interval_seconds)) or "0")) self.observability_persist_interval_seconds = max(0, int(_env_value("APPOPS_OBSERVABILITY_PERSIST_INTERVAL_SECONDS", "60" if production_default else str(self.observability_persist_interval_seconds)) or "0")) self.restore_drill_interval_seconds = max(0, int(_env_value("APPOPS_RESTORE_DRILL_INTERVAL_SECONDS", "86400" if production_default else str(self.restore_drill_interval_seconds)) or "0")) self.mcp_canary_interval_seconds = max(0, int(_env_value("APPOPS_MCP_CANARY_INTERVAL_SECONDS", "300" if production_default else str(self.mcp_canary_interval_seconds)) or "0")) self.backup_retention = max(1, min(200, int(_env_value("APPOPS_BACKUP_RETENTION", str(self.backup_retention)) or "20"))) self.mutation_rate_limit_per_minute = max(10, min(1000, int(_env_value("APPOPS_MUTATION_RATE_LIMIT", str(self.mutation_rate_limit_per_minute)) or "120"))) self.mcp_enabled = _env_bool("APPOPS_ENABLE_MCP", self.mcp_enabled) self.mcp_token = _env_value("APPOPS_MCP_TOKEN", self.mcp_token) self.mcp_token_file = _env_value("APPOPS_MCP_TOKEN_FILE", self.mcp_token_file) if not self.mcp_token and self.mcp_token_file: try: with open(self.mcp_token_file, encoding="utf-8") as token_file: self.mcp_token = token_file.read().strip() or None except OSError: self.mcp_token = None default_runner_statistics_token_file = str(Path(self.data_dir) / "runner-statistics-token") self.runner_statistics_token_file = _env_value( "APPOPS_RUNNER_STATISTICS_TOKEN_FILE", self.runner_statistics_token_file or default_runner_statistics_token_file, ) allowed_hosts = _env_value("APPOPS_MCP_ALLOWED_HOSTS", "localhost:*,127.0.0.1:*,198.18.10.150:*") or "" self.mcp_allowed_hosts = [item.strip() for item in allowed_hosts.split(",") if item.strip()] self.n8n_enabled = _env_bool("APPOPS_ENABLE_N8N", self.n8n_enabled) self.n8n_token = _env_value("APPOPS_N8N_TOKEN", self.n8n_token) self.n8n_token_file = _env_value("APPOPS_N8N_TOKEN_FILE", self.n8n_token_file) if not self.n8n_token and self.n8n_token_file: try: with open(self.n8n_token_file, encoding="utf-8") as token_file: self.n8n_token = token_file.read().strip() or None except OSError: self.n8n_token = None self.log_level = (_env_value("APPOPS_LOG_LEVEL", self.log_level) or self.log_level).lower() self.static_dir = _env_value("APPOPS_STATIC_DIR", self.static_dir) or self.static_dir self.gpu_metrics_url = _env_value("APPOPS_GPU_METRICS_URL", self.gpu_metrics_url) or "" self.frontend_origin = _env_value("APPOPS_FRONTEND_ORIGIN", None) or ( f"http://localhost:{self.port}" if self.environment == "production" else self.frontend_origin ) self.voice_token_file = _env_value("APP_OPS_VOICE_TOKEN_FILE", self.voice_token_file) self.voice_token = _secret_value("APP_OPS_VOICE_TOKEN", "APP_OPS_VOICE_TOKEN_FILE") or self.voice_token self.operator_proxy_auth_enabled = _env_bool("APPOPS_OPERATOR_PROXY_AUTH", self.operator_proxy_auth_enabled) self.operator_proxy_trusted_networks = _env_list("APPOPS_OPERATOR_PROXY_TRUSTED_NETWORKS", self.operator_proxy_trusted_networks) self.auth_public_url = (_env_value("APPOPS_AUTH_PUBLIC_URL", "") or "").rstrip("/") self.auth_admin_subjects = _env_list("APPOPS_AUTH_ADMIN_SUBJECTS") self.auth_admin_emails = _env_list("APPOPS_AUTH_ADMIN_EMAILS") self.authentik_issuer = _env_value("APPOPS_AUTHENTIK_ISSUER", "") or "" self.authentik_entra_source = _env_value("APPOPS_AUTHENTIK_ENTRA_SOURCE", "") or "" self.authentik_client_id = _env_value("APPOPS_AUTHENTIK_CLIENT_ID", "") or "" self.authentik_client_secret = _secret_value("APPOPS_AUTHENTIK_CLIENT_SECRET", "APPOPS_AUTHENTIK_CLIENT_SECRET_FILE") self.entra_tenant_id = _env_value("APPOPS_ENTRA_TENANT_ID", "") or "" self.entra_client_id = _env_value("APPOPS_ENTRA_CLIENT_ID", "") or "" self.entra_client_secret = _secret_value("APPOPS_ENTRA_CLIENT_SECRET", "APPOPS_ENTRA_CLIENT_SECRET_FILE") self.deployment_mode = (_env_value("APPOPS_DEPLOYMENT_MODE", self.deployment_mode) or self.deployment_mode).lower() self.verify_tls = _env_bool("APPOPS_VERIFY_TLS", self.verify_tls) self.secret_key_file = _env_value("APPOPS_SECRET_KEY_FILE", self.secret_key_file) self.secret_key = _env_value("APPOPS_SECRET_KEY", self.secret_key) self.secret_key_source = "env" if self.secret_key else "missing" if not self.secret_key and self.secret_key_file: try: with open(self.secret_key_file, encoding="utf-8") as key_file: self.secret_key = key_file.read().strip() or None self.secret_key_source = "file" if self.secret_key else "missing" except OSError: self.secret_key = None self.secret_key_source = "file_unreadable" self.secret_rotation_enabled = _env_bool("APPOPS_SECRET_ROTATION_ENABLED", self.secret_rotation_enabled) gitea_admin_token = _secret_value("GITEA_ADMIN_TOKEN", "GITEA_ADMIN_TOKEN_FILE") self.apis = { "PORTAINER_URL": _env_value("PORTAINER_URL", None), "PORTAINER_API_KEY": _env_value("PORTAINER_API_KEY", None), "PORTAINER_ENDPOINT_ID": _env_value("PORTAINER_ENDPOINT_ID", None), "PORTAINER_VERIFY_TLS": _env_bool("PORTAINER_VERIFY_TLS", self.verify_tls), "UNRAID_URL": _env_value("UNRAID_URL", None), "UNRAID_API_KEY": _env_value("UNRAID_API_KEY", None), "UNRAID_VERIFY_TLS": _env_bool("UNRAID_VERIFY_TLS", self.verify_tls), "GITEA_URL": _env_value("GITEA_URL", None), "GITEA_ADMIN_TOKEN": gitea_admin_token, "GITEA_VERIFY_TLS": _env_bool("GITEA_VERIFY_TLS", self.verify_tls), "GITEA_TIMEOUT_SECONDS": _env_value("GITEA_TIMEOUT_SECONDS", "8"), "GITEA_RUNNER_TARGETS_JSON": _env_value( "GITEA_RUNNER_TARGETS_JSON", '{}', ), # Deployment Control Plane V1 uses the private broker's dedicated # typed endpoint. The HMAC material is always resolved from the # encrypted Secret store by reference, never from this environment # preview or a deployment plan. "DEPLOYMENT_BROKER_URL": _env_value("DEPLOYMENT_BROKER_URL", None), "DEPLOYMENT_BROKER_VERIFY_TLS": _env_bool( "DEPLOYMENT_BROKER_VERIFY_TLS", self.verify_tls ), "DEPLOYMENT_BROKER_CA_FILE": _env_value( "DEPLOYMENT_BROKER_CA_FILE", None ), "DEPLOYMENT_BROKER_TIMEOUT_SECONDS": _env_value( "DEPLOYMENT_BROKER_TIMEOUT_SECONDS", "10" ), "SONARR_URL": _env_value("SONARR_URL", None), "SONARR_API_KEY": _env_value("SONARR_API_KEY", None), "SONARR_DEFAULT_ROOT_FOLDER": _env_value("SONARR_DEFAULT_ROOT_FOLDER", None), "SONARR_DEFAULT_QUALITY_PROFILE": _env_value("SONARR_DEFAULT_QUALITY_PROFILE", None), "SONARR_SEARCH_ON_ADD": _env_bool("SONARR_SEARCH_ON_ADD", True), "RADARR_URL": _env_value("RADARR_URL", None), "RADARR_API_KEY": _env_value("RADARR_API_KEY", None), "RADARR_DEFAULT_ROOT_FOLDER": _env_value("RADARR_DEFAULT_ROOT_FOLDER", None), "RADARR_DEFAULT_QUALITY_PROFILE": _env_value("RADARR_DEFAULT_QUALITY_PROFILE", None), "RADARR_DEFAULT_MINIMUM_AVAILABILITY": _env_value("RADARR_DEFAULT_MINIMUM_AVAILABILITY", "released"), "RADARR_SEARCH_ON_ADD": _env_bool("RADARR_SEARCH_ON_ADD", True), "RADARR_VERIFY_TLS": _env_bool("RADARR_VERIFY_TLS", self.verify_tls), "RADARR_TIMEOUT_SECONDS": _env_value("RADARR_TIMEOUT_SECONDS", "10"), "PROWLARR_URL": _env_value("PROWLARR_URL", None), "PROWLARR_API_KEY": _env_value("PROWLARR_API_KEY", None), "PROWLARR_VERIFY_TLS": _env_bool("PROWLARR_VERIFY_TLS", self.verify_tls), "PROWLARR_TIMEOUT_SECONDS": _env_value("PROWLARR_TIMEOUT_SECONDS", "10"), "DELUGE_URL": _env_value("DELUGE_URL", None), "DELUGE_PASSWORD": _env_value("DELUGE_PASSWORD", None), "DELUGE_VERIFY_TLS": _env_bool("DELUGE_VERIFY_TLS", self.verify_tls), "DELUGE_TIMEOUT_SECONDS": _env_value("DELUGE_TIMEOUT_SECONDS", "10"), "DELUGE_DEFAULT_HOST_ID": _env_value("DELUGE_DEFAULT_HOST_ID", None), "DELUGE_WEB_PATH": _env_value("DELUGE_WEB_PATH", "/json"), "PLEX_URL": _env_value("PLEX_URL", None), "PLEX_TOKEN": _env_value("PLEX_TOKEN", None), "PLEX_VERIFY_TLS": _env_bool("PLEX_VERIFY_TLS", self.verify_tls), "PLEX_TIMEOUT_SECONDS": _env_value("PLEX_TIMEOUT_SECONDS", "10"), "PLEX_DEFAULT_LIBRARY_ID": _env_value("PLEX_DEFAULT_LIBRARY_ID", None), "PLEX_MACHINE_IDENTIFIER": _env_value("PLEX_MACHINE_IDENTIFIER", None), "TAUTULLI_URL": _env_value("TAUTULLI_URL", None), "TAUTULLI_API_KEY": _env_value("TAUTULLI_API_KEY", None), "TAUTULLI_VERIFY_TLS": _env_bool("TAUTULLI_VERIFY_TLS", self.verify_tls), "TAUTULLI_TIMEOUT_SECONDS": _env_value("TAUTULLI_TIMEOUT_SECONDS", "10"), "NPM_URL": _env_value("NPM_URL", None), "NPM_IDENTITY": _env_value("NPM_IDENTITY", None), "NPM_SECRET": _env_value("NPM_SECRET", None), "NPM_VERIFY_TLS": _env_bool("NPM_VERIFY_TLS", True), "NPM_TIMEOUT_SECONDS": _env_value("NPM_TIMEOUT_SECONDS", "10"), "NPM_LETSENCRYPT_EMAIL": _env_value("NPM_LETSENCRYPT_EMAIL", None), "AUTHENTIK_URL": _env_value("AUTHENTIK_URL", None), "AUTHENTIK_TOKEN": _env_value("AUTHENTIK_TOKEN", None), "AUTHENTIK_VERIFY_TLS": _env_bool("AUTHENTIK_VERIFY_TLS", self.verify_tls), "AUTHENTIK_TIMEOUT_SECONDS": _env_value("AUTHENTIK_TIMEOUT_SECONDS", "10"), "AUTHENTIK_OUTPOST_URL": _env_value("AUTHENTIK_OUTPOST_URL", None), "AUTHENTIK_EXPECT_EMBEDDED_OUTPOST": _env_bool("AUTHENTIK_EXPECT_EMBEDDED_OUTPOST", True), # OIDC provisioning never guesses tenant-wide flow/mapping IDs. # Operators bind these once to existing, reviewed Authentik objects. "AUTHENTIK_OIDC_AUTHORIZATION_FLOW": _env_value("AUTHENTIK_OIDC_AUTHORIZATION_FLOW", None), "AUTHENTIK_OIDC_INVALIDATION_FLOW": _env_value("AUTHENTIK_OIDC_INVALIDATION_FLOW", None), "AUTHENTIK_OIDC_GROUP_CLAIM_MAPPING": _env_value("AUTHENTIK_OIDC_GROUP_CLAIM_MAPPING", None), # The reviewed group mapping must emit this exact claim. The # enrollment schema only accepts its bounded role contract. "AUTHENTIK_OIDC_ROLE_CLAIM": _env_value("AUTHENTIK_OIDC_ROLE_CLAIM", None), # Forge OIDC requires a pre-existing RSA keypair selected by ID; # the key material remains managed inside Authentik. "AUTHENTIK_FORGE_OIDC_SIGNING_KEY_ID": _env_value("AUTHENTIK_FORGE_OIDC_SIGNING_KEY_ID", None), # These IDs are loaded only from the encrypted Forge OIDC binding. # They deliberately have no environment or generic setup input. "AUTHENTIK_FORGE_OIDC_BINDING_VALID": False, "AUTHENTIK_FORGE_OIDC_PROVIDER_ID": None, "AUTHENTIK_FORGE_OIDC_OPENID_CLAIM_MAPPING": None, "AUTHENTIK_FORGE_OIDC_EMAIL_CLAIM_MAPPING": None, "AUTHENTIK_FORGE_OIDC_PROFILE_CLAIM_MAPPING": None, # The trusted Forge namespace owner is explicit and has no guess/default. "FORGE_PRIVATE_PACKAGE_OWNER": _env_value("FORGE_PRIVATE_PACKAGE_OWNER", None), "JELLYFIN_URL": _env_value("JELLYFIN_URL", None), "JELLYFIN_API_KEY": _env_value("JELLYFIN_API_KEY", None), "WIDEFROG_URL": _env_value("WIDEFROG_URL", None), "WIDEFROG_API_KEY": _env_value("WIDEFROG_API_KEY", None), "WIDEFROG_VERIFY_TLS": _env_bool("WIDEFROG_VERIFY_TLS", self.verify_tls), "WIDEFROG_TIMEOUT_SECONDS": _env_value("WIDEFROG_TIMEOUT_SECONDS", "10"), "HOME_ASSISTANT_URL": _env_value("HOME_ASSISTANT_URL", None), "HOME_ASSISTANT_TOKEN": _env_value("HOME_ASSISTANT_TOKEN", None), "HOME_ASSISTANT_VERIFY_TLS": _env_bool("HOME_ASSISTANT_VERIFY_TLS", self.verify_tls), "HOME_ASSISTANT_TIMEOUT_SECONDS": _env_value("HOME_ASSISTANT_TIMEOUT_SECONDS", "10"), "HOME_ASSISTANT_NOTIFY_DOMAIN": _env_value("HOME_ASSISTANT_NOTIFY_DOMAIN", "notify"), "HOME_ASSISTANT_NOTIFY_SERVICE": _env_value("HOME_ASSISTANT_NOTIFY_SERVICE", "notify"), "HOME_ASSISTANT_NOTIFY_TARGET": _env_value("HOME_ASSISTANT_NOTIFY_TARGET", None), "HOME_ASSISTANT_NOTIFICATION_TITLE_PREFIX": _env_value("HOME_ASSISTANT_NOTIFICATION_TITLE_PREFIX", "AppOps"), "HOME_ASSISTANT_DRY_RUN": _env_bool("HOME_ASSISTANT_DRY_RUN", False), "APPOPS_VERIFY_TLS": self.verify_tls, } def read_runner_statistics_token(self) -> str | None: """Read the fixed private credential at request time for no-restart provisioning.""" return _private_secret_file(self.runner_statistics_token_file) def redact_env_preview(self) -> dict[str, Any]: return { "APPOPS_BIND_HOST": self.bind_host, "APPOPS_PORT": self.port, "APPOPS_DB_PATH": self.db_path, "APPOPS_DATA_DIR": self.data_dir, "APPOPS_ENV": self.environment, "APPOPS_LOG_LEVEL": self.log_level, "APPOPS_STATIC_DIR": self.static_dir, "APPOPS_FRONTEND_ORIGIN": self.frontend_origin, "voice_token": bool(self.voice_token), "APPOPS_USER_AUTH_REQUIRED": True, "APPOPS_AUTH_PUBLIC_URL_CONFIGURED": bool(self.auth_public_url), "APPOPS_AUTHENTIK_OIDC_CONFIGURED": bool(self.authentik_issuer and self.authentik_client_id and self.authentik_client_secret), "APPOPS_ENTRA_OIDC_CONFIGURED": bool(self.entra_tenant_id and self.entra_client_id and self.entra_client_secret), "APPOPS_AUTH_ADMIN_BOOTSTRAP_CONFIGURED": bool(self.auth_admin_subjects or self.auth_admin_emails), "APPOPS_OPERATOR_PROXY_AUTH": self.operator_proxy_auth_enabled, "APPOPS_OPERATOR_PROXY_TRUSTED_NETWORKS_CONFIGURED": bool(self.operator_proxy_trusted_networks), "APPOPS_DEPLOYMENT_MODE": self.deployment_mode, "APPOPS_BACKUP_RETENTION": self.backup_retention, "APPOPS_ENABLE_MCP": self.mcp_enabled, "APPOPS_MCP_TOKEN_FILE": bool(self.mcp_token_file), "APPOPS_MCP_TOKEN_CONFIGURED": bool(self.mcp_token), "APPOPS_MCP_ALLOWED_HOSTS": list(self.mcp_allowed_hosts or []), "APPOPS_RUNNER_STATISTICS_TOKEN_FILE": bool(self.runner_statistics_token_file), "APPOPS_RUNNER_STATISTICS_TOKEN_CONFIGURED": bool(self.read_runner_statistics_token()), "APPOPS_ENABLE_N8N": self.n8n_enabled, "APPOPS_N8N_TOKEN_FILE": bool(self.n8n_token_file), "APPOPS_N8N_TOKEN_CONFIGURED": bool(self.n8n_token), "APPOPS_VERIFY_TLS": self.verify_tls, "APPOPS_SECRET_KEY": bool(self.secret_key), "APPOPS_SECRET_KEY_FILE": bool(self.secret_key_file), "APPOPS_SECRET_KEY_SOURCE": self.secret_key_source, "APPOPS_SECRET_ROTATION_ENABLED": self.secret_rotation_enabled, "PORTAINER_URL": bool(self.apis["PORTAINER_URL"]), "PORTAINER_VERIFY_TLS": bool(self.apis["PORTAINER_VERIFY_TLS"]), "UNRAID_URL": bool(self.apis["UNRAID_URL"]), "UNRAID_VERIFY_TLS": bool(self.apis["UNRAID_VERIFY_TLS"]), "GITEA_URL": bool(self.apis["GITEA_URL"]), "GITEA_ADMIN_TOKEN_CONFIGURED": bool(self.apis["GITEA_ADMIN_TOKEN"]), "GITEA_VERIFY_TLS": bool(self.apis["GITEA_VERIFY_TLS"]), "DEPLOYMENT_BROKER_URL": bool(self.apis["DEPLOYMENT_BROKER_URL"]), "DEPLOYMENT_BROKER_VERIFY_TLS": bool(self.apis["DEPLOYMENT_BROKER_VERIFY_TLS"]), "DEPLOYMENT_BROKER_CA_FILE": bool(self.apis["DEPLOYMENT_BROKER_CA_FILE"]), "DEPLOYMENT_BROKER_TIMEOUT_SECONDS": bool(self.apis["DEPLOYMENT_BROKER_TIMEOUT_SECONDS"]), "SONARR_URL": bool(self.apis["SONARR_URL"]), "SONARR_DEFAULT_ROOT_FOLDER": bool(self.apis["SONARR_DEFAULT_ROOT_FOLDER"]), "SONARR_DEFAULT_QUALITY_PROFILE": bool(self.apis["SONARR_DEFAULT_QUALITY_PROFILE"]), "SONARR_SEARCH_ON_ADD": bool(self.apis["SONARR_SEARCH_ON_ADD"]), "RADARR_URL": bool(self.apis["RADARR_URL"]), "RADARR_DEFAULT_ROOT_FOLDER": bool(self.apis["RADARR_DEFAULT_ROOT_FOLDER"]), "RADARR_DEFAULT_QUALITY_PROFILE": bool(self.apis["RADARR_DEFAULT_QUALITY_PROFILE"]), "RADARR_DEFAULT_MINIMUM_AVAILABILITY": bool(self.apis["RADARR_DEFAULT_MINIMUM_AVAILABILITY"]), "RADARR_SEARCH_ON_ADD": bool(self.apis["RADARR_SEARCH_ON_ADD"]), "RADARR_VERIFY_TLS": bool(self.apis["RADARR_VERIFY_TLS"]), "RADARR_TIMEOUT_SECONDS": bool(self.apis["RADARR_TIMEOUT_SECONDS"]), "PROWLARR_URL": bool(self.apis["PROWLARR_URL"]), "PROWLARR_VERIFY_TLS": bool(self.apis["PROWLARR_VERIFY_TLS"]), "PROWLARR_TIMEOUT_SECONDS": bool(self.apis["PROWLARR_TIMEOUT_SECONDS"]), "DELUGE_URL": bool(self.apis["DELUGE_URL"]), "DELUGE_VERIFY_TLS": bool(self.apis["DELUGE_VERIFY_TLS"]), "DELUGE_TIMEOUT_SECONDS": bool(self.apis["DELUGE_TIMEOUT_SECONDS"]), "DELUGE_DEFAULT_HOST_ID": bool(self.apis["DELUGE_DEFAULT_HOST_ID"]), "DELUGE_WEB_PATH": bool(self.apis["DELUGE_WEB_PATH"]), "PLEX_URL": bool(self.apis["PLEX_URL"]), "PLEX_VERIFY_TLS": bool(self.apis["PLEX_VERIFY_TLS"]), "PLEX_TIMEOUT_SECONDS": bool(self.apis["PLEX_TIMEOUT_SECONDS"]), "PLEX_DEFAULT_LIBRARY_ID": bool(self.apis["PLEX_DEFAULT_LIBRARY_ID"]), "PLEX_MACHINE_IDENTIFIER": bool(self.apis["PLEX_MACHINE_IDENTIFIER"]), "TAUTULLI_URL": bool(self.apis["TAUTULLI_URL"]), "TAUTULLI_VERIFY_TLS": bool(self.apis["TAUTULLI_VERIFY_TLS"]), "TAUTULLI_TIMEOUT_SECONDS": bool(self.apis["TAUTULLI_TIMEOUT_SECONDS"]), "NPM_URL": bool(self.apis["NPM_URL"]), "NPM_VERIFY_TLS": bool(self.apis["NPM_VERIFY_TLS"]), "NPM_TIMEOUT_SECONDS": bool(self.apis["NPM_TIMEOUT_SECONDS"]), "NPM_LETSENCRYPT_EMAIL": bool(self.apis["NPM_LETSENCRYPT_EMAIL"]), "AUTHENTIK_URL": bool(self.apis["AUTHENTIK_URL"]), "AUTHENTIK_VERIFY_TLS": bool(self.apis["AUTHENTIK_VERIFY_TLS"]), "AUTHENTIK_TIMEOUT_SECONDS": bool(self.apis["AUTHENTIK_TIMEOUT_SECONDS"]), "AUTHENTIK_OUTPOST_URL": bool(self.apis["AUTHENTIK_OUTPOST_URL"]), "AUTHENTIK_EXPECT_EMBEDDED_OUTPOST": bool(self.apis["AUTHENTIK_EXPECT_EMBEDDED_OUTPOST"]), "AUTHENTIK_OIDC_AUTHORIZATION_FLOW": bool(self.apis["AUTHENTIK_OIDC_AUTHORIZATION_FLOW"]), "AUTHENTIK_OIDC_INVALIDATION_FLOW": bool(self.apis["AUTHENTIK_OIDC_INVALIDATION_FLOW"]), "AUTHENTIK_OIDC_GROUP_CLAIM_MAPPING": bool(self.apis["AUTHENTIK_OIDC_GROUP_CLAIM_MAPPING"]), "AUTHENTIK_OIDC_ROLE_CLAIM": bool(self.apis["AUTHENTIK_OIDC_ROLE_CLAIM"]), "AUTHENTIK_FORGE_OIDC_SIGNING_KEY_ID": bool(self.apis["AUTHENTIK_FORGE_OIDC_SIGNING_KEY_ID"]), "AUTHENTIK_FORGE_OIDC_BINDING_VALID": bool(self.apis["AUTHENTIK_FORGE_OIDC_BINDING_VALID"]), "AUTHENTIK_FORGE_OIDC_PROVIDER_ID": bool(self.apis["AUTHENTIK_FORGE_OIDC_PROVIDER_ID"]), "AUTHENTIK_FORGE_OIDC_OPENID_CLAIM_MAPPING": bool(self.apis["AUTHENTIK_FORGE_OIDC_OPENID_CLAIM_MAPPING"]), "AUTHENTIK_FORGE_OIDC_EMAIL_CLAIM_MAPPING": bool(self.apis["AUTHENTIK_FORGE_OIDC_EMAIL_CLAIM_MAPPING"]), "AUTHENTIK_FORGE_OIDC_PROFILE_CLAIM_MAPPING": bool(self.apis["AUTHENTIK_FORGE_OIDC_PROFILE_CLAIM_MAPPING"]), "FORGE_PRIVATE_PACKAGE_OWNER": bool(self.apis["FORGE_PRIVATE_PACKAGE_OWNER"]), "JELLYFIN_URL": bool(self.apis["JELLYFIN_URL"]), "WIDEFROG_URL": bool(self.apis["WIDEFROG_URL"]), "WIDEFROG_VERIFY_TLS": bool(self.apis["WIDEFROG_VERIFY_TLS"]), "WIDEFROG_TIMEOUT_SECONDS": bool(self.apis["WIDEFROG_TIMEOUT_SECONDS"]), "HOME_ASSISTANT_URL": bool(self.apis["HOME_ASSISTANT_URL"]), "HOME_ASSISTANT_VERIFY_TLS": bool(self.apis["HOME_ASSISTANT_VERIFY_TLS"]), "HOME_ASSISTANT_TIMEOUT_SECONDS": bool(self.apis["HOME_ASSISTANT_TIMEOUT_SECONDS"]), "HOME_ASSISTANT_NOTIFY_DOMAIN": bool(self.apis["HOME_ASSISTANT_NOTIFY_DOMAIN"]), "HOME_ASSISTANT_NOTIFY_SERVICE": bool(self.apis["HOME_ASSISTANT_NOTIFY_SERVICE"]), "HOME_ASSISTANT_NOTIFY_TARGET": bool(self.apis["HOME_ASSISTANT_NOTIFY_TARGET"]), "HOME_ASSISTANT_NOTIFICATION_TITLE_PREFIX": bool(self.apis["HOME_ASSISTANT_NOTIFICATION_TITLE_PREFIX"]), "HOME_ASSISTANT_DRY_RUN": bool(self.apis["HOME_ASSISTANT_DRY_RUN"]), } settings = Settings()