spb/zyquo-cloud-web Public MIT
Zyquo Cloud Web — every cloud model, one beautiful chat, entirely in your browser.
TypeScript 81.9%
CSS 8.9%
JavaScript 7.5%
Shell 1.1%
HTML 0.6%
1/*2 * keys.ts3 * Zyquo Cloud Web4 *5 * Author: Simon-Pierre Boucher6 * Mail: contact@spboucher.ai7 *8 * Provider API key storage — plain localStorage under zyquo.cloud.web.keys,9 * namespaced JSON map. This module (and the rest of storage/) is the ONLY10 * place that touches localStorage. Keys never leave the device except inside11 * a provider request the user initiated. Optional passphrase lock (WebCrypto12 * AES-GCM) lives in vault.ts and wraps this map at rest.13 */1415import type { Provider } from '../types'1617const KEYS_KEY = 'zyquo.cloud.web.keys'1819export type KeyMap = Partial<Record<Provider, string>>2021/** In-memory overlay used while the passphrase lock is active (Phase 3). */22let unlockedOverlay: KeyMap | null = null2324export function setUnlockedOverlay(map: KeyMap | null): void {25 unlockedOverlay = map26}2728export function isOverlayActive(): boolean {29 return unlockedOverlay !== null30}3132export function readKeyMap(): KeyMap {33 if (unlockedOverlay) return { ...unlockedOverlay }34 try {35 const raw = localStorage.getItem(KEYS_KEY)36 if (!raw) return {}37 const parsed: unknown = JSON.parse(raw)38 if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {39 return parsed as KeyMap40 }41 return {}42 } catch {43 return {}44 }45}4647function writeKeyMap(map: KeyMap): void {48 if (unlockedOverlay) {49 unlockedOverlay = { ...map }50 return51 }52 localStorage.setItem(KEYS_KEY, JSON.stringify(map))53}5455export function getKey(provider: Provider): string | undefined {56 const value = readKeyMap()[provider]57 return value && value !== '' ? value : undefined58}5960export function setKey(provider: Provider, key: string): void {61 const map = readKeyMap()62 map[provider] = key63 writeKeyMap(map)64}6566export function removeKey(provider: Provider): void {67 const map = readKeyMap()68 delete map[provider]69 writeKeyMap(map)70}7172export function hasKey(provider: Provider): boolean {73 return getKey(provider) !== undefined74}7576export function configuredProviders(): Provider[] {77 return Object.entries(readKeyMap())78 .filter(([, v]) => typeof v === 'string' && v !== '')79 .map(([k]) => k as Provider)80}8182/** Masked display: last 4 characters only (e.g. "••••••••Q3ew"). */83export function maskedKey(key: string): string {84 if (key.length <= 4) return '••••'85 return `${'•'.repeat(8)}${key.slice(-4)}`86}8788/** Replaces the whole map (import / passphrase unlock). */89export function replaceKeyMap(map: KeyMap): void {90 writeKeyMap(map)91}9293export function clearAllKeys(): void {94 if (unlockedOverlay) {95 unlockedOverlay = {}96 }97 localStorage.removeItem(KEYS_KEY)98}99100/** Raw ciphertext/plaintext blob accessors for the passphrase vault (Phase 3). */101export function rawKeysRecord(): string | null {102 return localStorage.getItem(KEYS_KEY)103}104105export function setRawKeysRecord(value: string): void {106 localStorage.setItem(KEYS_KEY, value)107}108