refactor(runtime): prefer env-backed secrets at runtime

This commit is contained in:
2026-03-30 19:17:32 +02:00
parent 4f5d410b94
commit fed7aa5b61
13 changed files with 532 additions and 71 deletions
+12 -7
View File
@@ -1,5 +1,6 @@
import { createHash } from "node:crypto";
import type { PrismaClient } from "@capakraken/db";
import { resolveSystemSettingsRuntime } from "./system-settings-runtime.js";
const DEFAULT_ANONYMIZATION_DOMAIN = "superhartmut.de";
const DEFAULT_ANONYMIZATION_SEED = "capakraken-superhartmut-global";
@@ -639,10 +640,12 @@ export async function getAnonymizationConfig(
},
});
const runtimeSettings = resolveSystemSettingsRuntime(settings);
return {
enabled: settings?.anonymizationEnabled ?? false,
domain: settings?.anonymizationDomain?.trim() || DEFAULT_ANONYMIZATION_DOMAIN,
seed: settings?.anonymizationSeed?.trim() || DEFAULT_ANONYMIZATION_SEED,
enabled: runtimeSettings.anonymizationEnabled ?? false,
domain: runtimeSettings.anonymizationDomain?.trim() || DEFAULT_ANONYMIZATION_DOMAIN,
seed: runtimeSettings.anonymizationSeed?.trim() || DEFAULT_ANONYMIZATION_SEED,
mode: "global",
};
}
@@ -665,10 +668,12 @@ export async function getAnonymizationDirectory(
},
});
const runtimeSettings = resolveSystemSettingsRuntime(settings);
const config: AnonymizationConfig = {
enabled: settings?.anonymizationEnabled ?? false,
domain: settings?.anonymizationDomain?.trim() || DEFAULT_ANONYMIZATION_DOMAIN,
seed: settings?.anonymizationSeed?.trim() || DEFAULT_ANONYMIZATION_SEED,
enabled: runtimeSettings.anonymizationEnabled ?? false,
domain: runtimeSettings.anonymizationDomain?.trim() || DEFAULT_ANONYMIZATION_DOMAIN,
seed: runtimeSettings.anonymizationSeed?.trim() || DEFAULT_ANONYMIZATION_SEED,
mode: "global",
};
@@ -680,7 +685,7 @@ export async function getAnonymizationDirectory(
const usedSlugs = new Set<string>();
const byResourceId = new Map<string, ResourceAlias>();
const byAliasEid = new Map<string, string>();
const storedAliases = parseStoredAliases(settings?.anonymizationAliases);
const storedAliases = parseStoredAliases(runtimeSettings.anonymizationAliases);
let aliasesChanged = false;
for (const [resourceId, storedAlias] of Object.entries(storedAliases)) {
+41 -4
View File
@@ -4,6 +4,8 @@
*/
import nodemailer from "nodemailer";
import { prisma as db } from "@capakraken/db";
import { logger } from "./logger.js";
import { resolveSystemSettingsRuntime } from "./system-settings-runtime.js";
interface EmailPayload {
to: string | string[];
@@ -12,9 +14,43 @@ interface EmailPayload {
html?: string;
}
function sanitizeSmtpDiagnostic(err: unknown): string {
const message = err instanceof Error ? err.message : String(err);
return message
.replace(/https?:\/\/[^\s)\]}]+/gi, "<redacted-url>")
.replace(/\b[^\s@]+@[^\s@]+\.[^\s@]+\b/g, "<redacted-email>")
.replace(/\b(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}\b/g, "<redacted-host>")
.replace(/^Error:\s*/, "")
.slice(0, 300);
}
export function parseSmtpError(err: unknown): string {
const message = err instanceof Error ? err.message : String(err);
const lower = message.toLowerCase();
if (lower.includes("auth") || lower.includes("invalid login") || lower.includes("535")) {
return "SMTP authentication failed — check username and password.";
}
if (lower.includes("certificate") || lower.includes("tls") || lower.includes("ssl")) {
return "SMTP TLS negotiation failed — verify port and TLS settings.";
}
if (
lower.includes("econnrefused")
|| lower.includes("enotfound")
|| lower.includes("etimedout")
|| lower.includes("timeout")
) {
return "Cannot reach the SMTP server — check host, port, and network access.";
}
return "SMTP connection failed — review host, port, TLS, and credentials.";
}
async function getSmtpConfig() {
const settings = await db.systemSettings.findUnique({ where: { id: "singleton" } });
if (!settings?.smtpHost) return null;
const settings = resolveSystemSettingsRuntime(
await db.systemSettings.findUnique({ where: { id: "singleton" } }),
);
if (!settings.smtpHost) return null;
return {
host: settings.smtpHost,
port: settings.smtpPort ?? 587,
@@ -53,7 +89,7 @@ export async function sendEmail(payload: EmailPayload): Promise<boolean> {
return true;
} catch (err) {
console.error("[email] Failed to send email:", err);
logger.warn({ diagnostic: sanitizeSmtpDiagnostic(err) }, "Email send failed");
return false;
}
}
@@ -76,6 +112,7 @@ export async function testSmtpConnection(): Promise<{ ok: boolean; error?: strin
await transporter.verify();
return { ok: true };
} catch (err) {
return { ok: false, error: err instanceof Error ? err.message : String(err) };
logger.warn({ diagnostic: sanitizeSmtpDiagnostic(err) }, "SMTP connection test failed");
return { ok: false, error: parseSmtpError(err) };
}
}
@@ -0,0 +1,41 @@
type RuntimeAwareSystemSettings = {
aiProvider?: string | null;
azureOpenAiApiKey?: string | null;
azureDalleApiKey?: string | null;
geminiApiKey?: string | null;
smtpPassword?: string | null;
anonymizationSeed?: string | null;
};
function readEnvOverride(...names: string[]): string | null {
for (const name of names) {
const value = process.env[name]?.trim();
if (value) {
return value;
}
}
return null;
}
function resolvePrimaryAiApiKey(provider: string | null | undefined): string | null {
if (provider === "azure") {
return readEnvOverride("AZURE_OPENAI_API_KEY", "OPENAI_API_KEY");
}
return readEnvOverride("OPENAI_API_KEY", "AZURE_OPENAI_API_KEY");
}
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;
return resolved;
}