Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,295 @@
/**
* Real-vault end-to-end test. Drives a live `@suaveplan/vault.Vault`
* through `fromVault` → `createVaultSecretsAdapter` → `SecretsLike`
* reads, exercising the full unlock + drive + encryption pipeline
* without a single mock.
*
* Argon2id is enforced at the OWASP minimum (memoryKB=65_536,
* iterations=3, parallelism=1) by `@suaveplan/crypto-vault`. Each
* unlock costs ~5–30s on this hardware; the tests therefore share a
* single unlocked vault across the describe block via `beforeAll`,
* resetting per-test state by clearing the bridge's items so each
* scenario starts from a known empty state.
*/
import { EventBus } from "@suaveplan/events";
import {
afterAll,
beforeAll,
describe,
expect,
it,
} from "@suaveplan/testing/runner";
import type {
DriveLike,
DriveSharingLike,
SyncDelegationResult,
SyncSnapshot,
VaultConfig,
VaultSyncLike,
} from "@suaveplan/vault";
import { Vault } from "@suaveplan/vault";
import {
createSecretsVaultWatcher,
createVaultSecretsAdapter,
fromVault,
SECRETS_VAULT_CODES,
SecretsVaultError,
type SecretsVaultError as SecretsVaultErrorType,
VAULT_SECRETS_CATEGORY,
type VaultSecretsBackend,
} from "../src/index.js";
function memoryDrive(): DriveLike & { blobs: Map<string, Uint8Array> } {
const blobs = new Map<string, Uint8Array>();
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 NoopSync implements VaultSyncLike {
public lastSnapshot: SyncSnapshot = { items: [], snapshotAt: 0 };
public hydrate(s: SyncSnapshot): void {
this.lastSnapshot = s;
}
public async pull(): Promise<SyncDelegationResult> {
return { merged: [], conflicts: [], deleted: [], durationMs: 0 };
}
public async push(): Promise<SyncDelegationResult> {
return { merged: [], conflicts: [], deleted: [], durationMs: 0 };
}
public async sync(): Promise<SyncDelegationResult> {
return { merged: [], conflicts: [], deleted: [], durationMs: 0 };
}
public currentSnapshot(): SyncSnapshot {
return this.lastSnapshot;
}
}
function makeConfig(drive = memoryDrive()): VaultConfig {
return {
vaultId: "secrets",
email: "agent@suaveplan.example",
containerPath: "/secrets.vault",
attachmentsPrefix: "/secrets/att",
// OWASP minimums enforced by @suaveplan/crypto-vault.
argon2id: {
memoryKB: 65_536,
iterations: 3,
parallelism: 1,
outputLength: 32,
},
argon2idSalt: new Uint8Array(16),
drive,
driveSharing: noopSharing(),
sync: new NoopSync(),
};
}
async function clearSecrets(vault: Vault): Promise<void> {
const rows = vault.items.list({ category: VAULT_SECRETS_CATEGORY });
for (const row of rows) {
vault.items.delete(row.id);
}
}
describe("real @suaveplan/vault — end-to-end", () => {
// Share one unlocked vault across the block — `beforeAll` runs
// the ~5–30s Argon2id pipeline once instead of per-test.
let vault: Vault;
let backend: VaultSecretsBackend;
beforeAll(async () => {
vault = Vault.open(makeConfig());
await vault.unlock("pw", "sk");
backend = fromVault(vault);
}, 120000);
afterAll(async () => {
await vault.lock();
}, 30000);
it("unlock → set via bridge → adapter.require reads back", {
timeout: 30000,
}, async () => {
await clearSecrets(vault);
await backend.set("STRIPE_SECRET_KEY", "sk_live_real");
const adapter = createVaultSecretsAdapter({
vault: backend,
allowlist: ["STRIPE_SECRET_KEY"],
});
const value = await adapter.require("STRIPE_SECRET_KEY");
expect(value).toBe("sk_live_real");
adapter.dispose();
});
it("set then delete: listNames reflects the deletion", {
timeout: 30000,
}, async () => {
await clearSecrets(vault);
await backend.set("A", "1");
await backend.set("B", "2");
expect((await backend.listNames()).sort()).toEqual(["A", "B"]);
await backend.delete("A");
expect(await backend.listNames()).toEqual(["B"]);
});
it("watcher fans set() through the bridge as secret.changed", {
timeout: 30000,
}, async () => {
await clearSecrets(vault);
const adapter = createVaultSecretsAdapter({
vault: backend,
allowlist: ["STRIPE"],
});
const bus = new EventBus<{ "secret.changed": { name: string } }>();
const heard: string[] = [];
bus.on("secret.changed", (p) => {
heard.push(p.name);
});
const watcher = createSecretsVaultWatcher({
vault: backend,
allowlist: adapter.allowlist,
bus,
});
await backend.set("STRIPE", "sk_live_rotated");
await backend.set("AWS", "ignored — not in allowlist");
expect(heard).toEqual(["STRIPE"]);
watcher.dispose();
adapter.dispose();
});
it("write-path errors surface as SECRETS_VAULT.WRITE_FAILED", {
timeout: 30000,
}, async () => {
await clearSecrets(vault);
// Empty title — vault.items.create's ItemFieldsSchema rejects
// empty titles, surfacing as a VaultError that the bridge
// wraps as WRITE_FAILED.
let caught: unknown;
try {
await backend.set("", "value");
} catch (err) {
caught = err;
}
expect(caught).toBeInstanceOf(SecretsVaultError);
expect((caught as SecretsVaultErrorType).code).toBe(
SECRETS_VAULT_CODES.WRITE_FAILED
);
});
it("delete is a no-op for an absent name (no error, no fan-out)", {
timeout: 30000,
}, async () => {
await clearSecrets(vault);
const heard: string[] = [];
const unsub = backend.onSecretUpdated((p) => {
heard.push(p.name);
});
await backend.delete("never-existed");
expect(heard).toEqual([]);
unsub();
});
it("set overwrites an existing entry via the update path", {
timeout: 30000,
}, async () => {
await clearSecrets(vault);
await backend.set("ROT", "v1");
await backend.set("ROT", "v2");
await backend.set("ROT", "v3");
const adapter = createVaultSecretsAdapter({
vault: backend,
allowlist: ["ROT"],
});
await expect(adapter.require("ROT")).resolves.toBe("v3");
adapter.dispose();
});
it("listener errors do not propagate to the producer", {
timeout: 30000,
}, async () => {
await clearSecrets(vault);
const sync = backend.onSecretUpdated(() => {
throw new Error("listener boom");
});
const async = backend.onSecretUpdated(async () => {
throw new Error("async boom");
});
await expect(backend.set("X", "y")).resolves.toBeUndefined();
sync();
async();
});
});
// Locked-vault assertions need a SEPARATE, dedicated vault instance:
// `vault.lock()` calls the global `zeroizeAll()` which would invalidate
// the shared unlocked vault's key buffer. Run this AFTER the shared-
// vault describe block so its `afterAll` `lock()` doesn't trip on a
// zeroized buffer.
describe("real @suaveplan/vault — locked-vault semantics", () => {
it("locked vault → adapter.get raises SECRETS_VAULT.VAULT_LOCKED; bridge get returns undefined", {
timeout: 120000,
}, async () => {
const v = Vault.open(makeConfig());
await v.unlock("pw", "sk");
const b = fromVault(v);
await b.set("X", "y");
await v.flush();
await v.lock();
expect(b.isLocked).toBe(true);
await expect(b.get("X")).resolves.toBeUndefined();
expect(await b.listNames()).toEqual([]);
const adapter = createVaultSecretsAdapter({
vault: b,
allowlist: ["X"],
});
let caught: unknown;
try {
await adapter.get("X");
} catch (err) {
caught = err;
}
expect(caught).toBeInstanceOf(SecretsVaultError);
expect((caught as SecretsVaultErrorType).code).toBe(
SECRETS_VAULT_CODES.VAULT_LOCKED
);
adapter.dispose();
});
});