Files
sw-ads-agent/packages/harness/tests/credential-vault.test.ts
T
2026-09-07 09:57:33 +08:00

102 lines
5.6 KiB
TypeScript

import { randomBytes } from "node:crypto";
import { mkdtemp, readFile, stat } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { EncryptedFileCredentialVault, InMemoryModelConnectionStore, modelCredentialIdentity } from "../src/index.js";
const model = {
providerId: "training",
api: "openai-responses" as const,
baseUrl: "https://8.8.8.8/v1",
modelId: "training-model",
supportsImages: true,
};
describe("EncryptedFileCredentialVault", () => {
it("persists only authenticated ciphertext and decrypts after restart", async () => {
const root = await mkdtemp(path.join(tmpdir(), "studio-vault-"));
const filePath = path.join(root, "credentials.enc.json");
const masterKey = randomBytes(32).toString("base64");
const identity = modelCredentialIdentity(model);
const first = new EncryptedFileCredentialVault(filePath, masterKey);
await first.set(identity, "secret-model-key");
const raw = await readFile(filePath, "utf8");
expect(raw).not.toContain("secret-model-key");
expect((await stat(filePath)).mode & 0o777).toBe(0o600);
const restarted = new EncryptedFileCredentialVault(filePath, masterKey);
await expect(restarted.get(identity)).resolves.toBe("secret-model-key");
});
it("restores a credential for a new Web Session and preserves it when the old Session is removed", async () => {
const root = await mkdtemp(path.join(tmpdir(), "studio-vault-"));
const vault = new EncryptedFileCredentialVault(path.join(root, "credentials.enc.json"), randomBytes(32).toString("base64"));
const first = new InMemoryModelConnectionStore({}, vault);
await first.configure("session-one", { ...model, apiKey: "persistent-secret" });
expect(first.getRedacted("session-one")).toMatchObject({ keyConfigured: true, credentialStorage: "encrypted" });
await first.remove("session-one");
const restarted = new InMemoryModelConnectionStore({}, vault);
await restarted.configure("session-two", model);
expect(restarted.getRedacted("session-two")).toMatchObject({ keyConfigured: true, keyHint: "••••cret", credentialStorage: "encrypted" });
});
it("deletes the persisted credential only through Clear Credentials", async () => {
const root = await mkdtemp(path.join(tmpdir(), "studio-vault-"));
const vault = new EncryptedFileCredentialVault(path.join(root, "credentials.enc.json"), randomBytes(32).toString("base64"));
const store = new InMemoryModelConnectionStore({}, vault);
await store.configure("session-one", { ...model, apiKey: "persistent-secret" });
await store.clearCredentials("session-one");
const restarted = new InMemoryModelConnectionStore({}, vault);
await restarted.configure("session-two", model);
expect(restarted.getRedacted("session-two")).toMatchObject({ keyConfigured: false });
});
it("fails closed for an invalid or different master key", async () => {
expect(() => new EncryptedFileCredentialVault("unused", "not-a-32-byte-key")).toThrow(/32-byte key/);
const root = await mkdtemp(path.join(tmpdir(), "studio-vault-"));
const filePath = path.join(root, "credentials.enc.json");
const identity = modelCredentialIdentity(model);
const first = new EncryptedFileCredentialVault(filePath, randomBytes(32).toString("base64"));
await first.set(identity, "secret-model-key");
const wrong = new EncryptedFileCredentialVault(filePath, randomBytes(32).toString("base64"));
await expect(wrong.get(identity)).rejects.toMatchObject({ code: "CREDENTIAL_DECRYPTION_FAILED" });
});
it("loads a strict environment default and restores its encrypted credential", async () => {
const root = await mkdtemp(path.join(tmpdir(), "studio-vault-"));
const vault = new EncryptedFileCredentialVault(path.join(root, "credentials.enc.json"), randomBytes(32).toString("base64"));
await vault.set(modelCredentialIdentity(model), "environment-default-secret");
const store = new InMemoryModelConnectionStore({
AGENT_PROVIDER: model.providerId,
AGENT_MODEL: model.modelId,
LLM_API: model.api,
LLM_BASE_URL: model.baseUrl,
AGENT_DISPLAY_NAME: "Training Model",
AGENT_SUPPORTS_IMAGES: "true",
}, vault);
await store.initialize();
expect(store.getRedacted("fresh-session")).toMatchObject({ source: "environment", modelId: model.modelId, displayName: "Training Model", supportsImages: true, keyConfigured: true, credentialStorage: "encrypted" });
});
it("uses an environment API key without returning the raw credential", async () => {
const store = new InMemoryModelConnectionStore({
AGENT_PROVIDER: model.providerId,
AGENT_MODEL: model.modelId,
LLM_API: model.api,
LLM_BASE_URL: model.baseUrl,
OPENAI_API_KEY: "environment-api-secret",
});
await store.initialize();
const redacted = store.getRedacted("fresh-session");
expect(redacted).toMatchObject({ source: "environment", keyConfigured: true, keyHint: "••••cret", credentialStorage: "environment" });
expect(JSON.stringify(redacted)).not.toContain("environment-api-secret");
});
it("fails startup for partial or malformed environment defaults", async () => {
const partial = new InMemoryModelConnectionStore({ AGENT_PROVIDER: "openai" });
await expect(partial.initialize()).rejects.toMatchObject({ code: "INVALID_ENV_MODEL_CONFIG" });
const malformed = new InMemoryModelConnectionStore({ AGENT_PROVIDER: "openai", AGENT_MODEL: "model", AGENT_SUPPORTS_IMAGES: "yes" });
await expect(malformed.initialize()).rejects.toMatchObject({ code: "INVALID_ENV_MODEL_CONFIG" });
});
});