feat(ext/sw): typed-item vault ops via SessionHandle
This commit is contained in:
@@ -1,34 +1,26 @@
|
|||||||
/// Vault operations module.
|
/// Typed-item vault operations. All calls are handle-keyed — the master key
|
||||||
///
|
/// never crosses the WASM boundary.
|
||||||
/// 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 { SessionHandle } from '../../wasm/relicario_wasm';
|
||||||
import type { GitHost } from './git-host';
|
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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
let wasm: any = null;
|
let wasm: any = null;
|
||||||
|
|
||||||
/// Store the WASM module reference after initialization.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function setWasm(w: any): void {
|
export function setWasm(w: any): void { wasm = w; }
|
||||||
wasm = w;
|
|
||||||
}
|
|
||||||
|
|
||||||
function requireWasm(): any {
|
function requireWasm(): any {
|
||||||
if (!wasm) throw new Error('WASM module not initialized');
|
if (!wasm) throw new Error('WASM module not initialized');
|
||||||
return wasm;
|
return wasm;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Vault metadata: salt and KDF params stored unencrypted in the repo.
|
|
||||||
export interface VaultMeta {
|
export interface VaultMeta {
|
||||||
salt: Uint8Array;
|
salt: Uint8Array;
|
||||||
paramsJson: string;
|
paramsJson: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read the vault salt and KDF params from the git repo.
|
|
||||||
export async function fetchVaultMeta(git: GitHost): Promise<VaultMeta> {
|
export async function fetchVaultMeta(git: GitHost): Promise<VaultMeta> {
|
||||||
const saltBytes = await git.readFile('.relicario/salt');
|
const saltBytes = await git.readFile('.relicario/salt');
|
||||||
const paramsRaw = await git.readFile('.relicario/params.json');
|
const paramsRaw = await git.readFile('.relicario/params.json');
|
||||||
@@ -36,102 +28,110 @@ export async function fetchVaultMeta(git: GitHost): Promise<VaultMeta> {
|
|||||||
return { salt: saltBytes, paramsJson };
|
return { salt: saltBytes, paramsJson };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch and decrypt the manifest from the git repo.
|
// --- Manifest ---
|
||||||
|
|
||||||
export async function fetchAndDecryptManifest(
|
export async function fetchAndDecryptManifest(
|
||||||
git: GitHost,
|
git: GitHost,
|
||||||
masterKey: Uint8Array,
|
handle: SessionHandle,
|
||||||
): Promise<Manifest> {
|
): Promise<Manifest> {
|
||||||
const w = requireWasm();
|
const w = requireWasm();
|
||||||
const ciphertext = await git.readFile('manifest.enc');
|
const ciphertext = await git.readFile('manifest.enc');
|
||||||
const json = w.decrypt_manifest(ciphertext, masterKey);
|
return w.manifest_decrypt(handle, ciphertext) as Manifest;
|
||||||
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(
|
export async function encryptAndWriteManifest(
|
||||||
git: GitHost,
|
git: GitHost,
|
||||||
masterKey: Uint8Array,
|
handle: SessionHandle,
|
||||||
manifest: Manifest,
|
manifest: Manifest,
|
||||||
message: string,
|
message: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const w = requireWasm();
|
const w = requireWasm();
|
||||||
const manifestJson = JSON.stringify(manifest);
|
const ciphertext = w.manifest_encrypt(handle, JSON.stringify(manifest));
|
||||||
const ciphertext = w.encrypt_manifest(manifestJson, masterKey);
|
|
||||||
await git.writeFile('manifest.enc', ciphertext, message);
|
await git.writeFile('manifest.enc', ciphertext, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Filter manifest entries by group (case-insensitive). If no group given, returns all.
|
// --- Items ---
|
||||||
export function listEntries(
|
|
||||||
|
export async function fetchAndDecryptItem(
|
||||||
|
git: GitHost,
|
||||||
|
handle: SessionHandle,
|
||||||
|
id: ItemId,
|
||||||
|
): Promise<Item> {
|
||||||
|
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<void> {
|
||||||
|
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<VaultSettings> {
|
||||||
|
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<void> {
|
||||||
|
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,
|
manifest: Manifest,
|
||||||
group?: string,
|
group?: string,
|
||||||
): Array<[string, ManifestEntry]> {
|
): Array<[ItemId, ManifestEntry]> {
|
||||||
const entries = Object.entries(manifest.entries);
|
const entries = Object.entries(manifest.items) as Array<[ItemId, ManifestEntry]>;
|
||||||
if (!group) return entries;
|
// 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();
|
const g = group.toLowerCase();
|
||||||
return entries.filter(([, e]) =>
|
return live.filter(([, e]) => e.group?.toLowerCase() === g);
|
||||||
e.group?.toLowerCase() === g
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Case-insensitive substring search on name, url, and username.
|
export function searchItems(
|
||||||
export function searchEntries(
|
|
||||||
manifest: Manifest,
|
manifest: Manifest,
|
||||||
query: string,
|
query: string,
|
||||||
): Array<[string, ManifestEntry]> {
|
): Array<[ItemId, ManifestEntry]> {
|
||||||
const q = query.toLowerCase();
|
const q = query.toLowerCase();
|
||||||
return Object.entries(manifest.entries).filter(([, e]) => {
|
return (Object.entries(manifest.items) as Array<[ItemId, ManifestEntry]>)
|
||||||
if (e.name.toLowerCase().includes(q)) return true;
|
.filter(([, e]) => e.trashed_at === undefined)
|
||||||
if (e.url?.toLowerCase().includes(q)) return true;
|
.filter(([, e]) => {
|
||||||
if (e.username?.toLowerCase().includes(q)) return true;
|
if (e.title.toLowerCase().includes(q)) return true;
|
||||||
return false;
|
if (e.tags.some((t) => t.toLowerCase().includes(q))) return true;
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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;
|
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.
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user