- Master password derives an AES-GCM-256 key via PBKDF2-SHA256; the key never leaves the device and the remote only sees ciphertext blobs. - One file per entry under entries/; conflicts produce a sidecar the user resolves in the UI. - Vault stays unlocked across background-script suspensions via storage.session; auto-locks after 5 min of inactivity.
85 lines
2.5 KiB
TypeScript
85 lines
2.5 KiB
TypeScript
// Filesystem wrapper around lightning-fs (IndexedDB-backed). isomorphic-git
|
|
// needs the instance itself; our code uses the Promises-style helpers.
|
|
|
|
import LightningFS from '@isomorphic-git/lightning-fs';
|
|
|
|
const inst = new LightningFS('passchmop-repo');
|
|
|
|
export const fs = inst;
|
|
export const pfs = inst.promises;
|
|
export const dir = '/repo';
|
|
export const entriesDir = `${dir}/entries`;
|
|
|
|
interface FsError extends Error { code?: string }
|
|
|
|
export async function ensureDir(path: string): Promise<void> {
|
|
try {
|
|
await pfs.mkdir(path);
|
|
} catch (e) {
|
|
if ((e as FsError).code !== 'EEXIST') throw e;
|
|
}
|
|
}
|
|
|
|
export async function listEntryFiles(): Promise<string[]> {
|
|
try {
|
|
const names = await pfs.readdir(entriesDir);
|
|
return names.filter(n => n.endsWith('.enc'));
|
|
} catch (e) {
|
|
if ((e as FsError).code === 'ENOENT') return [];
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
export async function readEntryFile(filename: string): Promise<Uint8Array> {
|
|
const buf = await pfs.readFile(`${entriesDir}/${filename}`);
|
|
if (typeof buf === 'string') throw new Error('unexpected string read for entry file');
|
|
return new Uint8Array(buf);
|
|
}
|
|
|
|
export async function writeEntryFile(filename: string, bytes: Uint8Array): Promise<void> {
|
|
await ensureDir(entriesDir);
|
|
await pfs.writeFile(`${entriesDir}/${filename}`, bytes);
|
|
}
|
|
|
|
export async function removeEntryFile(filename: string): Promise<void> {
|
|
try {
|
|
await pfs.unlink(`${entriesDir}/${filename}`);
|
|
} catch (e) {
|
|
if ((e as FsError).code !== 'ENOENT') throw e;
|
|
}
|
|
}
|
|
|
|
export async function readJson<T = unknown>(path: string): Promise<T | null> {
|
|
try {
|
|
const buf = await pfs.readFile(`${dir}/${path}`, { encoding: 'utf8' });
|
|
const text = typeof buf === 'string' ? buf : new TextDecoder().decode(buf);
|
|
return JSON.parse(text) as T;
|
|
} catch (e) {
|
|
if ((e as FsError).code === 'ENOENT') return null;
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
export async function writeJson(path: string, obj: unknown): Promise<void> {
|
|
await ensureDir(dir);
|
|
await pfs.writeFile(`${dir}/${path}`, JSON.stringify(obj, null, 2), 'utf8');
|
|
}
|
|
|
|
export async function wipeRepo(): Promise<void> {
|
|
async function rm(path: string): Promise<void> {
|
|
try {
|
|
const stat = await pfs.stat(path);
|
|
if (stat.isDirectory()) {
|
|
const kids = await pfs.readdir(path);
|
|
for (const k of kids) await rm(`${path}/${k}`);
|
|
await pfs.rmdir(path);
|
|
} else {
|
|
await pfs.unlink(path);
|
|
}
|
|
} catch (e) {
|
|
if ((e as FsError).code !== 'ENOENT') throw e;
|
|
}
|
|
}
|
|
await rm(dir);
|
|
}
|