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,93 @@
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join, resolve } from "node:path";
import { describe, expect, it } from "@suaveplan/testing/runner";
const pkgRoot = resolve(__dirname, "..");
const srcRoot = join(pkgRoot, "src");
function walk(dir: string): string[] {
const out: string[] = [];
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
const st = statSync(full);
if (st.isDirectory()) out.push(...walk(full));
else out.push(full);
}
return out;
}
describe("@suaveplan/api-secrets-vault dep graph + layout", () => {
it("package.json carries the required @suaveplan/* runtime deps", {
timeout: 5000,
}, () => {
const pkg = JSON.parse(
readFileSync(join(pkgRoot, "package.json"), "utf8")
) as { dependencies: Record<string, string> };
const suaveplan = Object.keys(pkg.dependencies)
.filter((k) => k.startsWith("@suaveplan/"))
.sort();
expect(suaveplan).toEqual([
"@suaveplan/api-secrets",
"@suaveplan/api-types",
"@suaveplan/error",
"@suaveplan/events",
"@suaveplan/logger",
"@suaveplan/vault",
]);
});
it("zero peerDependencies", { timeout: 5000 }, () => {
const pkg = JSON.parse(
readFileSync(join(pkgRoot, "package.json"), "utf8")
) as { peerDependencies?: Record<string, string> };
expect(Object.keys(pkg.peerDependencies ?? {})).toEqual([]);
});
it("every src file uses default-import z (no named { z })", {
timeout: 5000,
}, () => {
const bad: string[] = [];
for (const file of walk(srcRoot)) {
if (!file.endsWith(".ts")) continue;
const src = readFileSync(file, "utf8");
if (/import\s*\{\s*z\s*\}\s*from\s*["']zod["']/.test(src)) {
bad.push(file);
}
}
expect(bad).toEqual([]);
});
it("no static-analysis suppression comments in src/", {
timeout: 5000,
}, () => {
const bad: string[] = [];
for (const file of walk(srcRoot)) {
if (!file.endsWith(".ts")) continue;
const src = readFileSync(file, "utf8");
if (/biome-ignore|eslint-disable|@ts-(ignore|nocheck)/.test(src)) {
bad.push(file);
}
}
expect(bad).toEqual([]);
});
it("package.json exposes the documented subpath exports", {
timeout: 5000,
}, () => {
const pkg = JSON.parse(
readFileSync(join(pkgRoot, "package.json"), "utf8")
) as { exports: Record<string, unknown> };
const subpaths = Object.keys(pkg.exports)
.filter((k) => k !== "./package.json" && k !== ".")
.sort();
expect(subpaths).toEqual([
"./adapter",
"./allowlist",
"./error-codes",
"./from-vault",
"./otel",
"./types",
"./watcher",
]);
});
});

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();
});
});

View File

@@ -0,0 +1,35 @@
/**
* Test setup for @suaveplan/api-secrets-vault.
*
* Installs an in-memory OpenTelemetry MeterProvider BEFORE any source
* module imports so the meter resolved on first call binds to it. The
* helper `flushTestMetrics` collects every snapshot emitted since the
* previous call, which the OTel assertion tests use to verify counter
* emission without relying on real exporters.
*/
import { metrics } from "@opentelemetry/api";
import {
AggregationTemporality,
InMemoryMetricExporter,
MeterProvider,
PeriodicExportingMetricReader,
} from "@opentelemetry/sdk-metrics";
export const testInMemoryMetricExporter = new InMemoryMetricExporter(
AggregationTemporality.CUMULATIVE
);
const reader = new PeriodicExportingMetricReader({
exporter: testInMemoryMetricExporter,
exportIntervalMillis: 60_000,
});
export const testMeterProvider = new MeterProvider({ readers: [reader] });
metrics.setGlobalMeterProvider(testMeterProvider);
export async function flushTestMetrics() {
await testMeterProvider.forceFlush();
const collected = testInMemoryMetricExporter.getMetrics();
testInMemoryMetricExporter.reset();
return collected;
}