import { describe, expect, it } from "@suaveplan/testing/runner"; import { VAULT_CODES } from "../errors/codes.js"; import { VaultError } from "../errors/vault-error.js"; import { createManifest, hashVaultId, MANIFEST_AEAD_XCHACHA20_POLY1305, MANIFEST_BYTE_SIZE, MANIFEST_FORMAT_VERSION, MANIFEST_KDF_ARGON2ID_2SKD, packManifest, touchManifest, unpackManifest, } from "./manifest.js"; const baseInput = { vaultId: "personal", argon2idMemoryKB: 65536, argon2idIterations: 3, argon2idParallelism: 4, schemaVersion: 1, now: 1_700_000_000_000, }; describe("vault manifest", () => { it("createManifest populates current versions and timestamps", { timeout: 5000, }, () => { const m = createManifest(baseInput); expect(m.formatVersion).toBe(MANIFEST_FORMAT_VERSION); expect(m.kdfId).toBe(MANIFEST_KDF_ARGON2ID_2SKD); expect(m.aeadAlgorithm).toBe(MANIFEST_AEAD_XCHACHA20_POLY1305); expect(m.argon2idMemoryKB).toBe(65536); expect(m.argon2idIterations).toBe(3); expect(m.argon2idParallelism).toBe(4); expect(m.createdAt).toBe(baseInput.now); expect(m.lastModifiedAt).toBe(baseInput.now); expect(m.schemaVersion).toBe(1); expect(m.vaultIdHash.length).toBe(32); }); it("hashVaultId is deterministic and 32 bytes", { timeout: 5000 }, () => { const a = hashVaultId("personal"); const b = hashVaultId("personal"); expect(a).toEqual(b); expect(a.length).toBe(32); const c = hashVaultId("work"); expect(c).not.toEqual(a); }); it("packManifest + unpackManifest round-trip preserves all fields", { timeout: 5000, }, () => { const m = createManifest(baseInput); const packed = packManifest(m); expect(packed.length).toBe(MANIFEST_BYTE_SIZE); const round = unpackManifest(packed); expect(round).toEqual(m); }); it("unpackManifest ignores trailing bytes", { timeout: 5000 }, () => { const m = createManifest(baseInput); const packed = packManifest(m); const padded = new Uint8Array(packed.length + 100); padded.set(packed, 0); const round = unpackManifest(padded); expect(round).toEqual(m); }); it("unpackManifest rejects too-short buffers with MANIFEST_INVALID", { timeout: 5000, }, () => { try { unpackManifest(new Uint8Array(MANIFEST_BYTE_SIZE - 1)); throw new Error("should have thrown"); } catch (err) { expect(VaultError.is(err)).toBe(true); expect((err as VaultError).code).toBe(VAULT_CODES.MANIFEST_INVALID); } }); it("packManifest rejects non-conforming manifests", { timeout: 5000, }, () => { const bad = createManifest(baseInput); try { packManifest({ ...bad, vaultIdHash: new Uint8Array(31), }); throw new Error("should have thrown"); } catch (err) { expect(VaultError.is(err)).toBe(true); expect((err as VaultError).code).toBe(VAULT_CODES.MANIFEST_INVALID); } }); it("unpackManifest rejects unsupported format versions", { timeout: 5000, }, () => { const m = createManifest(baseInput); const packed = packManifest(m); // Mutate the formatVersion field (first 2 bytes — big-endian). const tampered = new Uint8Array(packed); tampered[0] = 0xff; tampered[1] = 0xff; try { unpackManifest(tampered); throw new Error("should have thrown"); } catch (err) { expect(VaultError.is(err)).toBe(true); expect((err as VaultError).code).toBe( VAULT_CODES.MANIFEST_VERSION_UNSUPPORTED ); } }); it("touchManifest updates only lastModifiedAt", { timeout: 5000 }, () => { const m = createManifest(baseInput); const next = touchManifest(m, baseInput.now + 1000); expect(next.lastModifiedAt).toBe(baseInput.now + 1000); expect(next.createdAt).toBe(m.createdAt); expect(next.formatVersion).toBe(m.formatVersion); expect(next.vaultIdHash).toEqual(m.vaultIdHash); }); it("unpackManifest rejects manifests with corrupt internal numerics", { timeout: 5000, }, () => { const m = createManifest(baseInput); const packed = packManifest(m); const tampered = new Uint8Array(packed); // Zero out argon2idMemoryKB which violates min(1) on the schema. // formatVersion u16 (2) + kdfId u8 (1) + aeadAlgorithm u8 (1) = 4 // argon2idMemoryKB is at offset 4..8. for (let i = 4; i < 8; i++) tampered[i] = 0; try { unpackManifest(tampered); throw new Error("should have thrown"); } catch (err) { expect(VaultError.is(err)).toBe(true); expect((err as VaultError).code).toBe(VAULT_CODES.MANIFEST_INVALID); } }); });