musealpha / src /credentialsVault.ts
asdf98's picture
Upload 112 files
3d7d9b5 verified
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<string[]> {
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<CredentialRecord, 'id' | 'createdAt' | 'updatedAt'> & { 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<CredentialRecord | null> {
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<CredentialSummary[]> {
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();
}
}