/* * keys.ts * Zyquo Cloud Web * * Author: Simon-Pierre Boucher * Mail: contact@spboucher.ai * * Provider API key storage — plain localStorage under zyquo.cloud.web.keys, * namespaced JSON map. This module (and the rest of storage/) is the ONLY * place that touches localStorage. Keys never leave the device except inside * a provider request the user initiated. Optional passphrase lock (WebCrypto * AES-GCM) lives in vault.ts and wraps this map at rest. */ import type { Provider } from '../types' const KEYS_KEY = 'zyquo.cloud.web.keys' export type KeyMap = Partial> /** In-memory overlay used while the passphrase lock is active (Phase 3). */ let unlockedOverlay: KeyMap | null = null export function setUnlockedOverlay(map: KeyMap | null): void { unlockedOverlay = map } export function isOverlayActive(): boolean { return unlockedOverlay !== null } export function readKeyMap(): KeyMap { if (unlockedOverlay) return { ...unlockedOverlay } try { const raw = localStorage.getItem(KEYS_KEY) if (!raw) return {} const parsed: unknown = JSON.parse(raw) if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { return parsed as KeyMap } return {} } catch { return {} } } function writeKeyMap(map: KeyMap): void { if (unlockedOverlay) { unlockedOverlay = { ...map } return } localStorage.setItem(KEYS_KEY, JSON.stringify(map)) } export function getKey(provider: Provider): string | undefined { const value = readKeyMap()[provider] return value && value !== '' ? value : undefined } export function setKey(provider: Provider, key: string): void { const map = readKeyMap() map[provider] = key writeKeyMap(map) } export function removeKey(provider: Provider): void { const map = readKeyMap() delete map[provider] writeKeyMap(map) } export function hasKey(provider: Provider): boolean { return getKey(provider) !== undefined } export function configuredProviders(): Provider[] { return Object.entries(readKeyMap()) .filter(([, v]) => typeof v === 'string' && v !== '') .map(([k]) => k as Provider) } /** Masked display: last 4 characters only (e.g. "••••••••Q3ew"). */ export function maskedKey(key: string): string { if (key.length <= 4) return '••••' return `${'•'.repeat(8)}${key.slice(-4)}` } /** Replaces the whole map (import / passphrase unlock). */ export function replaceKeyMap(map: KeyMap): void { writeKeyMap(map) } export function clearAllKeys(): void { if (unlockedOverlay) { unlockedOverlay = {} } localStorage.removeItem(KEYS_KEY) } /** Raw ciphertext/plaintext blob accessors for the passphrase vault (Phase 3). */ export function rawKeysRecord(): string | null { return localStorage.getItem(KEYS_KEY) } export function setRawKeysRecord(value: string): void { localStorage.setItem(KEYS_KEY, value) }