AUDIT / src /lib /apiKeys.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 3)
95eb75a verified
Raw
History Blame Contribute Delete
36.9 kB
/**
* 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;
}