refactor(settings): adopt environment-only runtime secret flow

This commit is contained in:
2026-03-30 19:55:06 +02:00
parent fed7aa5b61
commit a19d2cbae0
19 changed files with 757 additions and 172 deletions
@@ -618,10 +618,12 @@ describe("assistant router tool gating", () => {
const adminNames = getToolNames([], SystemRole.ADMIN);
const userNames = getToolNames([], SystemRole.USER);
const managerNames = getToolNames([], SystemRole.MANAGER);
expect(adminNames).toContain("get_system_settings");
expect(adminNames).toContain("update_system_settings");
expect(adminNames).toContain("test_ai_connection");
expect(adminNames).toContain("test_smtp_connection");
expect(adminNames).toContain("clear_stored_runtime_secrets");
expect(adminNames).toContain("test_gemini_connection");
expect(adminNames).toContain("list_system_role_configs");
expect(adminNames).toContain("update_system_role_config");
@@ -632,12 +634,22 @@ describe("assistant router tool gating", () => {
expect(adminNames).toContain("delete_webhook");
expect(adminNames).toContain("test_webhook");
expect(adminNames).toContain("get_ai_configured");
expect(adminNames).toContain("list_system_role_configs");
expect(managerNames).not.toContain("get_system_settings");
expect(managerNames).not.toContain("update_system_settings");
expect(managerNames).not.toContain("clear_stored_runtime_secrets");
expect(managerNames).not.toContain("test_ai_connection");
expect(managerNames).not.toContain("get_ai_configured");
expect(managerNames).not.toContain("list_system_role_configs");
expect(managerNames).not.toContain("update_system_role_config");
expect(managerNames).not.toContain("list_webhooks");
expect(managerNames).not.toContain("create_webhook");
expect(userNames).not.toContain("get_system_settings");
expect(userNames).not.toContain("update_system_settings");
expect(userNames).not.toContain("test_ai_connection");
expect(userNames).not.toContain("get_ai_configured");
expect(userNames).not.toContain("clear_stored_runtime_secrets");
expect(userNames).not.toContain("list_system_role_configs");
expect(userNames).not.toContain("update_system_role_config");
expect(userNames).not.toContain("list_webhooks");
@@ -996,6 +1008,8 @@ describe("assistant router tool gating", () => {
expect(toolDescriptions.get("update_system_settings")).toContain("Always confirm first");
expect(toolDescriptions.get("get_ai_configured")).toContain("Admin role");
expect(toolDescriptions.get("list_system_role_configs")).toContain("Admin role");
expect(toolDescriptions.get("update_system_settings")).toContain("Runtime secrets must be provisioned");
expect(toolDescriptions.get("clear_stored_runtime_secrets")).toContain("deployment secret management");
expect(toolDescriptions.get("update_system_role_config")).toContain("Admin role");
expect(toolDescriptions.get("list_webhooks")).toContain("Secrets are masked");
expect(toolDescriptions.get("create_webhook")).toContain("Always confirm first");
@@ -150,6 +150,7 @@ function createToolContext(
describe("assistant import/export and dispo tools", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.unstubAllEnvs();
apiRateLimiter.reset();
totpValidateMock.mockReset();
vi.mocked(approveEstimateVersion).mockReset();
@@ -288,6 +289,67 @@ describe("assistant import/export and dispo tools", () => {
expect(JSON.parse(result.content)).toEqual({ configured: true });
});
it("treats environment-backed AI configuration as configured for assistant checks", async () => {
vi.stubEnv("OPENAI_API_KEY", "env-secret");
const ctx = createToolContext(
{
systemSettings: {
findUnique: vi.fn().mockResolvedValue({
aiProvider: "openai",
azureOpenAiDeployment: "gpt-4o-mini",
azureOpenAiApiKey: null,
}),
},
},
{ userRole: SystemRole.USER },
);
const result = await executeTool("get_ai_configured", "{}", ctx);
expect(JSON.parse(result.content)).toEqual({ configured: true });
});
it("clears legacy runtime secrets through the real settings router path", async () => {
const findUnique = vi.fn().mockResolvedValue({
azureOpenAiApiKey: "db-openai",
azureDalleApiKey: null,
geminiApiKey: "db-gemini",
smtpPassword: null,
anonymizationSeed: "db-seed",
});
const update = vi.fn().mockResolvedValue({ id: "singleton" });
const auditCreate = vi.fn().mockResolvedValue(undefined);
const ctx = createToolContext(
{
systemSettings: {
findUnique,
update,
},
auditLog: {
create: auditCreate,
},
},
{ userRole: SystemRole.ADMIN },
);
const result = await executeTool("clear_stored_runtime_secrets", "{}", ctx);
expect(JSON.parse(result.content)).toEqual({
ok: true,
clearedFields: ["azureOpenAiApiKey", "geminiApiKey", "anonymizationSeed"],
});
expect(update).toHaveBeenCalledWith({
where: { id: "singleton" },
data: {
azureOpenAiApiKey: null,
geminiApiKey: null,
anonymizationSeed: null,
},
});
expect(auditCreate).toHaveBeenCalled();
});
it("masks webhook secrets in assistant responses", async () => {
const ctx = createToolContext(
{
@@ -118,6 +118,9 @@ describe("runtime config hardening", () => {
expect(result.hasApiKey).toBe(true);
expect(result.hasSmtpPassword).toBe(true);
expect(result.hasGeminiApiKey).toBe(true);
expect(result.runtimeSecrets.azureOpenAiApiKey.activeSource).toBe("environment");
expect(result.runtimeSecrets.smtpPassword.activeSource).toBe("environment");
expect(result.legacyStoredSecretFields).toEqual([]);
});
it("prefers environment API keys during AI connection tests", async () => {
@@ -150,4 +153,81 @@ describe("runtime config hardening", () => {
}),
);
});
it("does not persist incoming secret fields through updateSystemSettings", async () => {
const findUnique = vi.fn().mockResolvedValue(null);
const upsert = vi.fn().mockResolvedValue({});
const caller = createAdminCaller({
systemSettings: {
findUnique,
upsert,
},
});
const result = await caller.updateSystemSettings({
azureOpenAiApiKey: "sk-should-not-store",
smtpPassword: "smtp-should-not-store",
geminiApiKey: "gemini-should-not-store",
azureDalleApiKey: "dalle-should-not-store",
anonymizationSeed: "seed-should-not-store",
aiProvider: "openai",
azureOpenAiDeployment: "gpt-4o-mini",
});
expect(result).toEqual({
ok: true,
ignoredSecretFields: [
"azureOpenAiApiKey",
"smtpPassword",
"anonymizationSeed",
"azureDalleApiKey",
"geminiApiKey",
],
secretStorageMode: "environment-only",
});
expect(upsert).toHaveBeenCalledWith({
where: { id: "singleton" },
create: {
id: "singleton",
aiProvider: "openai",
azureOpenAiDeployment: "gpt-4o-mini",
},
update: {
aiProvider: "openai",
azureOpenAiDeployment: "gpt-4o-mini",
},
});
});
it("can clear legacy runtime secrets from database storage", async () => {
const findUnique = vi.fn().mockResolvedValue({
azureOpenAiApiKey: "db-key",
azureDalleApiKey: null,
geminiApiKey: "db-gemini",
smtpPassword: "db-smtp",
anonymizationSeed: null,
});
const update = vi.fn().mockResolvedValue({});
const caller = createAdminCaller({
systemSettings: {
findUnique,
update,
},
});
const result = await caller.clearStoredRuntimeSecrets();
expect(result).toEqual({
ok: true,
clearedFields: ["azureOpenAiApiKey", "geminiApiKey", "smtpPassword"],
});
expect(update).toHaveBeenCalledWith({
where: { id: "singleton" },
data: {
azureOpenAiApiKey: null,
geminiApiKey: null,
smtpPassword: null,
},
});
});
});
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { resolveSystemSettingsRuntime } from "../lib/system-settings-runtime.js";
import { getRuntimeSecretStatuses, resolveSystemSettingsRuntime } from "../lib/system-settings-runtime.js";
describe("system settings runtime resolution", () => {
afterEach(() => {
@@ -39,4 +39,27 @@ describe("system settings runtime resolution", () => {
expect(settings.smtpPassword).toBe("db-password");
});
it("reports active source and legacy DB presence separately", () => {
vi.stubEnv("OPENAI_API_KEY", "env-openai-key");
const statuses = getRuntimeSecretStatuses({
aiProvider: "openai",
azureOpenAiApiKey: "db-key",
smtpPassword: "db-password",
});
expect(statuses.azureOpenAiApiKey).toEqual({
configured: true,
activeSource: "environment",
hasStoredValue: true,
envVarNames: ["OPENAI_API_KEY", "AZURE_OPENAI_API_KEY"],
});
expect(statuses.smtpPassword).toEqual({
configured: true,
activeSource: "database",
hasStoredValue: true,
envVarNames: ["SMTP_PASSWORD"],
});
});
});
@@ -7,6 +7,23 @@ type RuntimeAwareSystemSettings = {
anonymizationSeed?: string | null;
};
export const RUNTIME_SECRET_FIELDS = [
"azureOpenAiApiKey",
"azureDalleApiKey",
"geminiApiKey",
"smtpPassword",
"anonymizationSeed",
] as const;
export type RuntimeSecretField = (typeof RUNTIME_SECRET_FIELDS)[number];
export type RuntimeSecretStatus = {
configured: boolean;
activeSource: "environment" | "database" | "none";
hasStoredValue: boolean;
envVarNames: string[];
};
function readEnvOverride(...names: string[]): string | null {
for (const name of names) {
const value = process.env[name]?.trim();
@@ -26,16 +43,92 @@ function resolvePrimaryAiApiKey(provider: string | null | undefined): string | n
return readEnvOverride("OPENAI_API_KEY", "AZURE_OPENAI_API_KEY");
}
function getPrimaryAiEnvVarNames(provider: string | null | undefined): string[] {
if (provider === "azure") {
return ["AZURE_OPENAI_API_KEY", "OPENAI_API_KEY"];
}
return ["OPENAI_API_KEY", "AZURE_OPENAI_API_KEY"];
}
function resolveSecretEnvOverride(
field: RuntimeSecretField,
provider: string | null | undefined,
): string | null {
if (field === "azureOpenAiApiKey") {
return resolvePrimaryAiApiKey(provider);
}
if (field === "azureDalleApiKey") {
return readEnvOverride("AZURE_DALLE_API_KEY");
}
if (field === "geminiApiKey") {
return readEnvOverride("GEMINI_API_KEY");
}
if (field === "smtpPassword") {
return readEnvOverride("SMTP_PASSWORD");
}
return readEnvOverride("ANONYMIZATION_SEED");
}
function getSecretEnvVarNames(
field: RuntimeSecretField,
provider: string | null | undefined,
): string[] {
if (field === "azureOpenAiApiKey") {
return getPrimaryAiEnvVarNames(provider);
}
if (field === "azureDalleApiKey") {
return ["AZURE_DALLE_API_KEY"];
}
if (field === "geminiApiKey") {
return ["GEMINI_API_KEY"];
}
if (field === "smtpPassword") {
return ["SMTP_PASSWORD"];
}
return ["ANONYMIZATION_SEED"];
}
export function getRuntimeSecretStatuses(
settings: RuntimeAwareSystemSettings | null | undefined,
): Record<RuntimeSecretField, RuntimeSecretStatus> {
const provider = settings?.aiProvider;
return Object.fromEntries(
RUNTIME_SECRET_FIELDS.map((field) => {
const envValue = resolveSecretEnvOverride(field, provider);
const storedValue = settings?.[field]?.trim() || null;
const activeSource = envValue
? "environment"
: storedValue
? "database"
: "none";
return [
field,
{
configured: !!(envValue || storedValue),
activeSource,
hasStoredValue: !!storedValue,
envVarNames: getSecretEnvVarNames(field, provider),
} satisfies RuntimeSecretStatus,
];
}),
) as Record<RuntimeSecretField, RuntimeSecretStatus>;
}
export function resolveSystemSettingsRuntime<T extends RuntimeAwareSystemSettings>(
settings: T | null | undefined,
): T & Required<Pick<RuntimeAwareSystemSettings, "azureOpenAiApiKey" | "azureDalleApiKey" | "geminiApiKey" | "smtpPassword" | "anonymizationSeed">> {
const resolved = { ...(settings ?? {}) } as T & Required<Pick<RuntimeAwareSystemSettings, "azureOpenAiApiKey" | "azureDalleApiKey" | "geminiApiKey" | "smtpPassword" | "anonymizationSeed">>;
resolved.azureOpenAiApiKey = resolvePrimaryAiApiKey(resolved.aiProvider) ?? settings?.azureOpenAiApiKey ?? null;
resolved.azureDalleApiKey = readEnvOverride("AZURE_DALLE_API_KEY") ?? settings?.azureDalleApiKey ?? null;
resolved.geminiApiKey = readEnvOverride("GEMINI_API_KEY") ?? settings?.geminiApiKey ?? null;
resolved.smtpPassword = readEnvOverride("SMTP_PASSWORD") ?? settings?.smtpPassword ?? null;
resolved.anonymizationSeed = readEnvOverride("ANONYMIZATION_SEED") ?? settings?.anonymizationSeed ?? null;
resolved.azureOpenAiApiKey = resolveSecretEnvOverride("azureOpenAiApiKey", resolved.aiProvider) ?? settings?.azureOpenAiApiKey ?? null;
resolved.azureDalleApiKey = resolveSecretEnvOverride("azureDalleApiKey", resolved.aiProvider) ?? settings?.azureDalleApiKey ?? null;
resolved.geminiApiKey = resolveSecretEnvOverride("geminiApiKey", resolved.aiProvider) ?? settings?.geminiApiKey ?? null;
resolved.smtpPassword = resolveSecretEnvOverride("smtpPassword", resolved.aiProvider) ?? settings?.smtpPassword ?? null;
resolved.anonymizationSeed = resolveSecretEnvOverride("anonymizationSeed", resolved.aiProvider) ?? settings?.anonymizationSeed ?? null;
return resolved;
}
+21 -13
View File
@@ -76,12 +76,14 @@ import { insightsRouter } from "./insights.js";
import { scenarioRouter } from "./scenario.js";
import { allocationRouter } from "./allocation.js";
import { staffingRouter } from "./staffing.js";
import { resolveSystemSettingsRuntime } from "../lib/system-settings-runtime.js";
// ─── Mutation tool set for audit logging (EGAI 4.1.3.1 / IAAI 3.6.26) ──────
export const MUTATION_TOOLS = new Set([
"import_csv_data",
"update_system_settings",
"clear_stored_runtime_secrets",
"test_ai_connection",
"test_smtp_connection",
"test_gemini_connection",
@@ -4772,14 +4774,13 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
type: "function",
function: {
name: "update_system_settings",
description: "Update system settings through the real settings router. Admin role required. Always confirm first.",
description: "Update non-secret system settings through the real settings router. Runtime secrets must be provisioned via deployment environment or secret manager. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
aiProvider: { type: "string", enum: ["openai", "azure"] },
azureOpenAiEndpoint: { type: "string" },
azureOpenAiDeployment: { type: "string" },
azureOpenAiApiKey: { type: "string" },
azureApiVersion: { type: "string" },
aiMaxCompletionTokens: { type: "integer" },
aiTemperature: { type: "number" },
@@ -4789,17 +4790,13 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
smtpHost: { type: "string" },
smtpPort: { type: "integer" },
smtpUser: { type: "string" },
smtpPassword: { type: "string" },
smtpFrom: { type: "string" },
smtpTls: { type: "boolean" },
anonymizationEnabled: { type: "boolean" },
anonymizationDomain: { type: "string" },
anonymizationSeed: { type: "string" },
anonymizationMode: { type: "string", enum: ["global"] },
azureDalleDeployment: { type: "string" },
azureDalleEndpoint: { type: "string" },
azureDalleApiKey: { type: "string" },
geminiApiKey: { type: "string" },
geminiModel: { type: "string" },
imageProvider: { type: "string", enum: ["dalle", "gemini"] },
vacationDefaultDays: { type: "integer" },
@@ -4809,6 +4806,17 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
},
},
{
{
type: "function",
function: {
name: "clear_stored_runtime_secrets",
description: "Clear legacy database-stored runtime secrets after they have been migrated to deployment secret management. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {},
},
},
},
type: "function",
function: {
name: "test_ai_connection",
@@ -9306,7 +9314,6 @@ const executors = {
aiProvider?: "openai" | "azure";
azureOpenAiEndpoint?: string;
azureOpenAiDeployment?: string;
azureOpenAiApiKey?: string;
azureApiVersion?: string;
aiMaxCompletionTokens?: number;
aiTemperature?: number;
@@ -9322,17 +9329,13 @@ const executors = {
smtpHost?: string;
smtpPort?: number;
smtpUser?: string;
smtpPassword?: string;
smtpFrom?: string;
smtpTls?: boolean;
anonymizationEnabled?: boolean;
anonymizationDomain?: string;
anonymizationSeed?: string;
anonymizationMode?: "global";
azureDalleDeployment?: string;
azureDalleEndpoint?: string;
azureDalleApiKey?: string;
geminiApiKey?: string;
geminiModel?: string;
imageProvider?: "dalle" | "gemini";
vacationDefaultDays?: number;
@@ -9342,6 +9345,11 @@ const executors = {
return caller.updateSystemSettings(params);
},
async clear_stored_runtime_secrets(_params: Record<string, never>, ctx: ToolContext) {
const caller = createSettingsCaller(createScopedCallerContext(ctx));
return caller.clearStoredRuntimeSecrets();
},
async test_ai_connection(_params: Record<string, never>, ctx: ToolContext) {
const caller = createSettingsCaller(createScopedCallerContext(ctx));
return caller.testAiConnection();
@@ -9358,7 +9366,7 @@ const executors = {
},
async get_ai_configured(_params: Record<string, never>, ctx: ToolContext) {
const settings = await ctx.db.systemSettings.findUnique({
const settings = resolveSystemSettingsRuntime(await ctx.db.systemSettings.findUnique({
where: { id: "singleton" },
select: {
aiProvider: true,
@@ -9366,7 +9374,7 @@ const executors = {
azureOpenAiDeployment: true,
azureOpenAiApiKey: true,
},
});
}));
return { configured: isAiConfigured(settings) };
},
+1
View File
@@ -349,6 +349,7 @@ const ADMIN_ONLY_TOOLS = new Set([
"commit_dispo_import_batch",
"get_system_settings",
"update_system_settings",
"clear_stored_runtime_secrets",
"get_ai_configured",
"test_ai_connection",
"test_smtp_connection",
+60 -10
View File
@@ -6,7 +6,7 @@ import { VALUE_SCORE_WEIGHTS } from "@capakraken/shared";
import { testSmtpConnection } from "../lib/email.js";
import { createAuditEntry } from "../lib/audit.js";
import { logger } from "../lib/logger.js";
import { resolveSystemSettingsRuntime } from "../lib/system-settings-runtime.js";
import { getRuntimeSecretStatuses, RUNTIME_SECRET_FIELDS, resolveSystemSettingsRuntime } from "../lib/system-settings-runtime.js";
/** Fields that must never appear in audit log values */
const SENSITIVE_FIELDS = new Set([
@@ -23,6 +23,10 @@ export const settingsRouter = createTRPCRouter({
where: { id: "singleton" },
});
const runtimeSettings = resolveSystemSettingsRuntime(settings);
const runtimeSecrets = getRuntimeSecretStatuses(settings);
const legacyStoredSecretFields = RUNTIME_SECRET_FIELDS.filter(
(field) => runtimeSecrets[field].hasStoredValue,
);
const defaultWeights = {
skillDepth: VALUE_SCORE_WEIGHTS.SKILL_DEPTH,
@@ -42,6 +46,8 @@ export const settingsRouter = createTRPCRouter({
aiSummaryPrompt: settings?.aiSummaryPrompt ?? null,
defaultSummaryPrompt: DEFAULT_SUMMARY_PROMPT,
hasApiKey: !!runtimeSettings.azureOpenAiApiKey,
runtimeSecrets,
legacyStoredSecretFields,
scoreWeights: (settings?.scoreWeights as unknown as typeof defaultWeights) ?? defaultWeights,
scoreVisibleRoles: (settings?.scoreVisibleRoles as unknown as string[]) ?? ["ADMIN", "MANAGER"],
// SMTP
@@ -125,13 +131,14 @@ export const settingsRouter = createTRPCRouter({
)
.mutation(async ({ ctx, input }) => {
const data: Record<string, unknown> = {};
const ignoredSecretFields: string[] = [];
if (input.aiProvider !== undefined) data.aiProvider = input.aiProvider;
if (input.azureOpenAiEndpoint !== undefined)
data.azureOpenAiEndpoint = input.azureOpenAiEndpoint || null;
if (input.azureOpenAiDeployment !== undefined)
data.azureOpenAiDeployment = input.azureOpenAiDeployment || null;
if (input.azureOpenAiApiKey !== undefined)
data.azureOpenAiApiKey = input.azureOpenAiApiKey || null;
ignoredSecretFields.push("azureOpenAiApiKey");
if (input.azureApiVersion !== undefined)
data.azureApiVersion = input.azureApiVersion || null;
if (input.aiMaxCompletionTokens !== undefined)
@@ -148,16 +155,13 @@ export const settingsRouter = createTRPCRouter({
if (input.smtpHost !== undefined) data.smtpHost = input.smtpHost || null;
if (input.smtpPort !== undefined) data.smtpPort = input.smtpPort;
if (input.smtpUser !== undefined) data.smtpUser = input.smtpUser || null;
if (input.smtpPassword !== undefined) data.smtpPassword = input.smtpPassword || null;
if (input.smtpPassword !== undefined) ignoredSecretFields.push("smtpPassword");
if (input.smtpFrom !== undefined) data.smtpFrom = input.smtpFrom || null;
if (input.smtpTls !== undefined) data.smtpTls = input.smtpTls;
// Global anonymization
if (input.anonymizationEnabled !== undefined) data.anonymizationEnabled = input.anonymizationEnabled;
if (input.anonymizationDomain !== undefined) data.anonymizationDomain = input.anonymizationDomain || "superhartmut.de";
if (input.anonymizationSeed !== undefined) {
data.anonymizationSeed = input.anonymizationSeed || null;
data.anonymizationAliases = null;
}
if (input.anonymizationSeed !== undefined) ignoredSecretFields.push("anonymizationSeed");
if (input.anonymizationMode !== undefined) {
data.anonymizationMode = input.anonymizationMode;
data.anonymizationAliases = null;
@@ -168,10 +172,10 @@ export const settingsRouter = createTRPCRouter({
if (input.azureDalleEndpoint !== undefined)
data.azureDalleEndpoint = input.azureDalleEndpoint || null;
if (input.azureDalleApiKey !== undefined)
data.azureDalleApiKey = input.azureDalleApiKey || null;
ignoredSecretFields.push("azureDalleApiKey");
// Gemini
if (input.geminiApiKey !== undefined)
data.geminiApiKey = input.geminiApiKey || null;
ignoredSecretFields.push("geminiApiKey");
if (input.geminiModel !== undefined)
data.geminiModel = input.geminiModel || null;
// Image provider
@@ -182,6 +186,14 @@ export const settingsRouter = createTRPCRouter({
// Timeline
if (input.timelineUndoMaxSteps !== undefined) data.timelineUndoMaxSteps = input.timelineUndoMaxSteps;
if (Object.keys(data).length === 0) {
return {
ok: true,
ignoredSecretFields,
secretStorageMode: "environment-only" as const,
};
}
// Fetch current settings for before-snapshot
const before = await ctx.db.systemSettings.findUnique({ where: { id: "singleton" } });
@@ -215,9 +227,47 @@ export const settingsRouter = createTRPCRouter({
source: "ui",
});
return { ok: true };
return {
ok: true,
ignoredSecretFields,
secretStorageMode: "environment-only" as const,
};
}),
clearStoredRuntimeSecrets: adminProcedure.mutation(async ({ ctx }) => {
const existing = await ctx.db.systemSettings.findUnique({
where: { id: "singleton" },
select: Object.fromEntries(
RUNTIME_SECRET_FIELDS.map((field) => [field, true]),
) as Record<(typeof RUNTIME_SECRET_FIELDS)[number], true>,
});
const clearedFields = RUNTIME_SECRET_FIELDS.filter((field) => !!existing?.[field]);
if (clearedFields.length === 0) {
return { ok: true, clearedFields: [] as string[] };
}
await ctx.db.systemSettings.update({
where: { id: "singleton" },
data: Object.fromEntries(clearedFields.map((field) => [field, null])),
});
void createAuditEntry({
db: ctx.db,
entityType: "SystemSettings",
entityId: "singleton",
entityName: "Runtime Secrets",
action: "UPDATE",
userId: ctx.dbUser?.id,
after: { clearedFields },
source: "ui",
summary: `Cleared ${clearedFields.length} legacy runtime secret field(s) from database storage`,
});
return { ok: true, clearedFields };
}),
testAiConnection: adminProcedure.mutation(async ({ ctx }) => {
const settings = resolveSystemSettingsRuntime(await ctx.db.systemSettings.findUnique({
where: { id: "singleton" },