From bd9dd206ac2c339a79866b7c0b69cb461e17d9f1 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Mon, 20 Apr 2026 19:53:28 -0400 Subject: [PATCH] feat(ext/sw): typed-item vault ops via SessionHandle --- extension/src/service-worker/vault.ts | 172 +++++++++++++------------- 1 file changed, 86 insertions(+), 86 deletions(-) diff --git a/extension/src/service-worker/vault.ts b/extension/src/service-worker/vault.ts index cb1bc65..e978d0e 100644 --- a/extension/src/service-worker/vault.ts +++ b/extension/src/service-worker/vault.ts @@ -1,34 +1,26 @@ -/// Vault operations module. -/// -/// Bridges the WASM crypto functions with the git host API to provide -/// high-level vault operations: fetch/decrypt manifest, fetch/decrypt entries, -/// encrypt/write entries, search, and URL matching. +/// 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 { Entry, Manifest, ManifestEntry } from '../shared/types'; +import type { Item, ItemId, Manifest, ManifestEntry, VaultSettings } from '../shared/types'; -// WASM module reference — set once during init. // eslint-disable-next-line @typescript-eslint/no-explicit-any let wasm: any = null; -/// Store the WASM module reference after initialization. // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function setWasm(w: any): void { - wasm = w; -} +export function setWasm(w: any): void { wasm = w; } function requireWasm(): any { if (!wasm) throw new Error('WASM module not initialized'); return wasm; } -/// Vault metadata: salt and KDF params stored unencrypted in the repo. export interface VaultMeta { salt: Uint8Array; paramsJson: string; } -/// Read the vault salt and KDF params from the git repo. export async function fetchVaultMeta(git: GitHost): Promise { const saltBytes = await git.readFile('.relicario/salt'); const paramsRaw = await git.readFile('.relicario/params.json'); @@ -36,102 +28,110 @@ export async function fetchVaultMeta(git: GitHost): Promise { return { salt: saltBytes, paramsJson }; } -/// Fetch and decrypt the manifest from the git repo. +// --- Manifest --- + export async function fetchAndDecryptManifest( git: GitHost, - masterKey: Uint8Array, + handle: SessionHandle, ): Promise { const w = requireWasm(); const ciphertext = await git.readFile('manifest.enc'); - const json = w.decrypt_manifest(ciphertext, masterKey); - return JSON.parse(json) as Manifest; + return w.manifest_decrypt(handle, ciphertext) as Manifest; } -/// Fetch and decrypt a single entry from the git repo. -export async function fetchAndDecryptEntry( - git: GitHost, - masterKey: Uint8Array, - id: string, -): Promise { - const w = requireWasm(); - const ciphertext = await git.readFile(`entries/${id}.enc`); - const json = w.decrypt_entry(ciphertext, masterKey); - return JSON.parse(json) as Entry; -} - -/// Encrypt an entry and write it to the git repo. -export async function encryptAndWriteEntry( - git: GitHost, - masterKey: Uint8Array, - id: string, - entry: Entry, - message: string, -): Promise { - const w = requireWasm(); - const entryJson = JSON.stringify(entry); - const ciphertext = w.encrypt_entry(entryJson, masterKey); - await git.writeFile(`entries/${id}.enc`, ciphertext, message); -} - -/// Encrypt the manifest and write it to the git repo. export async function encryptAndWriteManifest( git: GitHost, - masterKey: Uint8Array, + handle: SessionHandle, manifest: Manifest, message: string, ): Promise { const w = requireWasm(); - const manifestJson = JSON.stringify(manifest); - const ciphertext = w.encrypt_manifest(manifestJson, masterKey); + const ciphertext = w.manifest_encrypt(handle, JSON.stringify(manifest)); await git.writeFile('manifest.enc', ciphertext, message); } -/// Filter manifest entries by group (case-insensitive). If no group given, returns all. -export function listEntries( +// --- 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<[string, ManifestEntry]> { - const entries = Object.entries(manifest.entries); - if (!group) return entries; +): 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 entries.filter(([, e]) => - e.group?.toLowerCase() === g - ); + return live.filter(([, e]) => e.group?.toLowerCase() === g); } -/// Case-insensitive substring search on name, url, and username. -export function searchEntries( +export function searchItems( manifest: Manifest, query: string, -): Array<[string, ManifestEntry]> { +): Array<[ItemId, ManifestEntry]> { const q = query.toLowerCase(); - return Object.entries(manifest.entries).filter(([, e]) => { - if (e.name.toLowerCase().includes(q)) return true; - if (e.url?.toLowerCase().includes(q)) return true; - if (e.username?.toLowerCase().includes(q)) return true; - return false; - }); -} - -/// Find entries whose URL matches the given page URL by hostname. -export function findByUrl( - manifest: Manifest, - url: string, -): Array<[string, ManifestEntry]> { - let hostname: string; - try { - hostname = new URL(url).hostname; - } catch { - return []; - } - - return Object.entries(manifest.entries).filter(([, e]) => { - if (!e.url) return false; - try { - const entryHost = new URL(e.url).hostname; - return entryHost === hostname; - } catch { + 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; - } - }); + }); +} + +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); + // icon_hint is derived by Rust core from LoginCore.url's hostname, + // so hostname equality on icon_hint is the cheapest match. }