/** * Centralized API Key Management * Single source of truth per storage e retrieval di tutte le API key. * * Priorità: env vars (build-time) > localStorage (runtime, impostato dall'utente) * Usa src/config/env.ts per le env vars — mai import.meta.env direttamente. * * S764: layer di obfuscazione XOR+base64 su localStorage. * La chiave di obfuscazione è un UUID casuale persistente in localStorage (__agk__). * Migration automatica: valori plaintext legacy vengono riescritti obfuscati al primo accesso. * API pubblica invariata — tutti getter/setter rimangono sincroni. * ARCH-2: layer AES-GCM-256 — chiave non-estraibile in IndexedDB, in-memory cache per API sync. * init() chiamato una volta in App.tsx al boot. Fallback XOR se IndexedDB non disponibile. * * STORAGE-ALIGN (2026-06-27): STORAGE_KEYS allineato a ENV — aggiunti 12 slot mancanti * (GROQ_4, NVIDIA_2/3/4, OPENROUTER_B/C/D, GEMINI_B/C/D, E2B, E2B_2). * Rimossi tutti i cast (ENV as any) e (STORAGE_KEYS as any) — ora type-safe. * Aggiunti getGeminiTokens(), getOpenRouterTokens(), getNvidiaTokens() per multi-key. */ import { ENV } from "@/config/env"; // ─── Storage Keys ───────────────────────────────────────────────────────── const STORAGE_KEYS = { HUGGINGFACE: "agent_hf_token", GROQ: "agent_groq_token", GROQ_2: "agent_groq_token_2", GROQ_3: "agent_groq_token_3", GROQ_4: "agent_groq_token_4", // S-RAILWAY-BYPASS slot 4 OPENROUTER: "agent_openrouter_token", OPENROUTER_B: "agent_openrouter_token_b", // STORAGE-ALIGN OPENROUTER_C: "agent_openrouter_token_c", // STORAGE-ALIGN OPENROUTER_D: "agent_openrouter_token_d", // STORAGE-ALIGN OPENAI: "agent_openai_token", TOGETHER: "agent_together_token", GEMINI: "agent_gemini_token", GEMINI_B: "agent_gemini_token_b", // STORAGE-ALIGN GEMINI_C: "agent_gemini_token_c", // STORAGE-ALIGN GEMINI_D: "agent_gemini_token_d", // STORAGE-ALIGN CLOUDFLARE: "agent_cf_token", // R27 — formato legacy: "accountId:apiToken" CEREBRAS: "agent_cerebras_token", // S325 — Cerebras 2000+ tok/s, free tier SAMBANOVA: "agent_sambanova_token", // S408 — SambaNova 2200+ tok/s, free tier GITHUB: "agent_github_token", // S-LOOP9 — GitHub Models (fallback localStorage) NVIDIA: "agent_nvidia_token", // NVIDIA NIM — integrate.api.nvidia.com NVIDIA_2: "agent_nvidia_token_2", // STORAGE-ALIGN NVIDIA_3: "agent_nvidia_token_3", // STORAGE-ALIGN NVIDIA_4: "agent_nvidia_token_4", // STORAGE-ALIGN E2B: "agent_e2b_token", // STORAGE-ALIGN E2B_2: "agent_e2b_token_2", // STORAGE-ALIGN // S-TGSEC: Telegram config cifrato con AES-GCM (stesso layer delle chiavi AI) TELEGRAM_TOKEN: "agent_telegram_token", TELEGRAM_CHAT_ID: "agent_telegram_chat_id", NOTION: "agent_notion_token", // P39-UX: Notion API key (AES-GCM, no env var) } as const; // ─── Obfuscation Layer (S764) ───────────────────────────────────────────── // Protegge le API key da ispezione visiva casual e scraping automatico di localStorage. // Chiave in localStorage (stessa origin) → non crittografia vera, ma ostacola // lookup diretto delle chiavi. Upgrade a AES-GCM (async) in sprint dedicato. /** Nome del localStorage key che contiene la chiave di obfuscazione */ const _OB_KEY_NAME = "__agk__"; /** Prefisso che identifica un valore obfuscato */ const _OB_PREFIX = "__ob_"; // ─── ARCH-2: AES-GCM-256 layer ─────────────────────────────────────────── // CryptoKey AES-256 non-estraibile conservata in IndexedDB (mai esposta come stringa). // In-memory _tokenCache rende i getter pubblici sincroni dopo initApiKeyEncryption(). const _AES_PREFIX = "__aes2_"; // valore cifrato AES-GCM v2 in localStorage const _AES_DB_NAME = "agk-db"; // IndexedDB database name const _AES_DB_VER = 1; const _AES_STORE = "keys"; const _AES_KEY_ID = "main"; /** Cache in-memory dei token decodificati — popolata da initApiKeyEncryption(). */ const _tokenCache = new Map(); /** CryptoKey AES-GCM-256 non-estraibile. null finché initApiKeyEncryption() non completa. */ let _aesKey: CryptoKey | null = null; /** F6-FIX: Promise che si risolve al termine di initApiKeyEncryption(). * null = init non ancora avviato. Usare waitForApiKeyInit() prima del primo accesso ai token. */ let _initPromise: Promise | null = null; // VAULT-SEC: storage mode — pattern 1Password/Bitwarden: mai XOR come fallback. // 'aes-persistent': IndexedDB key + localStorage ciphertext (normale) // 'aes-session': chiave AES in-memory + sessionStorage ciphertext (Private Mode) type _StorageMode = 'initializing' | 'aes-persistent' | 'aes-session'; let _storageMode: _StorageMode = 'initializing'; const _modeListeners = new Set<(m: 'aes-persistent' | 'aes-session') => void>(); function _emitMode(m: 'aes-persistent' | 'aes-session') { _modeListeners.forEach(fn => fn(m)); } /** Ritorna il crypto storage mode. Usato dalla UI per il badge di sicurezza. */ export function getStorageMode(): _StorageMode { return _storageMode; } /** Ascolta cambio di storage mode. Ritorna cleanup fn. */ export function onStorageModeChange(fn: (m: 'aes-persistent' | 'aes-session') => void): () => void { _modeListeners.add(fn); return () => _modeListeners.delete(fn); } /** * TOKEN-FIX: Ottiene o crea la chiave di obfuscazione XOR. * Salvata in ENTRAMBI sessionStorage (veloce) e localStorage (persistente). * * PERCHÉ: iOS Safari svuota sessionStorage quando la tab viene uccisa in background. * Se la chiave era SOLO in sessionStorage (vecchio comportamento), tutti i token * salvati in localStorage diventano illeggibili → loop infinito di re-inserimento. * Tenendo la chiave anche in localStorage il fallback funziona sempre. */ function _getOrCreateObKey(): string { try { // 1. Prova sessionStorage (accesso rapido, stessa sessione) const fromSession = sessionStorage.getItem(_OB_KEY_NAME); if (fromSession && fromSession.length >= 16) return fromSession; // 2. Fallback localStorage — sopravvive a tab kill iOS Safari e ricariche const fromLocal = localStorage.getItem(_OB_KEY_NAME); if (fromLocal && fromLocal.length >= 16) { // Ripristina in sessionStorage per questa sessione (performance) try { sessionStorage.setItem(_OB_KEY_NAME, fromLocal); } catch { /* non-blocking */ } return fromLocal; } // 3. Prima esecuzione: genera chiave e salva in ENTRAMBI per persistenza cross-sessione const key = (crypto.randomUUID() + crypto.randomUUID()).replace(/-/g, ""); try { sessionStorage.setItem(_OB_KEY_NAME, key); } catch { /* non-blocking */ } try { localStorage.setItem(_OB_KEY_NAME, key); } catch { /* non-blocking */ } return key; } catch { return "agente-ai-fallback-ob-key-2024"; // storage non disponibile } } /** * Obfusca un valore con XOR + base64. * Non è cifratura crittografica — ostacola ispezione visiva e scraping. */ function _obfuscate(value: string, obKey: string): string { const kBytes = Array.from(obKey).map(c => c.charCodeAt(0)); const result = Array.from(value).map((c, i) => String.fromCharCode(c.charCodeAt(0) ^ (kBytes[i % kBytes.length] & 0x7F)) ).join(""); return _OB_PREFIX + btoa(unescape(encodeURIComponent(result))); } /** * Deobfusca un valore. Ritorna null se il formato non è valido. */ function _deobfuscate(stored: string, obKey: string): string | null { if (!stored.startsWith(_OB_PREFIX)) return null; try { const decoded = decodeURIComponent(escape(atob(stored.slice(_OB_PREFIX.length)))); const kBytes = Array.from(obKey).map(c => c.charCodeAt(0)); return Array.from(decoded).map((c, i) => String.fromCharCode(c.charCodeAt(0) ^ (kBytes[i % kBytes.length] & 0x7F)) ).join(""); } catch { return null; } } // ─── ARCH-2: AES helpers ───────────────────────────────────────────────── function _uint8ToBase64(buf: Uint8Array): string { let s = ""; for (let i = 0; i < buf.length; i++) s += String.fromCharCode(buf[i]); return btoa(s); } async function _openAesKeyStore(): Promise { return new Promise((resolve, reject) => { const req = indexedDB.open(_AES_DB_NAME, _AES_DB_VER); req.onupgradeneeded = () => { req.result.createObjectStore(_AES_STORE); }; req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } async function _loadOrCreateAesKey(): Promise { const db = await _openAesKeyStore(); return new Promise((resolve, reject) => { const tx = db.transaction(_AES_STORE, "readwrite"); const get = tx.objectStore(_AES_STORE).get(_AES_KEY_ID); get.onsuccess = async () => { if (get.result) { db.close(); resolve(get.result as CryptoKey); return; } try { const key = await crypto.subtle.generateKey( { name: "AES-GCM", length: 256 }, false, // non-extractable: mai esposta come raw bytes ["encrypt", "decrypt"], ); const tx2 = db.transaction(_AES_STORE, "readwrite"); tx2.objectStore(_AES_STORE).put(key, _AES_KEY_ID); tx2.oncomplete = () => { db.close(); resolve(key); }; tx2.onerror = () => { db.close(); reject(tx2.error); }; } catch (e) { db.close(); reject(e); } }; get.onerror = () => { db.close(); reject(get.error); }; }); } async function _aesEncrypt(value: string, key: CryptoKey): Promise { const iv = crypto.getRandomValues(new Uint8Array(12)); // 96-bit IV (GCM standard) const ct = await crypto.subtle.encrypt( { name: "AES-GCM", iv }, key, new TextEncoder().encode(value), ); const buf = new Uint8Array(12 + ct.byteLength); buf.set(iv, 0); buf.set(new Uint8Array(ct), 12); return _AES_PREFIX + _uint8ToBase64(buf); } async function _aesDecrypt(stored: string, key: CryptoKey): Promise { if (!stored.startsWith(_AES_PREFIX)) return null; try { const raw = atob(stored.slice(_AES_PREFIX.length)); const buf = new Uint8Array(raw.length); for (let i = 0; i < raw.length; i++) buf[i] = raw.charCodeAt(i); const iv = buf.slice(0, 12); const ct = buf.slice(12); const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct); return new TextDecoder().decode(pt); } catch { return null; } // chiave sbagliata, corruzione o formato errato } /** * ARCH-2: Inizializza layer AES-GCM. Chiamare UNA VOLTA in App.tsx al boot (fire-and-forget). * * - Crea/carica CryptoKey AES-GCM-256 non-estraibile da IndexedDB * - Popola _tokenCache decifrando tutti i token esistenti in localStorage * - Migra valori XOR legacy (__ob_) e plaintext → AES (__aes2_) * - Idempotente: se _aesKey già impostato, esce immediatamente * - Fallback silenzioso: se IndexedDB non disponibile, XOR rimane attivo */ /** F6-FIX: attende il completamento di initApiKeyEncryption(). * Chiamare nell'agentLoop prima del primo accesso ai token per evitare getter null pre-init. */ export function waitForApiKeyInit(): Promise { if (_initPromise) return _initPromise; // Init non ancora avviato — ritorna promise risolta (token saranno plaintext/XOR) return Promise.resolve(); } export async function initApiKeyEncryption(): Promise { if (_aesKey) return; // idempotente // F6-FIX: deduplica chiamate concorrenti — seconda chiamata riceve la stessa promise if (_initPromise) return _initPromise; let _resolve!: () => void; _initPromise = new Promise(res => { _resolve = res; }); try { _aesKey = await _loadOrCreateAesKey(); _storageMode = 'aes-persistent'; _emitMode('aes-persistent'); // Leggi OB key UNA SOLA VOLTA (fallback per migrazione XOR) const obKey = localStorage.getItem(_OB_KEY_NAME) ?? sessionStorage.getItem(_OB_KEY_NAME) ?? ""; for (const lsKey of Object.values(STORAGE_KEYS)) { try { const raw = localStorage.getItem(lsKey); if (!raw) continue; let cleartext: string | null = null; if (raw.startsWith(_AES_PREFIX)) { // Già AES-cifrato → solo decifrare in cache cleartext = await _aesDecrypt(raw, _aesKey); } else if (raw.startsWith(_OB_PREFIX)) { // XOR legacy → decifrare + migrare ad AES in localStorage cleartext = _deobfuscate(raw, obKey); if (cleartext) { const enc = await _aesEncrypt(cleartext, _aesKey); try { localStorage.setItem(lsKey, enc); } catch { /* non-blocking */ } } } else { // Plaintext legacy → migrare ad AES cleartext = raw; const enc = await _aesEncrypt(raw, _aesKey); try { localStorage.setItem(lsKey, enc); } catch { /* non-blocking */ } } if (cleartext) _tokenCache.set(lsKey, cleartext); } catch { /* errore singolo token — continua con gli altri */ } } } catch { // VAULT-SEC: mai XOR — AES-256-GCM non-extractable in-memory (aes-session) try { _aesKey = await crypto.subtle.generateKey( { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'], ); } catch { _aesKey = null; } _storageMode = 'aes-session'; _emitMode('aes-session'); } finally { // F6-FIX: segnala a tutti i getter in attesa che l'init è completato try { _resolve(); } catch { /* già risolta o _resolve non impostata */ } } } // ─── Safe localStorage ──────────────────────────────────────────────────── function safeGetFromStorage(key: string): string | null { // ARCH-2: cache first — popolata da initApiKeyEncryption() al boot (50-100ms) const cached = _tokenCache.get(key); if (cached !== undefined) return cached; // Fallback sync: usato nel periodo pre-init o quando IndexedDB non disponibile try { const raw = _storageMode === 'aes-session' ? (sessionStorage.getItem(key) ?? localStorage.getItem(key)) : localStorage.getItem(key); if (!raw) return null; if (raw.startsWith(_AES_PREFIX)) { // Valore AES ma cache non ancora pronta — init async non completato // Ritorna null: il componente riceverà il valore al prossimo render post-init return null; } if (raw.startsWith(_OB_PREFIX)) { const obKey = sessionStorage.getItem(_OB_KEY_NAME) ?? localStorage.getItem(_OB_KEY_NAME) ?? ""; if (!obKey) return null; return _deobfuscate(raw, obKey); } // Plaintext legacy — migra a XOR al volo (side-effect, sync) try { const obKey = _getOrCreateObKey(); localStorage.setItem(key, _obfuscate(raw, obKey)); } catch { /* migration fallisce silenziosamente */ } return raw; } catch { return null; } } function safeSetToStorage(key: string, value: string): boolean { try { // ARCH-2: aggiorna cache in-memory sync (getter vedono il nuovo valore immediatamente) if (value) _tokenCache.set(key, value); else _tokenCache.delete(key); if (value) { if (_aesKey) { // AES init completato: fire-and-forget encrypt → localStorage // VAULT-SEC: usa sessionStorage in Private Mode, localStorage altrimenti const _tgt = _storageMode === 'aes-session' ? sessionStorage : localStorage; _aesEncrypt(value, _aesKey) .then(enc => { try { _tgt.setItem(key, enc); } catch {} }) .catch(() => { /* encrypt fail — valore in _tokenCache questa sessione */ }); } // _aesKey null: solo _tokenCache, zero XOR } else { localStorage.removeItem(key); } return true; } catch { return false; } } // ─── Getters: env var ha precedenza su localStorage ─────────────────────── export function getHuggingFaceToken(): string | null { const t = ENV.HF_KEY || safeGetFromStorage(STORAGE_KEYS.HUGGINGFACE); if (t && t.trim()) return t.trim(); // P19-B2b: in produzione HF Token è nel CF Worker Secrets return import.meta.env.PROD ? "proxy" : null; } /** * S-RAILWAY-BYPASS: Ritorna il token Groq per lo slot specificato (1-4). * Supporta 4 profili Railway diversi per moltiplicare i rate limit. * STORAGE-ALIGN: rimosso (ENV as any) — ENV.GROQ_KEY_4 ora type-safe. */ export function getGroqToken(slot: string = "1"): string | null { const storageKey = slot === "2" ? STORAGE_KEYS.GROQ_2 : slot === "3" ? STORAGE_KEYS.GROQ_3 : slot === "4" ? STORAGE_KEYS.GROQ_4 : STORAGE_KEYS.GROQ; const envKey = slot === "2" ? ENV.GROQ_KEY_2 : slot === "3" ? ENV.GROQ_KEY_3 : slot === "4" ? ENV.GROQ_KEY_4 : ENV.GROQ_KEY; const t = envKey || safeGetFromStorage(storageKey); return t && t.trim() ? t.trim() : null; } /** * S408: Groq key rotation — restituisce tutte le chiavi configurate (max 4). */ export function getGroqTokens(): string[] { const keys = [ getGroqToken("1"), getGroqToken("2"), getGroqToken("3"), getGroqToken("4"), ]; const real = keys.filter((k): k is string => !!(k && k.trim())).map(k => k.trim()); if (!real.length && import.meta.env.PROD) return ["proxy"]; return real; } /** * OpenRouter token per lo slot specificato (1=A, 2=B, 3=C, 4=D). * STORAGE-ALIGN: aggiunto supporto slot B/C/D. */ export function getOpenRouterToken(slot: string = "1"): string | null { const storageKey = slot === "2" ? STORAGE_KEYS.OPENROUTER_B : slot === "3" ? STORAGE_KEYS.OPENROUTER_C : slot === "4" ? STORAGE_KEYS.OPENROUTER_D : STORAGE_KEYS.OPENROUTER; const envKey = slot === "2" ? ENV.OPENROUTER_KEY_B : slot === "3" ? ENV.OPENROUTER_KEY_C : slot === "4" ? ENV.OPENROUTER_KEY_D : ENV.OPENROUTER_KEY; const t = envKey || safeGetFromStorage(storageKey); if (t && t.trim()) return t.trim(); // P20-OR: in produzione OpenRouter key è nel CF Worker Secrets return slot === "1" && import.meta.env.PROD ? "proxy" : null; } /** * Restituisce tutte le chiavi OpenRouter configurate (A → D). */ export function getOpenRouterTokens(): string[] { const keys = [ getOpenRouterToken("1"), getOpenRouterToken("2"), getOpenRouterToken("3"), getOpenRouterToken("4"), ]; const real = keys.filter((k): k is string => !!(k && k.trim() && k !== "proxy")).map(k => k.trim()); if (!real.length && import.meta.env.PROD) return ["proxy"]; return real; } export function getOpenAIToken(): string | null { const t = ENV.OPENAI_KEY || safeGetFromStorage(STORAGE_KEYS.OPENAI); return t && t.trim() ? t.trim() : null; } export function getTogetherToken(): string | null { const t = ENV.TOGETHER_KEY || safeGetFromStorage(STORAGE_KEYS.TOGETHER); return t && t.trim() ? t.trim() : null; } /** * Gemini token per lo slot specificato (1=A, 2=B, 3=C, 4=D). * STORAGE-ALIGN: aggiunto supporto slot B/C/D. */ export function getGeminiToken(slot: string = "1"): string | null { const storageKey = slot === "2" ? STORAGE_KEYS.GEMINI_B : slot === "3" ? STORAGE_KEYS.GEMINI_C : slot === "4" ? STORAGE_KEYS.GEMINI_D : STORAGE_KEYS.GEMINI; const envKey = slot === "2" ? ENV.GEMINI_KEY_B : slot === "3" ? ENV.GEMINI_KEY_C : slot === "4" ? ENV.GEMINI_KEY_D : ENV.GEMINI_KEY; const t = envKey || safeGetFromStorage(storageKey); if (t && t.trim()) return t.trim(); // P20-B1: in produzione Gemini key è nel CF Worker Secrets return slot === "1" && import.meta.env.PROD ? "proxy" : null; } /** * Restituisce tutte le chiavi Gemini configurate (A → D). */ export function getGeminiTokens(): string[] { const keys = [ getGeminiToken("1"), getGeminiToken("2"), getGeminiToken("3"), getGeminiToken("4"), ]; const real = keys.filter((k): k is string => !!(k && k.trim() && k !== "proxy")).map(k => k.trim()); if (!real.length && import.meta.env.PROD) return ["proxy"]; return real; } // S325: Cerebras — ultra-fast inference, 2000+ tok/s su Llama 3.3 70B, 1M tok/giorno gratis export function getCerebrasToken(): string | null { const t = ENV.CEREBRAS_KEY || safeGetFromStorage(STORAGE_KEYS.CEREBRAS); if (t && t.trim()) return t.trim(); // P19-B2b: in produzione Cerebras Key è nel CF Worker Secrets return import.meta.env.PROD ? "proxy" : null; } // S408: SambaNova — 2200+ tok/s, Llama 3.3 70B / Llama 4 Scout / DeepSeek-R1, free tier // Signup: cloud.sambanova.ai — quota: 60 req/min, nessun limite giornaliero duro export function getSambanovaToken(): string | null { const t = ENV.SAMBANOVA_KEY || safeGetFromStorage(STORAGE_KEYS.SAMBANOVA); if (t && t.trim()) return t.trim(); // P19-B2b: in produzione SambaNova Key è nel CF Worker Secrets return import.meta.env.PROD ? "proxy" : null; } /** * NVIDIA NIM — 15 modelli verificati live 2026-06-23 (integrate.api.nvidia.com/v1, free tier). * STORAGE-ALIGN: rimossi (STORAGE_KEYS as any) e (ENV as any) — ora type-safe. */ export function getNvidiaToken(slot: string = "1"): string | null { const storageKey = slot === "2" ? STORAGE_KEYS.NVIDIA_2 : slot === "3" ? STORAGE_KEYS.NVIDIA_3 : slot === "4" ? STORAGE_KEYS.NVIDIA_4 : STORAGE_KEYS.NVIDIA; const envKey = slot === "2" ? ENV.NVIDIA_KEY_B : slot === "3" ? ENV.NVIDIA_KEY_C : slot === "4" ? ENV.NVIDIA_KEY_D : ENV.NVIDIA_KEY; const t = envKey || safeGetFromStorage(storageKey); if (t && t.trim()) return t.trim(); return import.meta.env.PROD ? "proxy" : null; } /** * Restituisce tutte le chiavi NVIDIA configurate (slot 1 → 4). */ export function getNvidiaTokens(): string[] { const keys = [ getNvidiaToken("1"), getNvidiaToken("2"), getNvidiaToken("3"), getNvidiaToken("4"), ]; const real = keys.filter((k): k is string => !!(k && k.trim() && k !== "proxy")).map(k => k.trim()); if (!real.length && import.meta.env.PROD) return ["proxy"]; return real; } /** * E2B — code execution sandbox. * STORAGE-ALIGN: rimossi (STORAGE_KEYS as any) e (ENV as any) — ora type-safe. */ export function getE2BToken(slot: string = "1"): string | null { const storageKey = slot === "2" ? STORAGE_KEYS.E2B_2 : STORAGE_KEYS.E2B; const envKey = slot === "2" ? ENV.E2B_KEY_2 : ENV.E2B_KEY; const t = envKey || safeGetFromStorage(storageKey); return t && t.trim() ? t.trim() : null; } // S-LOOP9: GitHub Models — GPT-4o, gpt-4.1, DeepSeek-R1, Phi-4-reasoning, Llama-405B GRATIS // GITHUB_TOKEN già nei secrets Replit + iniettato da vite.config.ts (VITE_GITHUB_TOKEN) // Endpoint: models.inference.ai.azure.com (Azure-hosted, OpenAI-compat, free tier) export function getGithubModelsToken(): string | null { const t = ENV.GITHUB_TOKEN || safeGetFromStorage(STORAGE_KEYS.GITHUB); return t && t.trim() ? t.trim() : null; } // ─── Cloudflare Workers AI (R27 + S408) ─────────────────────────────────────── // Priorità: env vars (CF_ACCOUNT_ID + CF_API_TOKEN) > localStorage (formato "accountId:token") // Con i secrets Replit già configurati, questo funziona automaticamente senza localStorage. export function getCfAiToken(): { accountId: string; token: string } | null { // S408: leggi prima da env vars (CF_API_TOKEN + CF_ACCOUNT_ID sono già in Replit secrets) if (ENV.CF_ACCOUNT_ID && ENV.CF_AI_TOKEN) { return { accountId: ENV.CF_ACCOUNT_ID, token: ENV.CF_AI_TOKEN }; } // Fallback: localStorage (formato legacy "accountId:apiToken") const raw = safeGetFromStorage(STORAGE_KEYS.CLOUDFLARE); if (!raw || !raw.trim()) return null; const sep = raw.indexOf(":"); if (sep < 1) return null; const accountId = raw.slice(0, sep).trim(); const token = raw.slice(sep + 1).trim(); return (accountId && token) ? { accountId, token } : null; } // ─── Setters (solo localStorage — le env vars sono immutabili a runtime) ── export function setHuggingFaceToken(token: string): boolean { return safeSetToStorage(STORAGE_KEYS.HUGGINGFACE, token); } export function setGroqToken(token: string): boolean { return safeSetToStorage(STORAGE_KEYS.GROQ, token); } export function setGroqToken2(token: string): boolean { return safeSetToStorage(STORAGE_KEYS.GROQ_2, token); } export function setGroqToken3(token: string): boolean { return safeSetToStorage(STORAGE_KEYS.GROQ_3, token); } export function setGroqToken4(token: string): boolean { return safeSetToStorage(STORAGE_KEYS.GROQ_4, token); } export function setOpenRouterToken(token: string): boolean { return safeSetToStorage(STORAGE_KEYS.OPENROUTER, token); } export function setOpenRouterTokenB(token: string): boolean { return safeSetToStorage(STORAGE_KEYS.OPENROUTER_B, token); } export function setOpenRouterTokenC(token: string): boolean { return safeSetToStorage(STORAGE_KEYS.OPENROUTER_C, token); } export function setOpenRouterTokenD(token: string): boolean { return safeSetToStorage(STORAGE_KEYS.OPENROUTER_D, token); } export function setOpenAIToken(token: string): boolean { return safeSetToStorage(STORAGE_KEYS.OPENAI, token); } export function setTogetherToken(token: string): boolean { return safeSetToStorage(STORAGE_KEYS.TOGETHER, token); } export function setGeminiToken(token: string): boolean { return safeSetToStorage(STORAGE_KEYS.GEMINI, token); } export function setGeminiTokenB(token: string): boolean { return safeSetToStorage(STORAGE_KEYS.GEMINI_B, token); } export function setGeminiTokenC(token: string): boolean { return safeSetToStorage(STORAGE_KEYS.GEMINI_C, token); } export function setGeminiTokenD(token: string): boolean { return safeSetToStorage(STORAGE_KEYS.GEMINI_D, token); } export function setCerebrasToken(token: string): boolean { return safeSetToStorage(STORAGE_KEYS.CEREBRAS, token); } export function setSambanovaToken(token: string): boolean { return safeSetToStorage(STORAGE_KEYS.SAMBANOVA, token); } export function setNvidiaToken(token: string): boolean { return safeSetToStorage(STORAGE_KEYS.NVIDIA, token); } export function setNvidiaToken2(token: string): boolean { return safeSetToStorage(STORAGE_KEYS.NVIDIA_2, token); } export function setNvidiaToken3(token: string): boolean { return safeSetToStorage(STORAGE_KEYS.NVIDIA_3, token); } export function setNvidiaToken4(token: string): boolean { return safeSetToStorage(STORAGE_KEYS.NVIDIA_4, token); } export function setE2BToken(token: string): boolean { return safeSetToStorage(STORAGE_KEYS.E2B, token); } export function setE2BToken2(token: string): boolean { return safeSetToStorage(STORAGE_KEYS.E2B_2, token); } export function setCfAiToken(token: string): boolean { return safeSetToStorage(STORAGE_KEYS.CLOUDFLARE, token); } // S-LOOP9: GitHub Models token — per override manuale (di default usa VITE_GITHUB_TOKEN da env) export function setGithubModelsToken(token: string): boolean { return safeSetToStorage(STORAGE_KEYS.GITHUB, token); } // ─── Clear all tokens ───────────────────────────────────────────────────── // ─── Telegram config (S-TGSEC: AES-GCM via safeGetFromStorage/safeSetToStorage) ────────────── export function getTelegramToken(): string | null { const t = safeGetFromStorage(STORAGE_KEYS.TELEGRAM_TOKEN); return t && t.trim() ? t.trim() : null; } export function getTelegramChatId(): string | null { const t = safeGetFromStorage(STORAGE_KEYS.TELEGRAM_CHAT_ID); return t && t.trim() ? t.trim() : null; } // P39-UX: Notion API Key — cifrata con AES-GCM-256 (sempre user-input, mai da env) export function getNotionToken(): string | null { const t = safeGetFromStorage(STORAGE_KEYS.NOTION); return t && t.trim() ? t.trim() : null; } /** * Salva il Telegram Bot Token cifrato con AES-GCM-256 (stesso layer delle chiavi AI). * Passare stringa vuota per cancellare. */ export function setTelegramToken(value: string): boolean { return safeSetToStorage(STORAGE_KEYS.TELEGRAM_TOKEN, value); } /** * Salva il Telegram Chat ID cifrato con AES-GCM-256. * Passare stringa vuota per cancellare. */ export function setTelegramChatId(value: string): boolean { return safeSetToStorage(STORAGE_KEYS.TELEGRAM_CHAT_ID, value); } /** Salva il Notion API Key cifrato con AES-GCM-256. Passare stringa vuota per cancellare. */ export function setNotionToken(value: string): boolean { return safeSetToStorage(STORAGE_KEYS.NOTION, value); } /** * S-KEYROT: Rotazione crittografica della chiave AES — per logout sicuro. * * Operazioni: * 1. Pulisce _tokenCache (valori in-memory non più accessibili) * 2. Rimuove tutti i valori cifrati da localStorage (nessun ciphertext rimane) * 3. Genera una NUOVA CryptoKey AES-GCM-256 non-estraibile e la salva in IndexedDB * (sovrascrive la vecchia — i vecchi ciphertext, anche se catturati prima del * logout, sono ora inutilizzabili perché la chiave è irrecuperabile) * 4. Aggiorna _aesKey in-memory con la nuova chiave (pronta per la prossima sessione) * * Sicurezza: l'attaccante che aveva uno snapshot di IndexedDB + localStorage PRIMA * del logout non può più decifrare nulla — chiave e ciphertext sono entrambi sostituiti. * Fallback: se IndexedDB non è disponibile, pulisce solo cache + localStorage. */ export async function revokeAndRotateAesKey(): Promise { // 1. Invalida cache in-memory immediatamente _tokenCache.clear(); _aesKey = null; // 2. Rimuovi tutti i ciphertext da localStorage for (const lsKey of Object.values(STORAGE_KEYS)) { try { localStorage.removeItem(lsKey); } catch { /* non-blocking */ } } // 3. Genera nuova CryptoKey e sovrascrivi la vecchia in IndexedDB try { const db = await _openAesKeyStore(); const newKey = await crypto.subtle.generateKey( { name: "AES-GCM", length: 256 }, false, // non-extractable — mai esposta come raw bytes ["encrypt", "decrypt"], ); await new Promise((resolve, reject) => { const tx = db.transaction(_AES_STORE, "readwrite"); tx.objectStore(_AES_STORE).put(newKey, _AES_KEY_ID); // sovrascrive la vecchia tx.oncomplete = () => { db.close(); resolve(); }; tx.onerror = () => { db.close(); reject(tx.error); }; }); _aesKey = newKey; // pronta per la prossima scrittura post-revoke } catch { // IndexedDB non disponibile — chiave non ruotata, ma localStorage è già pulito _aesKey = null; } } export function clearAllTokens(): void { _tokenCache.clear(); // ARCH-2: pulisci anche cache in-memory Object.values(STORAGE_KEYS).forEach(key => { try { localStorage.removeItem(key); } catch { /* ignore */ } }); } // ─── Debug: token source info ───────────────────────────────────────────── /** STORAGE-ALIGN: mappa completa — tutti gli slot multi-key inclusi. */ export function isTokenFromEnvironment(provider: keyof typeof STORAGE_KEYS): boolean { const envMap: Record = { HUGGINGFACE: ENV.HF_KEY, GROQ: ENV.GROQ_KEY, GROQ_2: ENV.GROQ_KEY_2, GROQ_3: ENV.GROQ_KEY_3, GROQ_4: ENV.GROQ_KEY_4, OPENROUTER: ENV.OPENROUTER_KEY, OPENROUTER_B: ENV.OPENROUTER_KEY_B, OPENROUTER_C: ENV.OPENROUTER_KEY_C, OPENROUTER_D: ENV.OPENROUTER_KEY_D, OPENAI: ENV.OPENAI_KEY, TOGETHER: ENV.TOGETHER_KEY, GEMINI: ENV.GEMINI_KEY, GEMINI_B: ENV.GEMINI_KEY_B, GEMINI_C: ENV.GEMINI_KEY_C, GEMINI_D: ENV.GEMINI_KEY_D, CLOUDFLARE: (ENV.CF_ACCOUNT_ID && ENV.CF_AI_TOKEN) ? "configured" : "", CEREBRAS: ENV.CEREBRAS_KEY, SAMBANOVA: ENV.SAMBANOVA_KEY, GITHUB: ENV.GITHUB_TOKEN, NVIDIA: ENV.NVIDIA_KEY, NVIDIA_2: ENV.NVIDIA_KEY_B, NVIDIA_3: ENV.NVIDIA_KEY_C, NVIDIA_4: ENV.NVIDIA_KEY_D, E2B: ENV.E2B_KEY, E2B_2: ENV.E2B_KEY_2, TELEGRAM_TOKEN: "", TELEGRAM_CHAT_ID: "", NOTION: "", }; return !!envMap[provider]; } /** STORAGE-ALIGN: lista completa con tutti i provider e slot multi-key. */ export function getAllConfiguredTokens(): Record { return { huggingface: !!getHuggingFaceToken(), groq: !!getGroqToken("1"), "groq-2": !!getGroqToken("2"), "groq-3": !!getGroqToken("3"), "groq-4": !!getGroqToken("4"), openrouter: !!getOpenRouterToken("1"), "openrouter-b": !!getOpenRouterToken("2"), "openrouter-c": !!getOpenRouterToken("3"), "openrouter-d": !!getOpenRouterToken("4"), openai: !!getOpenAIToken(), together: !!getTogetherToken(), gemini: !!getGeminiToken("1"), "gemini-b": !!getGeminiToken("2"), "gemini-c": !!getGeminiToken("3"), "gemini-d": !!getGeminiToken("4"), cerebras: !!getCerebrasToken(), sambanova: !!getSambanovaToken(), "github-models": !!getGithubModelsToken(), "cf-workers": !!getCfAiToken(), nvidia: !!getNvidiaToken("1"), "nvidia-2": !!getNvidiaToken("2"), "nvidia-3": !!getNvidiaToken("3"), "nvidia-4": !!getNvidiaToken("4"), "e2b": !!getE2BToken("1"), "e2b-2": !!getE2BToken("2"), notion: !!getNotionToken(), }; } // ─── Listener per cambiamenti token (usato da UI) ───────────────────────── type TokenChangeListener = (provider: string, token: string | null) => void; const listeners = new Set(); export function onTokenChange(fn: TokenChangeListener): () => void { listeners.add(fn); return () => listeners.delete(fn); } function emitTokenChange(provider: string, token: string | null): void { listeners.forEach(fn => fn(provider, token)); } // ─── Setters con notifica ────────────────────────────────────────────────── export function setHuggingFaceTokenWithNotification(token: string): boolean { const result = setHuggingFaceToken(token); if (result) emitTokenChange("huggingface", token || null); return result; } export function setGroqTokenWithNotification(token: string): boolean { const result = setGroqToken(token); if (result) emitTokenChange("groq", token || null); return result; } export function setOpenRouterTokenWithNotification(token: string): boolean { const result = setOpenRouterToken(token); if (result) emitTokenChange("openrouter", token || null); return result; } export function setOpenAITokenWithNotification(token: string): boolean { const result = setOpenAIToken(token); if (result) emitTokenChange("openai", token || null); return result; } export function setTogetherTokenWithNotification(token: string): boolean { const result = setTogetherToken(token); if (result) emitTokenChange("together", token || null); return result; } export function setGeminiTokenWithNotification(token: string): boolean { const result = setGeminiToken(token); if (result) emitTokenChange("gemini", token || null); return result; } export function setCerebrasTokenWithNotification(token: string): boolean { const result = setCerebrasToken(token); if (result) emitTokenChange("cerebras", token || null); return result; } export function setSambanovaTokenWithNotification(token: string): boolean { const result = setSambanovaToken(token); if (result) emitTokenChange("sambanova", token || null); return result; } export function setNotionTokenWithNotification(token: string): boolean { const result = setNotionToken(token); if (result) emitTokenChange("notion", token || null); return result; }