import { base64FromBytes, bytesFromBase64, type SecretStore } from '@chat-app/shared/auth'; import { isTauriRuntime } from './globalShortcut'; import { makeStrongholdStore, migrateLocalStorageToStronghold } from './strongholdStore'; // Secret store with two backends: // - Tauri: Stronghold-encrypted vault file in appLocalDataDir. Survives app // reinstalls and is encrypted at rest with a password derived from the // authenticated user-id. // - Web / pre-auth: localStorage (legacy dev fallback). // // Callers don't need to care which one is active — they import a singleton // and call setSecretStoreUser(userId) once the session is known. Until that // happens, calls fall through to localStorage. const PREFIX = 'chatapp.secret:'; const localStore: SecretStore = { async getSecret(key: string): Promise { const raw = window.localStorage.getItem(PREFIX + key); if (!raw) return null; return bytesFromBase64(raw); }, async setSecret(key: string, value: Uint8Array): Promise { const encoded = await base64FromBytes(value); window.localStorage.setItem(PREFIX + key, encoded); }, async removeSecret(key: string): Promise { window.localStorage.removeItem(PREFIX + key); }, }; let activeBackend: SecretStore = localStore; let activeUserId: string | null = null; export async function setSecretStoreUser(userId: string | null): Promise { if (userId === activeUserId) return; activeUserId = userId; if (userId && isTauriRuntime()) { const stronghold = makeStrongholdStore(userId); try { // Force a tiny round-trip to verify Stronghold can actually open the // vault on this machine. If not (broken vault file, bundled rust crate // mismatch, etc.) we fall back to localStorage so the rest of the app // remains usable instead of bricking device registration. await stronghold.getSecret('__probe'); activeBackend = stronghold; try { await migrateLocalStorageToStronghold(userId, PREFIX); } catch (err: unknown) { console.warn('stronghold migration failed', err); } } catch (err: unknown) { console.warn('stronghold init failed — falling back to localStorage', err); activeBackend = localStore; } } else { activeBackend = localStore; } } // Singleton with stable identity — internals delegate to whichever backend is // currently active. Existing call-sites that imported `devLocalSecretStore` // keep working without changes. export const devLocalSecretStore: SecretStore = { async getSecret(key) { return activeBackend.getSecret(key); }, async setSecret(key, value) { return activeBackend.setSecret(key, value); }, async removeSecret(key) { return activeBackend.removeSecret(key); }, }; export function isStrongholdActive(): boolean { return activeBackend !== localStore; }