import Hashids from 'hashids'; const env = (typeof import.meta !== 'undefined' && import.meta.env ? import.meta.env : {}) as Record; export const HASHER_SETTINGS = { minHashLength: Number(env.VITE_HASHER_MIN_LENGTH) || 11, salt: (env.VITE_HASHER_SALT as string) || '', }; let hasher = new Hashids(HASHER_SETTINGS.salt, HASHER_SETTINGS.minHashLength); export function updateHasherSettings(salt?: string, minLength?: number | string): void { let updated = false; if (salt && typeof salt === 'string' && salt.trim() !== '') { HASHER_SETTINGS.salt = salt.trim(); updated = true; } if (minLength !== undefined && minLength !== null && minLength !== '') { const parsed = Number(minLength); if (!isNaN(parsed) && parsed > 0) { HASHER_SETTINGS.minHashLength = parsed; updated = true; } } if (updated) { hasher = new Hashids(HASHER_SETTINGS.salt, HASHER_SETTINGS.minHashLength); } } /** * Validates if candidate is already a HashID generated by this hasher. */ export function isHashId(candidate: string | null | undefined): boolean { if (!candidate || typeof candidate !== 'string' || candidate.length < HASHER_SETTINGS.minHashLength) { return false; } try { const decodedNumbers = hasher.decode(candidate); if (Array.isArray(decodedNumbers) && decodedNumbers.length > 0) return true; const decodedHex = hasher.decodeHex(candidate); if (decodedHex && decodedHex.length > 0) return true; } catch { return false; } return false; } /** * Encodes a user idNumber using Hashids Hex encoding. * Supports South African national IDs (13 digits), passports, and alphanumeric keys. */ export function encodeIdNumberHex(idNumber: string | number | null | undefined): string { if (idNumber === undefined || idNumber === null) return ''; const str = String(idNumber).trim(); if (!str) return ''; // Prevent double-encoding if already a Hashid if (isHashId(str)) { return str; } // If already pure hex digits (e.g. standard decimal SA ID 9501015800085 or hex hash) if (/^[0-9a-fA-F]+$/.test(str)) { try { const encoded = hasher.encodeHex(str.toLowerCase()); if (encoded) return encoded; } catch { // Fallback below } } // For arbitrary alphanumeric ID strings, encode character bytes to hex try { const hex = Array.from(str) .map((c) => c.charCodeAt(0).toString(16).padStart(2, '0')) .join(''); const encoded = hasher.encodeHex(hex); return encoded || str; } catch { return str; } } /** * Decodes a Hashids Hex encoded string back to original idNumber. */ export function decodeIdNumberHex(hash: string | null | undefined): string { if (!hash || typeof hash !== 'string') return ''; const trimmed = hash.trim(); if (!trimmed) return ''; try { const decodedHex = hasher.decodeHex(trimmed); if (!decodedHex) return trimmed; // Check if decoded hex represents ASCII text with 2-char hex pairs if (/^[0-9a-fA-F]{4,}$/.test(decodedHex) && decodedHex.length % 2 === 0) { let isText = true; let text = ''; for (let i = 0; i < decodedHex.length; i += 2) { const code = parseInt(decodedHex.slice(i, i + 2), 16); if (code < 32 || code > 126) { isText = false; break; } text += String.fromCharCode(code); } if (isText && text.length > 0) return text; } return decodedHex; } catch { return trimmed; } } /** * Converts any numeric or string ID to a safe 64-bit integer string. * Preserves precision beyond 2^53 - 1 by avoiding standard float coercion. */ export function toSafeInt64String(val: string | number | bigint | null | undefined): string { if (val === undefined || val === null || val === '') return ''; if (typeof val === 'bigint') return val.toString(); if (typeof val === 'number') { if (isNaN(val) || !isFinite(val)) return ''; return Math.trunc(val).toString(); } const str = String(val).trim(); if (/^-?\d+$/.test(str)) { return str; } return str; } /** * Deterministically encodes a numeric ID or 64-bit integer into a standard HashID string. */ export function encodeEntityId(id: string | number | bigint | null | undefined): string { if (id === undefined || id === null || id === '') return ''; const str = typeof id === 'bigint' ? id.toString() : String(id).trim(); if (!str) return ''; if (isHashId(str)) return str; // If safe integer range if (/^\d+$/.test(str)) { try { const num = Number(str); if (num <= Number.MAX_SAFE_INTEGER && num >= 0) { return hasher.encode(num); } // For integers above 2^53 - 1, encode as hex const bigHex = BigInt(str).toString(16); return hasher.encodeHex(bigHex); } catch { // Fall through } } return str; } /** * Deterministically decodes a standard HashID string to its canonical ID representation as string. */ export function decodeEntityId(hashId: string | number | null | undefined): string { if (hashId === undefined || hashId === null || hashId === '') return ''; const str = String(hashId).trim(); if (!str) return ''; if (/^-?\d+$/.test(str)) return str; try { const decoded = hasher.decode(str); if (Array.isArray(decoded) && decoded.length > 0) { return decoded[0].toString(); } const decodedHex = hasher.decodeHex(str); if (decodedHex) { try { return BigInt('0x' + decodedHex).toString(); } catch { return decodedHex; } } } catch { // Return raw } return str; }