Files
relicario/extension/src/service-worker/vault.ts
adlee-was-taken 336e90fc84 fix: use static import + initSync for WASM in service worker
Chrome MV3 service workers do not support dynamic import().
Switch to static import of the wasm-pack JS glue and use
initSync() with fetch() to load the WASM binary at runtime.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 11:37:44 -04:00

138 lines
4.0 KiB
TypeScript

/// 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.
import type { GitHost } from './git-host';
import type { Entry, Manifest, ManifestEntry } 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;
}
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<VaultMeta> {
const saltBytes = await git.readFile('.idfoto/salt');
const paramsRaw = await git.readFile('.idfoto/params.json');
const paramsJson = new TextDecoder().decode(paramsRaw);
return { salt: saltBytes, paramsJson };
}
/// Fetch and decrypt the manifest from the git repo.
export async function fetchAndDecryptManifest(
git: GitHost,
masterKey: Uint8Array,
): Promise<Manifest> {
const w = requireWasm();
const ciphertext = await git.readFile('manifest.enc');
const json = w.decrypt_manifest(ciphertext, masterKey);
return JSON.parse(json) as Manifest;
}
/// Fetch and decrypt a single entry from the git repo.
export async function fetchAndDecryptEntry(
git: GitHost,
masterKey: Uint8Array,
id: string,
): Promise<Entry> {
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<void> {
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,
manifest: Manifest,
message: string,
): Promise<void> {
const w = requireWasm();
const manifestJson = JSON.stringify(manifest);
const ciphertext = w.encrypt_manifest(manifestJson, masterKey);
await git.writeFile('manifest.enc', ciphertext, message);
}
/// Filter manifest entries by group (case-insensitive). If no group given, returns all.
export function listEntries(
manifest: Manifest,
group?: string,
): Array<[string, ManifestEntry]> {
const entries = Object.entries(manifest.entries);
if (!group) return entries;
const g = group.toLowerCase();
return entries.filter(([, e]) =>
e.group?.toLowerCase() === g
);
}
/// Case-insensitive substring search on name, url, and username.
export function searchEntries(
manifest: Manifest,
query: string,
): Array<[string, 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 false;
}
});
}