import { generateSecureBase58Token } from "@reviq/utils"; import { base58 } from "@scure/base"; // Re-export for convenience export { generateSecureBase58Token }; /** * Token prefix for all RevIQ API tokens */ export const TOKEN_PREFIX = "reviq_"; /** * Hash a token with SHA-256 for storage in database * Never store raw tokens - always hash first * Uses Web Crypto API for Cloudflare Workers compatibility */ export const hashToken = async (token: string): Promise => { const encoder = new TextEncoder(); const data = encoder.encode(token); const hashBuffer = await crypto.subtle.digest("SHA-256", data); const hashArray = new Uint8Array(hashBuffer); return Array.from(hashArray) .map((b) => b.toString(16).padStart(2, "0")) .join(""); }; /** * Validate that a token has the correct format * Returns the raw bytes if valid, null if invalid */ export const parseToken = (token: string): Uint8Array | null => { if (!token.startsWith(TOKEN_PREFIX)) { return null; } const encoded = token.slice(TOKEN_PREFIX.length); try { const bytes = base58.decode(encoded); // Expect 32 bytes of entropy if (bytes.length !== 32) { return null; } return bytes; } catch { return null; } }; /** * Check if a token has the valid reviq_ prefix format */ export const isValidTokenFormat = (token: string): boolean => { return parseToken(token) !== null; }; /** * Generate a session token (UUID v4) */ export const generateSessionToken = (): string => { return crypto.randomUUID(); }; /** * Device fingerprint prefix for new fingerprints */ export const DEVICE_FINGERPRINT_PREFIX = "device_"; /** * Generate a device fingerprint (base58 with device_ prefix) */ export const generateDeviceFingerprint = (): string => { return generateSecureBase58Token(DEVICE_FINGERPRINT_PREFIX); }; /** * Check if a string is a valid device fingerprint. * Accepts both new format (device_ prefix) and legacy UUIDs. */ export const isValidDeviceFingerprint = (fingerprint: string): boolean => { // New format: device_ prefix with base58 if (fingerprint.startsWith(DEVICE_FINGERPRINT_PREFIX)) { const base58Part = fingerprint.slice(DEVICE_FINGERPRINT_PREFIX.length); if (base58Part.length === 0) { return false; } try { base58.decode(base58Part); return true; } catch { return false; } } // Legacy format: UUID v4 const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; return uuidRegex.test(fingerprint); }; /** * Generate expiration date */ export const generateExpiry = (seconds: number): Date => { return new Date(Date.now() + seconds * 1000); };