import { randomBytes, randomUUID } from 'node:crypto'; import { authConfig, env, TERMS_VERSION } from './auth-config'; /** Zufaellige E-Mail-Adresse je Test (Spec 7: jeder Test nutzt eigene Adressen). */ export function randomEmail(prefix = 'e2e'): string { return `${prefix}-${Date.now().toString(36)}-${randomBytes(4).toString('hex')}@example.test`; } /** Zufaelliges, ausreichend langes Passwort (AUTH-CFG-05: mind. 10 Zeichen). */ export function randomPassword(): string { return `Pw-${randomBytes(8).toString('hex')}-x`; } /** Zufaellige IPv4 fuer `X-Forwarded-For` (nur Projekt `throttle`, nie gegen die CLI, R13). */ export function randomIp(): string { const octet = () => 1 + Math.floor(Math.random() * 253); return `${octet()}.${octet()}.${octet()}.${octet()}`; } export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); /** Wartet etwas mehr als AUTH-CFG-08 (1 s), damit Folgemails nicht an 429 scheitern. */ export const waitMailInterval = () => sleep(authConfig.maxFrequencySeconds * 1000 + 300); export interface GoTrueResponse { status: number; body: T; headers: Headers; /** Dauer in Millisekunden. */ ms: number; } export interface ClientOptions { /** Basis-URL inkl. `/auth/v1` (Standard: Kong). */ baseUrl?: string; /** Beim Zugriff ueber Kong noetig, direkt auf GoTrue nicht. */ apikey?: string | null; /** Zusatz-Header, z. B. `X-Forwarded-For` (nur `throttle`). */ headers?: Record; } /** Minimaler GoTrue-Client (REST, wie `supabase_flutter`). */ export class GoTrueClient { readonly baseUrl: string; private readonly apikey: string | null; private readonly extraHeaders: Record; constructor(opts: ClientOptions = {}) { this.baseUrl = opts.baseUrl ?? `${env.supabaseUrl}/auth/v1`; this.apikey = opts.apikey === undefined ? env.anonKey : opts.apikey; this.extraHeaders = opts.headers ?? {}; } async request( method: string, path: string, body?: unknown, token?: string, extra: Record = {}, ): Promise> { const headers: Record = { 'content-type': 'application/json', ...this.extraHeaders, ...extra }; if (this.apikey) headers.apikey = this.apikey; if (token) headers.authorization = `Bearer ${token}`; const started = Date.now(); const res = await fetch(`${this.baseUrl}${path}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), }); const text = await res.text(); let parsed: any = text; try { parsed = text ? JSON.parse(text) : null; } catch { /* Body ist kein JSON */ } return { status: res.status, body: parsed, headers: res.headers, ms: Date.now() - started }; } signUp(email: string, password: string, data: Record | null = validMetadata()) { return this.request('POST', '/signup', { email, password, ...(data ? { data } : {}) }); } signIn(email: string, password: string) { return this.request('POST', '/token?grant_type=password', { email, password }); } refresh(refreshToken: string) { return this.request('POST', '/token?grant_type=refresh_token', { refresh_token: refreshToken }); } recover(email: string) { return this.request('POST', '/recover', { email }); } resend(email: string, type: 'signup' = 'signup') { return this.request('POST', '/resend', { email, type }); } /** `verifyOTP(tokenHash, type)`. */ verify(tokenHash: string, type: 'email' | 'signup' | 'recovery') { return this.request('POST', '/verify', { token_hash: tokenHash, type }); } getUser(accessToken: string) { return this.request('GET', '/user', undefined, accessToken); } updateUser(accessToken: string, attributes: Record) { return this.request('PUT', '/user', attributes, accessToken); } /** Legt ein bestaetigtes Konto direkt an (Service-Role, nur lokal), fuer Tests, die die Anmeldung pruefen. */ async createConfirmedUser( email: string, password: string, data: Record = validMetadata(), ) { return this.request( 'POST', '/admin/users', { email, password, email_confirm: true, user_metadata: data }, env.serviceRoleKey, ); } } /** Gueltige Registrierungs-Metadaten (Spec 4.2). */ export function validMetadata(overrides: Record = {}): Record { const data: Record = { display_name: 'E2E Tester', age_confirmed: true, terms_accepted: true, terms_version: TERMS_VERSION, locale: 'en', ...overrides, }; for (const key of Object.keys(data)) if (data[key] === undefined) delete data[key]; return data; } export const uniqueId = () => randomUUID();