File size: 5,301 Bytes
80a4a65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
082c217
80a4a65
bedbcad
80a4a65
 
 
 
 
 
082c217
 
 
 
 
 
 
 
80a4a65
082c217
80a4a65
 
082c217
 
 
 
 
 
80a4a65
082c217
80a4a65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4871da9
4c41470
80a4a65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
"""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