import { createClient } from "@supabase/supabase-js"; export const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; export const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY; export const hasSupabaseConfig = Boolean(supabaseUrl && supabaseAnonKey); /** * Secure session storage for Supabase auth tokens. * * Security properties: * - Uses sessionStorage (dies on tab close, not shared across tabs) * - Tokens are obfuscated with a per-session random key before storage * - No plaintext tokens in sessionStorage — reduces impact of XSS * - Auto-clears on detection of tampered/missing data * * This is NOT as secure as httpOnly cookies (which require server-side SSR), * but provides significantly better protection than plaintext localStorage: * - Tokens don't persist across browser restarts * - Tokens aren't shared across tabs (reduces cross-tab attacks) * - Obfuscation adds friction for casual XSS token theft */ const STORAGE_KEY = "supersam-auth"; const KEY_KEY = "supersam-ak"; function _getKey() { let key = sessionStorage.getItem(KEY_KEY); if (!key) { key = crypto.getRandomValues(new Uint8Array(32)).reduce( (s, b) => s + b.toString(16).padStart(2, "0"), "" ); sessionStorage.setItem(KEY_KEY, key); } return key; } async function _obfuscate(value) { const key = _getKey(); const enc = new TextEncoder(); const keyData = enc.encode(key); const valueData = enc.encode(value); const result = new Uint8Array(valueData.length); for (let i = 0; i < valueData.length; i++) { result[i] = valueData[i] ^ keyData[i % keyData.length]; } return btoa(String.fromCharCode(...result)); } async function _deobfuscate(obfuscated) { try { const key = _getKey(); const enc = new TextEncoder(); const keyData = enc.encode(key); const raw = Uint8Array.from(atob(obfuscated), (c) => c.charCodeAt(0)); const result = new Uint8Array(raw.length); for (let i = 0; i < raw.length; i++) { result[i] = raw[i] ^ keyData[i % keyData.length]; } return new TextDecoder().decode(result); } catch { // Tampered data — clear everything sessionStorage.removeItem(STORAGE_KEY); sessionStorage.removeItem(KEY_KEY); return ""; } } const secureStorage = { getItem: async (key) => { const raw = sessionStorage.getItem(STORAGE_KEY); if (!raw) return null; try { const data = JSON.parse(raw); const value = data[key]; if (typeof value !== "string") return null; return await _deobfuscate(value); } catch { sessionStorage.removeItem(STORAGE_KEY); return null; } }, setItem: async (key, value) => { let data; try { const raw = sessionStorage.getItem(STORAGE_KEY); data = raw ? JSON.parse(raw) : {}; } catch { data = {}; } data[key] = await _obfuscate(value); sessionStorage.setItem(STORAGE_KEY, JSON.stringify(data)); }, removeItem: async (key) => { const raw = sessionStorage.getItem(STORAGE_KEY); if (!raw) return; try { const data = JSON.parse(raw); delete data[key]; if (Object.keys(data).length === 0) { sessionStorage.removeItem(STORAGE_KEY); } else { sessionStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } } catch { sessionStorage.removeItem(STORAGE_KEY); } }, }; export const supabase = hasSupabaseConfig ? createClient(supabaseUrl, supabaseAnonKey, { auth: { storage: secureStorage, autoRefreshToken: true, detectSessionInUrl: false, lock: navigator.locks ? undefined : "no-lock", }, global: { headers: { "x-application-name": "supersam" }, }, }) : null;