/// Typed-item vault operations. All calls are handle-keyed — the master key /// never crosses the WASM boundary. import type { SessionHandle } from '../../wasm/relicario_wasm'; import type { GitHost } from './git-host'; import type { Item, ItemId, Manifest, ManifestEntry, VaultSettings } from '../shared/types'; // eslint-disable-next-line @typescript-eslint/no-explicit-any let wasm: any = null; // eslint-disable-next-line @typescript-eslint/no-explicit-any export function setWasm(w: any): void { wasm = w; } function requireWasm(): any { if (!wasm) throw new Error('WASM module not initialized'); return wasm; } export interface VaultMeta { salt: Uint8Array; paramsJson: string; } export async function fetchVaultMeta(git: GitHost): Promise { const saltBytes = await git.readFile('.relicario/salt'); const paramsRaw = await git.readFile('.relicario/params.json'); const paramsJson = new TextDecoder().decode(paramsRaw); return { salt: saltBytes, paramsJson }; } // --- Manifest --- export async function fetchAndDecryptManifest( git: GitHost, handle: SessionHandle, ): Promise { const w = requireWasm(); const ciphertext = await git.readFile('manifest.enc'); return w.manifest_decrypt(handle, ciphertext) as Manifest; } export async function encryptAndWriteManifest( git: GitHost, handle: SessionHandle, manifest: Manifest, message: string, ): Promise { const w = requireWasm(); const ciphertext = w.manifest_encrypt(handle, JSON.stringify(manifest)); await git.writeFile('manifest.enc', ciphertext, message); } // --- Items --- export async function fetchAndDecryptItem( git: GitHost, handle: SessionHandle, id: ItemId, ): Promise { const w = requireWasm(); const ciphertext = await git.readFile(`items/${id}.enc`); return w.item_decrypt(handle, ciphertext) as Item; } export async function encryptAndWriteItem( git: GitHost, handle: SessionHandle, id: ItemId, item: Item, message: string, ): Promise { const w = requireWasm(); const ciphertext = w.item_encrypt(handle, JSON.stringify(item)); await git.writeFile(`items/${id}.enc`, ciphertext, message); } // --- Settings (the α subset the SW reads/writes is autofill_origin_acks) --- export async function fetchAndDecryptSettings( git: GitHost, handle: SessionHandle, ): Promise { const w = requireWasm(); const ciphertext = await git.readFile('settings.enc'); return w.settings_decrypt(handle, ciphertext) as VaultSettings; } export async function encryptAndWriteSettings( git: GitHost, handle: SessionHandle, settings: VaultSettings, message: string, ): Promise { const w = requireWasm(); const ciphertext = w.settings_encrypt(handle, JSON.stringify(settings)); await git.writeFile('settings.enc', ciphertext, message); } // --- In-memory manifest helpers --- export function listItems( manifest: Manifest, group?: string, ): Array<[ItemId, ManifestEntry]> { const entries = Object.entries(manifest.items) as Array<[ItemId, ManifestEntry]>; // Hide trashed items from the default list view. const live = entries.filter(([, e]) => e.trashed_at === undefined); if (!group) return live; const g = group.toLowerCase(); return live.filter(([, e]) => e.group?.toLowerCase() === g); } export function searchItems( manifest: Manifest, query: string, ): Array<[ItemId, ManifestEntry]> { const q = query.toLowerCase(); return (Object.entries(manifest.items) as Array<[ItemId, ManifestEntry]>) .filter(([, e]) => e.trashed_at === undefined) .filter(([, e]) => { if (e.title.toLowerCase().includes(q)) return true; if (e.tags.some((t) => t.toLowerCase().includes(q))) return true; return false; }); } /// Match manifest entries against a page hostname. /// /// icon_hint is derived by the Rust core (crates/relicario-core/src/manifest.rs) /// from LoginCore.url's hostname, so equality on icon_hint is the cheapest match. /// α is intentionally coarse: no www.-stripping, no public-suffix matching /// (`www.github.com` saved items will not match `github.com`, and vice versa). /// Tighter matching is a 1C-β/γ concern. export function findByHostname( manifest: Manifest, hostname: string, ): Array<[ItemId, ManifestEntry]> { const h = hostname.toLowerCase(); return (Object.entries(manifest.items) as Array<[ItemId, ManifestEntry]>) .filter(([, e]) => e.trashed_at === undefined) .filter(([, e]) => (e.icon_hint ?? '').toLowerCase() === h); }