"""Environment-loaded configuration. All environment variables used anywhere in the backend live here so the routers / services never call ``os.environ.get`` directly. """ # Make sure .env is loaded no matter who imports us first. from app._env import load_app_env # noqa: E402 load_app_env() import os # --------------------------------------------------------------------------- # # Supabase # # --------------------------------------------------------------------------- # SUPABASE_URL = os.environ.get("SUPABASE_URL", "") SUPABASE_ANON_KEY = os.environ.get("SUPABASE_ANON_KEY", "") SUPABASE_JWT_SECRET = os.environ.get("SUPABASE_JWT_SECRET", "") # --------------------------------------------------------------------------- # # AI providers — endpoints, model names, keys # # --------------------------------------------------------------------------- # # Cloudflare Workers AI — secondary tier in the fallback chain. CLOUDFLARE_API_TOKEN = os.environ.get("CLOUDFLARE_API_TOKEN", "") CLOUDFLARE_ACCOUNT_ID = os.environ.get("CLOUDFLARE_ACCOUNT_ID", "") CLOUDFLARE_MODEL = os.environ.get("CLOUDFLARE_MODEL", "@cf/meta/llama-3.1-8b-instruct") CLOUDFLARE_URL = ( f"https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/run/{CLOUDFLARE_MODEL}" if CLOUDFLARE_ACCOUNT_ID else "" ) # Groq — secondary AI, also the legacy scenario/eval backend. GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "") GROQ_API_URL = "https://api.groq.com/openai/v1/chat/completions" GROQ_MODEL = os.environ.get("GROQ_MODEL", "llama-3.1-8b-instant") # NVIDIA — tertiary AI tier. NVIDIA_API_KEY = os.environ.get("NVIDIA_API_KEY", "") NVIDIA_MODEL = os.environ.get("NVIDIA_MODEL", "deepseek-ai/deepseek-v4-pro") NVIDIA_URL = "https://integrate.api.nvidia.com/v1/chat/completions" # Mistral — primary AI, runs first per user request. MISTRAL_API_KEY = os.environ.get("MISTRAL_API_KEY", "") MISTRAL_MODEL = os.environ.get("MISTRAL_MODEL", "mistral-large-latest") MISTRAL_API_URL = os.environ.get( "MISTRAL_API_URL", "https://api.mistral.ai/v1/chat/completions", ) # Supabase Edge Functions used as auth / XP proxies. SUPABASE_EDGE_URL = f"{SUPABASE_URL}/functions/v1" # --------------------------------------------------------------------------- # # Supabase Storage bucket for AI-generated challenge files # # --------------------------------------------------------------------------- # # Name of the public bucket ``app.services.file_storage`` uploads # ``fileToGenerate`` payloads to. The bucket's RLS policies live in # ``db/schema/010_challenge_files_bucket.sql``. CHALLENGE_FILES_BUCKET = "challenge-files" # --------------------------------------------------------------------------- # # Per-table canonical module (see AGENTS.md "Challenge Type vs Module") # # --------------------------------------------------------------------------- # # One entry per per-type challenges table — the ONLY value the # ``module`` column is allowed to take for rows going into that # table. ``normalize_row_module()`` enforces this at insert time so # AI generators can keep using topic-shaped names internally # (``xss``, ``sqli``, ``hash-cracking``) without violating the # post-011 CHECK constraints. TABLE_CANONICAL_MODULE: dict[str, str] = { "encryption_challenges": "crypto", "code_fixing_challenges": "code-fixing", "log_analysis_challenges": "log-analysis", "vulnerability_hunter_challenges": "vulnerability-hunter", "steganography_challenges": "steganography", "web_exploitation_challenges": "web-exploitation", } def normalize_row_module(table: str, row: dict) -> dict: """Enforce the canonical ``module`` value for a row going to ``table``. The migration 011 CHECK constraint rejects any row whose ``module`` column does not equal the canonical challenge type for its table. The AI generators used to populate ``module`` with a topic-shaped string (``xss``, ``hash-cracking``, ``web-security``, …) — we transparently rewrite that into: * ``module`` = canonical challenge type (so the DB accepts it) * ``topic`` = the original topic-shaped string (so the front-end can still show / filter on it) If the row already carries the canonical value, ``topic`` falls back to whatever ``module`` said. The function returns the mutated ``row`` (in place) for convenience; the return value is the same dict. Raises ``ValueError`` when called with an unknown table name — that always indicates a programming error, never a bad row. """ canonical = TABLE_CANONICAL_MODULE.get(table) if canonical is None: raise ValueError( f"normalize_row_module: unknown table {table!r}. " f"Add it to app.core.config.TABLE_CANONICAL_MODULE." ) original = row.get("module", "") # Move the original topic-shaped value into ``topic`` (if not # already set) and pin ``module`` to the canonical challenge type. if row.get("topic") is None or row.get("topic") == "": row["topic"] = original or canonical row["module"] = canonical return row