Files
pfm/PostFundManagement.ConsoleUI/src/utils/hashids.ts
T

195 lines
5.5 KiB
TypeScript

import Hashids from 'hashids';
const env = (typeof import.meta !== 'undefined' && import.meta.env
? import.meta.env
: {}) as Record<string, string | undefined>;
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;
}