/** * Konstanten der Auth-Konfiguration (Spec #1 § 4.1, `docs/auth-konfiguration.md`). * * Diese Datei muss mit `supabase/config.toml` und `docs/auth-konfiguration.md` uebereinstimmen; * `tests/api/config.spec.ts` prueft das. Tests lesen Zahlen nur von hier und verweisen im Namen * auf die Zeilen-ID (z. B. `AUTH-CFG-07`). * * Werte, die lokal (Supabase CLI) und in Staging/Prod unterschiedlich sind, tragen hier den * LOKALEN Wert (die E2E-Tests laufen nur lokal bzw. in der CI gegen die CLI). */ /** Hosts, die als „lokal“ gelten (URL-Hostnamen; IPv6 steht in URL.hostname mit Klammern). */ const LOCAL_HOSTS = ['127.0.0.1', 'localhost', '[::1]']; /** * Zusaetzlich erlaubte Hosts fuer die CI (z. B. der Docker-Host `docker`), kommagetrennt in * E2E_ALLOWED_HOSTS. Nur setzen, wenn der Dienst ein Wegwerf-Stack ist. */ function allowedHosts(): string[] { const extra = (process.env.E2E_ALLOWED_HOSTS ?? '').split(',').map((h) => h.trim()).filter(Boolean); return [...LOCAL_HOSTS, ...extra]; } export function isLocalHost(hostname: string): boolean { return allowedHosts().includes(hostname.toLowerCase()); } /** Bricht ab, wenn die URL nicht auf einen lokalen (oder explizit erlaubten) Host zeigt. */ export function assertLocalUrl(name: string, value: string): string { const host = new URL(value).hostname; if (!isLocalHost(host)) { throw new Error(`${name}: Host "${host}" ist nicht lokal. Die E2E-Tests legen Testkonten an; ggf. E2E_ALLOWED_HOSTS setzen.`); } return value; } const DEMO_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0'; const DEMO_SERVICE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU'; const supabaseUrl = assertLocalUrl('SUPABASE_URL', process.env.SUPABASE_URL ?? 'http://127.0.0.1:54321'); const isDefaultLocal = ['127.0.0.1', 'localhost', '[::1]'].includes(new URL(supabaseUrl).hostname); function key(envName: string, demo: string): string { const v = process.env[envName]; if (v) return v; // Demo-Schluessel nur bei lokaler URL; sonst muss der Aufrufer den Schluessel setzen. if (!isDefaultLocal) throw new Error(`${envName} muss gesetzt sein, wenn SUPABASE_URL nicht lokal ist.`); return demo; } /** Umgebung (Standardwerte = lokale Demo-Werte der Supabase CLI, keine Secrets). */ export const env = { /** Kong, wie die App. */ supabaseUrl, /** Legacy-JWT-Anon-Key der CLI (oeffentlicher Demo-Wert, nur bei lokaler URL als Fallback). */ anonKey: key('SUPABASE_ANON_KEY', DEMO_ANON_KEY), /** Legacy-Service-Role-Key der CLI (oeffentlicher Demo-Wert, nur lokal). */ serviceRoleKey: key('SUPABASE_SERVICE_ROLE_KEY', DEMO_SERVICE_KEY), mailpitUrl: assertLocalUrl('MAILPIT_URL', process.env.MAILPIT_URL ?? 'http://127.0.0.1:54324'), dbUrl: process.env.E2E_DB_URL ?? 'postgresql://postgres:postgres@127.0.0.1:54322/postgres', /** Web-Build. Immer `localhost` (nicht 127.0.0.1): Die Mail-Links nutzen AUTH-CFG-01. */ webUrl: process.env.WEB_URL ?? 'http://localhost:3000', /** Zusaetzlicher GoTrue-Container mit AUTH-CFG-13 (Projekt `throttle`, Skript start-throttle-gotrue.sh). */ throttleGotrueUrl: process.env.THROTTLE_GOTRUE_URL, /** GoTrue-Container mit unerreichbarem SMTP (Projekt `spike`, Issue #37). */ spikeGotrueUrl: process.env.SPIKE_GOTRUE_URL, } as const; export const authConfig = { /** AUTH-CFG-01: Site-URL lokal. */ siteUrl: 'http://localhost:3000', /** AUTH-CFG-05: Passwort-Mindestlaenge. */ passwordMinLength: 10, /** AUTH-CFG-07: Gueltigkeit Bestaetigungs- und Reset-Link in Sekunden. */ otpExpirySeconds: 3600, /** AUTH-CFG-08: Mindestabstand zwischen Mails je Nutzer, lokal 1 s. */ maxFrequencySeconds: 1, /** AUTH-CFG-10: Burst des Login-Limiters je IP (Login #31 -> 429). */ loginBurst: 30, /** AUTH-CFG-11: Burst des OTP-Limiters (Signup, Recover, Resend) je IP. */ otpBurst: 30, /** AUTH-CFG-14: JWT-Laufzeit in Sekunden. */ jwtExpirySeconds: 3600, /** AUTH-CFG-15: Wiederverwendungsfenster der Refresh-Token-Rotation in Sekunden. */ refreshTokenReuseIntervalSeconds: 10, /** Anzeigename: Grenzen in Codepoints (Spec 3.2). */ displayNameMaxLength: 30, } as const; /** Mailbetreffe (AUTH-CFG-17/-18, `supabase/config.toml`). */ export const subjects = { confirmation: { de: 'Bestätige deine E-Mail-Adresse für Shuffle', en: 'Confirm your email address for Shuffle' }, recovery: { de: 'Passwort für Shuffle zurücksetzen', en: 'Reset your Shuffle password' }, } as const; /** Fehlercodes von GoTrue, auf die sich die Spec stuetzt (§ 4.3/4.4). */ export const errorCodes = { invalidCredentials: 'invalid_credentials', emailNotConfirmed: 'email_not_confirmed', weakPassword: 'weak_password', samePassword: 'same_password', otpExpired: 'otp_expired', overRequestRateLimit: 'over_request_rate_limit', overEmailSendRateLimit: 'over_email_send_rate_limit', userAlreadyExists: 'user_already_exists', } as const; /** Trigger-Fehlertexte bei signUp (HTTP 500, Body `{"code":"P0001","message":...}`, B0 (f)). */ export const triggerErrors = { displayNameInvalid: 'display_name_invalid', ageConfirmationRequired: 'age_confirmation_required', termsAcceptanceRequired: 'terms_acceptance_required', termsVersionRequired: 'terms_version_required', } as const; /** Version der Rechtstexte, die der Web-Build sendet (`app/env/ci.example.json`). */ export const TERMS_VERSION = process.env.TERMS_VERSION ?? '2026-11-draft';