import { Stronghold, type Client } from '@tauri-apps/plugin-stronghold'; import { appDataDir, join } from '@tauri-apps/api/path'; export interface CredentialRecord { id: string; origin: string; username: string; password: string; label?: string; createdAt: number; updatedAt: number; } export interface CredentialSummary { id: string; origin: string; username: string; label?: string; updatedAt: number; } const CLIENT_NAME = 'musealpha-credentials'; const INDEX_KEY = 'cred:index'; let stronghold: Stronghold | null = null; let client: Client | null = null; const enc = new TextEncoder(); const dec = new TextDecoder(); function bytes(v: string): number[] { return Array.from(enc.encode(v)); } function text(v: number[] | Uint8Array): string { return dec.decode(new Uint8Array(v)); } async function vaultPath() { return await join(await appDataDir(), 'credentials.stronghold'); } export async function unlockVault(masterPassword: string) { const path = await vaultPath(); stronghold = await Stronghold.load(path, masterPassword); try { client = await stronghold.loadClient(CLIENT_NAME); } catch { client = await stronghold.createClient(CLIENT_NAME); await stronghold.save(); } } export function isVaultUnlocked() { return !!stronghold && !!client; } export function lockVault() { stronghold = null; client = null; } function ensureClient(): Client { if (!client || !stronghold) throw new Error('Password vault is locked'); return client; } async function loadIndex(): Promise { const store = ensureClient().getStore(); const raw = await store.get(INDEX_KEY); if (!raw) return []; try { return JSON.parse(text(raw)); } catch { return []; } } async function saveIndex(ids: string[]) { const store = ensureClient().getStore(); await store.insert(INDEX_KEY, bytes(JSON.stringify([...new Set(ids)]))); await stronghold!.save(); } function key(id: string) { return `cred:${id}`; } export async function saveCredential(input: Omit & { id?: string }) { const store = ensureClient().getStore(); const now = Date.now(); const id = input.id || crypto.randomUUID(); const existing = input.id ? await getCredential(input.id).catch(() => null) : null; const record: CredentialRecord = { id, origin: normalizeOrigin(input.origin), username: input.username, password: input.password, label: input.label, createdAt: existing?.createdAt || now, updatedAt: now, }; await store.insert(key(id), bytes(JSON.stringify(record))); const idx = await loadIndex(); await saveIndex([...idx, id]); await stronghold!.save(); return record; } export async function getCredential(id: string): Promise { const store = ensureClient().getStore(); const raw = await store.get(key(id)); if (!raw) return null; return JSON.parse(text(raw)); } export async function listCredentials(origin?: string): Promise { const idx = await loadIndex(); const normalized = origin ? normalizeOrigin(origin) : null; const out: CredentialSummary[] = []; for (const id of idx) { const r = await getCredential(id).catch(() => null); if (!r) continue; if (normalized && r.origin !== normalized) continue; out.push({ id: r.id, origin: r.origin, username: r.username, label: r.label, updatedAt: r.updatedAt }); } return out.sort((a, b) => b.updatedAt - a.updatedAt); } export async function deleteCredential(id: string) { const store = ensureClient().getStore(); await store.remove(key(id)); const idx = await loadIndex(); await saveIndex(idx.filter(x => x !== id)); await stronghold!.save(); } export function normalizeOrigin(input: string): string { try { const u = new URL(input.includes('://') ? input : `https://${input}`); return `${u.protocol}//${u.host}`.toLowerCase(); } catch { return input.trim().toLowerCase(); } }