Spaces:
Sleeping
Sleeping
| /** | |
| * 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<string, string>(); | |
| /** 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<void> | 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<IDBDatabase> { | |
| 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<CryptoKey> { | |
| 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<string> { | |
| 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<string | null> { | |
| 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<void> { | |
| if (_initPromise) return _initPromise; | |
| // Init non ancora avviato — ritorna promise risolta (token saranno plaintext/XOR) | |
| return Promise.resolve(); | |
| } | |
| export async function initApiKeyEncryption(): Promise<void> { | |
| 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<void>(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<void> { | |
| // 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<void>((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<keyof typeof STORAGE_KEYS, string> = { | |
| 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<string, boolean> { | |
| 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<TokenChangeListener>(); | |
| 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; | |
| } | |