import { describe, expect, it } from "@suaveplan/testing/runner"; import { VAULT_CODES } from "../src/errors/codes.js"; import { VaultError } from "../src/errors/vault-error.js"; import type { DriveLike, DriveSharingLike, SyncDelegationResult, SyncSnapshot, VaultSyncLike, } from "../src/types/adapters.js"; import type { VaultConfig } from "../src/types/types.js"; import { Vault } from "../src/vault/vault.js"; function memoryDrive(): DriveLike & { blobs: Map } { const blobs = new Map(); return { blobs, async read(path) { const v = blobs.get(path); if (v === undefined) throw new Error(`missing ${path}`); return v; }, async write(path, data) { blobs.set(path, new Uint8Array(data)); }, async delete(path) { return blobs.delete(path); }, async exists(path) { return blobs.has(path); }, }; } function noopSharing(): DriveSharingLike { return { async createLink() { return { id: "x", url: "https://x.example/x" }; }, async revokeLink() {}, async isActive() { return true; }, }; } class CountingSync implements VaultSyncLike { public lastSnapshot: SyncSnapshot = { items: [], snapshotAt: 0 }; public hydrateCount = 0; public hydrate(s: SyncSnapshot): void { this.hydrateCount += 1; this.lastSnapshot = s; } public async pull(): Promise { return { merged: this.lastSnapshot.items.map((i) => i.id), conflicts: [], deleted: [], durationMs: 0, }; } public async push(): Promise { return { merged: this.lastSnapshot.items.map((i) => i.id), conflicts: [], deleted: [], durationMs: 0, }; } public async sync(): Promise { return { merged: this.lastSnapshot.items.map((i) => i.id), conflicts: [], deleted: [], durationMs: 0, }; } public currentSnapshot(): SyncSnapshot { return this.lastSnapshot; } } function makeConfig( drive = memoryDrive(), sync: VaultSyncLike = new CountingSync() ): VaultConfig { return { vaultId: "personal", email: "user@example.com", containerPath: "/personal.vault", attachmentsPrefix: "/personal/att", argon2id: { memoryKB: 65536, iterations: 3, parallelism: 1, outputLength: 32, }, argon2idSalt: new Uint8Array(16), drive, driveSharing: noopSharing(), sync, }; } describe("vault — integration", () => { it("end-to-end: unlock, create item with concealed field, lock, re-unlock, get item", { timeout: 30000, }, async () => { const drive = memoryDrive(); const config = makeConfig(drive); const vault = Vault.open(config); await vault.unlock("pass", "secret"); const created = vault.items.create({ title: "GitHub", category: "login", fields: { username: "alice", url: "https://github.com" }, concealedFields: { password: "p@ssw0rd" }, tags: ["work"], favorite: true, }); expect(created.concealedFields.password).toBe("p@ssw0rd"); await vault.flush(); await vault.lock(); expect(vault.isLocked).toBe(true); const v2 = Vault.open(config); await v2.unlock("pass", "secret"); const fetched = v2.items.get(created.id); expect(fetched).not.toBeNull(); expect(fetched?.title).toBe("GitHub"); expect(fetched?.concealedFields.password).toBe("p@ssw0rd"); await v2.lock(); }); it("wrong password rejects with DECRYPT_FAILED, vault stays locked", { timeout: 30000, }, async () => { const drive = memoryDrive(); const config = makeConfig(drive); const v1 = Vault.open(config); await v1.unlock("pass", "secret"); await v1.flush(); await v1.lock(); const v2 = Vault.open(config); try { await v2.unlock("wrong", "secret"); throw new Error("should have thrown"); } catch (err) { expect(VaultError.is(err)).toBe(true); expect((err as VaultError).code).toBe(VAULT_CODES.DECRYPT_FAILED); } expect(v2.isLocked).toBe(true); }); it("tampered drive blob rejects with DECRYPT_FAILED", { timeout: 30000, }, async () => { const drive = memoryDrive(); const config = makeConfig(drive); const v1 = Vault.open(config); await v1.unlock("pass", "secret"); await v1.flush(); await v1.lock(); const blob = drive.blobs.get(config.containerPath) as Uint8Array; const tampered = new Uint8Array(blob); tampered[tampered.length - 5] ^= 0x42; drive.blobs.set(config.containerPath, tampered); const v2 = Vault.open(config); try { await v2.unlock("pass", "secret"); throw new Error("should have thrown"); } catch (err) { expect(VaultError.is(err)).toBe(true); expect((err as VaultError).code).toBe(VAULT_CODES.DECRYPT_FAILED); } }); it("attachments survive lock + re-unlock through drive", { timeout: 30000, }, async () => { const drive = memoryDrive(); const config = makeConfig(drive); const vault = Vault.open(config); await vault.unlock("pass", "secret"); const data = new TextEncoder().encode("hello attachment"); const meta = await vault.attachments.upload({ itemId: "i", filename: "f.txt", mimeType: "text/plain", data, }); await vault.flush(); await vault.lock(); const v2 = Vault.open(config); await v2.unlock("pass", "secret"); // Attachment metadata is per-runtime; re-injecting is the // caller's job. We still verify the drive blob is decrypted // correctly when metadata is known. ( v2.attachments as unknown as { metadata: Map; } ).metadata.set(meta.id, meta); const recovered = await v2.attachments.download(meta.id); expect(new TextDecoder().decode(recovered)).toBe("hello attachment"); await v2.lock(); }); it("sync delegation works end-to-end through the orchestrator", { timeout: 30000, }, async () => { const sync = new CountingSync(); const drive = memoryDrive(); const vault = Vault.open(makeConfig(drive, sync)); await vault.unlock("pass", "secret"); vault.items.create({ title: "x", category: "login", fields: {}, concealedFields: {}, tags: [], favorite: false, }); const result = await vault.sync.sync(); expect(result.merged.length).toBe(1); expect(sync.hydrateCount).toBeGreaterThan(0); await vault.lock(); }); it("shares delegation works end-to-end", { timeout: 30000 }, async () => { const drive = memoryDrive(); const vault = Vault.open(makeConfig(drive)); await vault.unlock("pass", "secret"); const link = await vault.shares.createLink({ itemId: "i", itemPlaintext: new TextEncoder().encode("payload"), expiresAt: null, maxViews: null, }); expect(link.url).toContain("https://"); expect(await vault.shares.isActive(link.id)).toBe(true); await vault.shares.revokeLink(link.id); expect(await vault.shares.isActive(link.id)).toBe(false); await vault.lock(); }); });