import { randomInt, timingSafeEqual, createHmac } from 'node:crypto'; // Unambiguous alphabet (no 0/O/1/I/L) for human-readable, dictation-safe keys. const ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789'; /** Generate a license key like ABCD-EFGH-JKMN-PQRS. */ export function generateKey(groups = 4, groupLen = 4) { const parts = []; for (let g = 0; g < groups; g++) { let s = ''; for (let i = 0; i < groupLen; i++) { s += ALPHABET[randomInt(0, ALPHABET.length)]; } parts.push(s); } return parts.join('-'); } /** * Normalize a domain for stable comparison: * strip scheme, path, port, leading www, lowercase. */ export function normalizeDomain(input) { if (!input || typeof input !== 'string') return ''; let d = input.trim().toLowerCase(); d = d.replace(/^[a-z]+:\/\//, ''); // scheme d = d.replace(/\/.*$/, ''); // path d = d.replace(/:.*$/, ''); // port d = d.replace(/^www\./, ''); // www return d; } /** Timing-safe string compare for tokens. */ export function safeEqual(a, b) { const ba = Buffer.from(String(a || '')); const bb = Buffer.from(String(b || '')); if (ba.length !== bb.length) return false; return timingSafeEqual(ba, bb); } export function nowIso() { return new Date().toISOString(); } /** True if an ISO date string is in the past. */ export function isExpired(expiresAt) { if (!expiresAt) return false; const t = Date.parse(expiresAt); if (Number.isNaN(t)) return false; return t < Date.now(); } /** Compare dotted version strings. Returns 1 if a>b, -1 if a parseInt(n, 10) || 0); const pb = String(b).split('.').map((n) => parseInt(n, 10) || 0); const len = Math.max(pa.length, pb.length); for (let i = 0; i < len; i++) { const x = pa[i] || 0; const y = pb[i] || 0; if (x > y) return 1; if (x < y) return -1; } return 0; } /** Sign a small JSON payload (download tokens). Returns "body.sig" (base64url). */ export function signToken(payload, secret) { const body = Buffer.from(JSON.stringify(payload)).toString('base64url'); const sig = createHmac('sha256', secret).update(body).digest('base64url'); return body + '.' + sig; } /** Verify a token from signToken. Returns the payload, or null if bad/expired. */ export function verifyToken(token, secret) { if (typeof token !== 'string' || !token.includes('.')) return null; const [body, sig] = token.split('.'); if (!body || !sig) return null; const expected = createHmac('sha256', secret).update(body).digest('base64url'); const a = Buffer.from(sig); const b = Buffer.from(expected); if (a.length !== b.length || !timingSafeEqual(a, b)) return null; try { const payload = JSON.parse(Buffer.from(body, 'base64url').toString('utf8')); if (payload.exp && Date.now() > payload.exp) return null; return payload; } catch { return null; } }