import crypto from 'node:crypto'; // Ohne leicht verwechselbare Zeichen (0/O, 1/I/L). const CODE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789'; export function randomCode(length = 8) { const bytes = crypto.randomBytes(length); let out = ''; for (let i = 0; i < length; i++) out += CODE_ALPHABET[bytes[i] % CODE_ALPHABET.length]; return out; } export function normalizeCode(input) { return String(input ?? '') .toUpperCase() .replace(/[^A-Z0-9]/g, ''); } export function formatCode(code) { return code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code; } export function randomToken() { return crypto.randomBytes(32).toString('base64url'); } export function randomId() { return crypto.randomBytes(9).toString('base64url'); } /** Alphanumerisches Passwort, sicher für Konsolenbefehle ohne Escaping. */ export function randomPassword(length = 16) { const alphabet = 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'; const bytes = crypto.randomBytes(length); let out = ''; for (let i = 0; i < length; i++) out += alphabet[bytes[i] % alphabet.length]; return out; } export function hashToken(token) { return crypto.createHash('sha256').update(String(token)).digest('hex'); } export function safeEqual(a, b) { const ba = Buffer.from(String(a)); const bb = Buffer.from(String(b)); return ba.length === bb.length && crypto.timingSafeEqual(ba, bb); } /** Einfacher In-Memory-Ratenbegrenzer (Fenster pro Schlüssel). */ export function createRateLimiter({ windowMs, max }) { const hits = new Map(); return function check(key) { const now = Date.now(); const entry = hits.get(key); if (!entry || now - entry.start > windowMs) { hits.set(key, { start: now, count: 1 }); return true; } entry.count += 1; if (hits.size > 10_000) { for (const [k, v] of hits) if (now - v.start > windowMs) hits.delete(k); } return entry.count <= max; }; }