refactor(runtime): prefer env-backed secrets at runtime
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { getAnonymizationDirectory } from "../lib/anonymization.js";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { getAnonymizationConfig, getAnonymizationDirectory } from "../lib/anonymization.js";
|
||||
|
||||
describe("anonymization directory", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("persists aliases so existing resources keep the same identity when new resources appear", async () => {
|
||||
let storedAliases: Record<string, { displayName: string; eid: string }> = {
|
||||
resource_a: {
|
||||
@@ -126,4 +130,21 @@ describe("anonymization directory", () => {
|
||||
expect(alias?.eid).toMatch(/^[a-z]+(?:\.[a-z]+)*$/);
|
||||
expect(db.systemSettings.update).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("prefers the anonymization seed from the environment at runtime", async () => {
|
||||
vi.stubEnv("ANONYMIZATION_SEED", "env-seed");
|
||||
|
||||
const config = await getAnonymizationConfig({
|
||||
systemSettings: {
|
||||
findUnique: vi.fn(async () => ({
|
||||
anonymizationEnabled: true,
|
||||
anonymizationDomain: "example.test",
|
||||
anonymizationSeed: "db-seed",
|
||||
anonymizationMode: "global",
|
||||
})),
|
||||
},
|
||||
} as never);
|
||||
|
||||
expect(config.seed).toBe("env-seed");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import nodemailer from "nodemailer";
|
||||
import { logger } from "../lib/logger.js";
|
||||
import { testSmtpConnection } from "../lib/email.js";
|
||||
|
||||
const { findUnique, verify, createTransport } = vi.hoisted(() => {
|
||||
const verify = vi.fn();
|
||||
return {
|
||||
findUnique: vi.fn(),
|
||||
verify,
|
||||
createTransport: vi.fn(() => ({
|
||||
verify,
|
||||
sendMail: vi.fn(),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@capakraken/db", () => ({
|
||||
prisma: {
|
||||
systemSettings: {
|
||||
findUnique,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("nodemailer", () => ({
|
||||
default: {
|
||||
createTransport,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../lib/logger.js", () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("email runtime config hardening", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
findUnique.mockResolvedValue({
|
||||
smtpHost: "smtp.secret.internal",
|
||||
smtpPort: 587,
|
||||
smtpUser: "alice@example.com",
|
||||
smtpPassword: "super-secret",
|
||||
smtpTls: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("returns a classified SMTP auth error without leaking diagnostics", async () => {
|
||||
verify.mockRejectedValueOnce(
|
||||
new Error("Invalid login for alice@example.com against smtp.secret.internal"),
|
||||
);
|
||||
|
||||
const result = await testSmtpConnection();
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
error: "SMTP authentication failed — check username and password.",
|
||||
});
|
||||
|
||||
expect(nodemailer.createTransport).toHaveBeenCalledWith({
|
||||
host: "smtp.secret.internal",
|
||||
port: 587,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: "alice@example.com",
|
||||
pass: "super-secret",
|
||||
},
|
||||
});
|
||||
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
diagnostic: expect.any(String),
|
||||
}),
|
||||
"SMTP connection test failed",
|
||||
);
|
||||
|
||||
const diagnostic = vi.mocked(logger.warn).mock.calls[0]?.[0];
|
||||
expect(JSON.stringify(diagnostic)).toContain("<redacted-email>");
|
||||
expect(JSON.stringify(diagnostic)).toContain("<redacted-host>");
|
||||
expect(JSON.stringify(diagnostic)).not.toContain("alice@example.com");
|
||||
expect(JSON.stringify(diagnostic)).not.toContain("smtp.secret.internal");
|
||||
});
|
||||
|
||||
it("prefers SMTP_PASSWORD from the environment at runtime", async () => {
|
||||
vi.stubEnv("SMTP_PASSWORD", "env-smtp-password");
|
||||
findUnique.mockResolvedValue({
|
||||
smtpHost: "smtp.secret.internal",
|
||||
smtpPort: 587,
|
||||
smtpUser: "alice@example.com",
|
||||
smtpPassword: "db-password",
|
||||
smtpTls: true,
|
||||
});
|
||||
verify.mockResolvedValueOnce(true);
|
||||
|
||||
const result = await testSmtpConnection();
|
||||
|
||||
expect(result).toEqual({ ok: true });
|
||||
expect(nodemailer.createTransport).toHaveBeenCalledWith({
|
||||
host: "smtp.secret.internal",
|
||||
port: 587,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: "alice@example.com",
|
||||
pass: "env-smtp-password",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,153 @@
|
||||
import { SystemRole } from "@capakraken/shared";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { parseAiError } from "../ai-client.js";
|
||||
import { logger } from "../lib/logger.js";
|
||||
import { settingsRouter } from "../router/settings.js";
|
||||
import { createCallerFactory } from "../trpc.js";
|
||||
|
||||
vi.mock("../lib/logger.js", () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const createCaller = createCallerFactory(settingsRouter);
|
||||
|
||||
function createAdminCaller(db: Record<string, unknown>) {
|
||||
return createCaller({
|
||||
session: {
|
||||
user: { email: "admin@example.com", name: "Admin", image: null },
|
||||
expires: "2099-01-01T00:00:00.000Z",
|
||||
},
|
||||
db: db as never,
|
||||
dbUser: {
|
||||
id: "admin_1",
|
||||
systemRole: SystemRole.ADMIN,
|
||||
permissionOverrides: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("runtime config hardening", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("sanitizes AI fallback diagnostics", () => {
|
||||
const error = parseAiError(
|
||||
new Error(
|
||||
"Provider failed at https://example.openai.azure.com/path?api-key=topsecret with Bearer sk-super-secret",
|
||||
),
|
||||
);
|
||||
|
||||
expect(error).toContain("<redacted-url>");
|
||||
expect(error).toContain("<redacted-secret>");
|
||||
expect(error).not.toContain("https://example.openai.azure.com");
|
||||
expect(error).not.toContain("sk-super-secret");
|
||||
expect(error).not.toContain("topsecret");
|
||||
});
|
||||
|
||||
it("does not expose raw diagnostics from AI connection tests", async () => {
|
||||
const findUnique = vi.fn().mockResolvedValue({
|
||||
aiProvider: "openai",
|
||||
azureOpenAiDeployment: "gpt-4o-mini",
|
||||
azureOpenAiApiKey: "sk-live-secret",
|
||||
});
|
||||
|
||||
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
text: vi.fn().mockResolvedValue(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: "upstream failure at https://api.openai.com/v1/chat/completions?api-key=hidden",
|
||||
},
|
||||
}),
|
||||
),
|
||||
}));
|
||||
|
||||
const caller = createAdminCaller({
|
||||
systemSettings: {
|
||||
findUnique,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await caller.testAiConnection();
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result).not.toHaveProperty("raw");
|
||||
expect(result.error).toContain("<redacted-url>");
|
||||
expect(result.error).not.toContain("https://api.openai.com");
|
||||
expect(result.error).not.toContain("hidden");
|
||||
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
provider: "openai",
|
||||
diagnostic: expect.stringContaining("<redacted-url>"),
|
||||
}),
|
||||
"AI connection test failed",
|
||||
);
|
||||
});
|
||||
|
||||
it("reports secret presence flags when secrets come from environment overrides", async () => {
|
||||
vi.stubEnv("OPENAI_API_KEY", "env-openai-key");
|
||||
vi.stubEnv("SMTP_PASSWORD", "env-smtp-password");
|
||||
vi.stubEnv("GEMINI_API_KEY", "env-gemini-key");
|
||||
|
||||
const caller = createAdminCaller({
|
||||
systemSettings: {
|
||||
findUnique: vi.fn().mockResolvedValue({
|
||||
aiProvider: "openai",
|
||||
azureOpenAiApiKey: null,
|
||||
smtpPassword: null,
|
||||
geminiApiKey: null,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const result = await caller.getSystemSettings();
|
||||
|
||||
expect(result.hasApiKey).toBe(true);
|
||||
expect(result.hasSmtpPassword).toBe(true);
|
||||
expect(result.hasGeminiApiKey).toBe(true);
|
||||
});
|
||||
|
||||
it("prefers environment API keys during AI connection tests", async () => {
|
||||
vi.stubEnv("OPENAI_API_KEY", "env-openai-key");
|
||||
const fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
text: vi.fn().mockResolvedValue("ok"),
|
||||
});
|
||||
vi.stubGlobal("fetch", fetch);
|
||||
|
||||
const caller = createAdminCaller({
|
||||
systemSettings: {
|
||||
findUnique: vi.fn().mockResolvedValue({
|
||||
aiProvider: "openai",
|
||||
azureOpenAiDeployment: "gpt-4o-mini",
|
||||
azureOpenAiApiKey: null,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const result = await caller.testAiConnection();
|
||||
|
||||
expect(result).toEqual({ ok: true });
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"https://api.openai.com/v1/chat/completions",
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: "Bearer env-openai-key",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { resolveSystemSettingsRuntime } from "../lib/system-settings-runtime.js";
|
||||
|
||||
describe("system settings runtime resolution", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("prefers OPENAI_API_KEY for direct OpenAI runtime settings", () => {
|
||||
vi.stubEnv("OPENAI_API_KEY", "env-openai-key");
|
||||
vi.stubEnv("AZURE_OPENAI_API_KEY", "env-azure-key");
|
||||
|
||||
const settings = resolveSystemSettingsRuntime({
|
||||
aiProvider: "openai",
|
||||
azureOpenAiApiKey: "db-key",
|
||||
});
|
||||
|
||||
expect(settings.azureOpenAiApiKey).toBe("env-openai-key");
|
||||
});
|
||||
|
||||
it("prefers AZURE_OPENAI_API_KEY for Azure runtime settings", () => {
|
||||
vi.stubEnv("OPENAI_API_KEY", "env-openai-key");
|
||||
vi.stubEnv("AZURE_OPENAI_API_KEY", "env-azure-key");
|
||||
|
||||
const settings = resolveSystemSettingsRuntime({
|
||||
aiProvider: "azure",
|
||||
azureOpenAiApiKey: "db-key",
|
||||
});
|
||||
|
||||
expect(settings.azureOpenAiApiKey).toBe("env-azure-key");
|
||||
});
|
||||
|
||||
it("ignores blank environment overrides", () => {
|
||||
vi.stubEnv("SMTP_PASSWORD", " ");
|
||||
|
||||
const settings = resolveSystemSettingsRuntime({
|
||||
smtpPassword: "db-password",
|
||||
});
|
||||
|
||||
expect(settings.smtpPassword).toBe("db-password");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user