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