/** * Short-code generation. * * Strategy (mirrors what the Go backend will do): * - "random" → base62 encoding of a monotonically increasing counter. This * yields the SHORTEST possible string that is still globally * unique (no collision checks, no wasted length). A counter of * N needs ceil(log62(N)) chars: 62^4 ≈ 14.7M, 62^5 ≈ 916M. * To avoid leaking sequence/volume, the counter is passed * through a reversible bit-scramble before encoding. * - "memorable" → 3 short, easy words joined by hyphens. * - "custom" → user supplied (validated, authenticated only). */ const BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; export function encodeBase62(num: number): string { if (num === 0) return BASE62[0]; let n = num; let out = ""; while (n > 0) { out = BASE62[n % 62] + out; n = Math.floor(n / 62); } return out; } /** * Reversible scramble so sequential counter values don't produce sequential * codes. Knuth multiplicative hash over a 31-bit space with a coprime * multiplier — keeps codes short while looking random. */ function scramble(id: number): number { const PRIME = 2654435761; // 2^32 golden-ratio prime const MOD = 0x7fffffff; // 2^31 - 1 return ((id + 1) * PRIME) % MOD; } export function codeFromCounter(counter: number): string { // Bump the base so even the first link gets a pleasant 4-char code. return encodeBase62(scramble(counter + 100_000)); } const CUSTOM_RE = /^[a-zA-Z0-9_-]{3,32}$/; export function validateCustomAlias(alias: string): string | null { if (!alias) return "Pick an alias for your link."; if (alias.length < 3) return "At least 3 characters."; if (alias.length > 32) return "Keep it under 32 characters."; if (!CUSTOM_RE.test(alias)) return "Only letters, numbers, hyphens and underscores."; return null; } const URL_RE = /^https?:\/\/[^\s.]+\.[^\s]{2,}$/i; export function normalizeUrl(input: string): string { const trimmed = input.trim(); if (!trimmed) return ""; if (/^https?:\/\//i.test(trimmed)) return trimmed; return `https://${trimmed}`; } export function isValidUrl(input: string): boolean { return URL_RE.test(normalizeUrl(input)); }