Added Typescript and rewritten config.js into config.ts (use AI)
This commit is contained in:
@@ -1,2 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
data/uploads/
|
data/uploads/
|
||||||
|
package-lock.json
|
||||||
|
.gitignore
|
||||||
+25
-3
@@ -1,7 +1,29 @@
|
|||||||
import js from "@eslint/js";
|
import js from "@eslint/js";
|
||||||
|
import jsdoc from "eslint-plugin-jsdoc";
|
||||||
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
import globals from "globals";
|
import globals from "globals";
|
||||||
import { defineConfig } from "eslint/config";
|
|
||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
{ files: ["**/*.{js,mjs,cjs}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.browser } },
|
// Ordner, die nicht gelintet werden sollen (an dein Projekt anpassen)
|
||||||
]);
|
globalIgnores(["sessions/", "uploads/", "data/"]),
|
||||||
|
|
||||||
|
// Server-Code (Node)
|
||||||
|
{
|
||||||
|
files: ["**/*.{js,mjs,cjs}"],
|
||||||
|
ignores: ["public/**"],
|
||||||
|
plugins: { js },
|
||||||
|
extends: ["js/recommended"],
|
||||||
|
languageOptions: { globals: globals.node },
|
||||||
|
},
|
||||||
|
|
||||||
|
// Frontend-Code (Browser)
|
||||||
|
{
|
||||||
|
files: ["public/**/*.js"],
|
||||||
|
plugins: { js },
|
||||||
|
extends: ["js/recommended"],
|
||||||
|
languageOptions: { globals: globals.browser },
|
||||||
|
},
|
||||||
|
|
||||||
|
// JSDoc-Regeln
|
||||||
|
jsdoc.configs["flat/recommended-typescript-flavor"],
|
||||||
|
]);
|
||||||
+9
-3
@@ -6,10 +6,12 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22"
|
"node": ">=22"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
"dev": "node --watch server.js",
|
"dev": "node --watch server.js",
|
||||||
"lint": "node scripts/lint.mjs",
|
"lint": "eslint .",
|
||||||
|
"typecheck": "tsc --noEmit && tsc --noEmit -p public",
|
||||||
|
"check": "npm run lint && npm run typecheck",
|
||||||
"test": "node --test"
|
"test": "node --test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -25,7 +27,11 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@types/node": "^26.6.2",
|
||||||
"eslint": "^10.11.0",
|
"eslint": "^10.11.0",
|
||||||
"globals": "^17.12.0"
|
"eslint-plugin-jsdoc": "^64.5.4",
|
||||||
|
"globals": "^17.12.0",
|
||||||
|
"typescript": "^6.0.3",
|
||||||
|
"typescript-eslint": "^8.70.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"include": ["**/*.js"]
|
||||||
|
}
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import path from "path";
|
|
||||||
|
|
||||||
const bool = (value, fallback = false) => {
|
|
||||||
if (value == null || value === "") return fallback;
|
|
||||||
return ["1", "true", "yes", "on"].includes(String(value).toLowerCase());
|
|
||||||
};
|
|
||||||
|
|
||||||
const int = (value, fallback) => {
|
|
||||||
const parsed = Number.parseInt(value, 10);
|
|
||||||
return Number.isInteger(parsed) ? parsed : fallback;
|
|
||||||
};
|
|
||||||
|
|
||||||
const trustProxy = (value, fallback = false) => {
|
|
||||||
if (value == null || value === "") return fallback;
|
|
||||||
const normalized = String(value).toLowerCase();
|
|
||||||
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
||||||
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
||||||
const parsed = Number.parseInt(value, 10);
|
|
||||||
return Number.isInteger(parsed) ? parsed : value;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function createConfig(overrides = {}) {
|
|
||||||
const rootDir = overrides.rootDir || process.cwd();
|
|
||||||
const dataDir = overrides.dataDir || path.resolve(rootDir, "data");
|
|
||||||
const sessionsDir = overrides.sessionsDir || path.resolve(rootDir, "sessions");
|
|
||||||
const testMode = bool(process.env.TEST_MODE, process.env.NODE_ENV === "test");
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
rootDir,
|
|
||||||
publicDir: path.resolve(rootDir, "public"),
|
|
||||||
dataDir,
|
|
||||||
uploadDir: process.env.UPLOAD_DIR || (process.env.NODE_ENV === "production" ? "/app/data/uploads" : path.resolve(dataDir, "uploads")),
|
|
||||||
uploadMaxSizeMb: Math.max(1, int(process.env.UPLOAD_MAX_SIZE_MB, 10)),
|
|
||||||
uploadMaxFiles: Math.max(1, int(process.env.UPLOAD_MAX_FILES, 5)),
|
|
||||||
uploadMaxPixels: Math.max(1, int(process.env.UPLOAD_MAX_PIXELS, 30000000)),
|
|
||||||
visionModel: process.env.VISION_MODEL || "gemma3:4b",
|
|
||||||
sessionsDir,
|
|
||||||
port: int(process.env.PORT, 3000),
|
|
||||||
dbPath: process.env.DB_PATH || path.resolve(dataDir, "app.db"),
|
|
||||||
sessionSecret: process.env.SESSION_SECRET || "change-me-session-secret",
|
|
||||||
cookieSecure: bool(process.env.COOKIE_SECURE, false),
|
|
||||||
allowInsecureCookies: bool(process.env.ALLOW_INSECURE_COOKIES, false),
|
|
||||||
trustProxy: trustProxy(process.env.TRUST_PROXY, false),
|
|
||||||
sessionMaxAgeMs: int(process.env.SESSION_MAX_AGE_MS, 7 * 24 * 60 * 60 * 1000),
|
|
||||||
registrationInviteCode: process.env.REGISTRATION_INVITE_CODE || "",
|
|
||||||
sameOriginProtectionEnabled: bool(process.env.SAME_ORIGIN_PROTECTION_ENABLED, true),
|
|
||||||
staticMaxAge: process.env.STATIC_MAX_AGE || (process.env.NODE_ENV === "production" ? "1h" : 0),
|
|
||||||
staticEtag: bool(process.env.STATIC_ETAG, true),
|
|
||||||
staticFallthrough: bool(process.env.STATIC_FALLTHROUGH, true),
|
|
||||||
eventLoopLagMonitorEnabled: bool(process.env.EVENT_LOOP_LAG_MONITOR_ENABLED, !testMode),
|
|
||||||
eventLoopLagWarnMs: int(process.env.EVENT_LOOP_LAG_WARN_MS, 50),
|
|
||||||
slowRequestLoggingEnabled: bool(process.env.SLOW_REQUEST_LOGGING_ENABLED, !testMode),
|
|
||||||
slowRequestWarnMs: int(process.env.SLOW_REQUEST_WARN_MS, 50),
|
|
||||||
structuredRequestLoggingEnabled: bool(process.env.STRUCTURED_REQUEST_LOGGING_ENABLED, !testMode),
|
|
||||||
visionDebugSha256Enabled: bool(process.env.VISION_DEBUG_SHA256_ENABLED, false),
|
|
||||||
chatLimit: int(process.env.CHAT_LIMIT, 10),
|
|
||||||
chatHistoryLimit: int(process.env.CHAT_HISTORY_LIMIT, 20),
|
|
||||||
chatSummaryUpdateEveryMessages: int(process.env.CHAT_SUMMARY_UPDATE_EVERY_MESSAGES, 8),
|
|
||||||
chatSummaryMaxChars: int(process.env.CHAT_SUMMARY_MAX_CHARS, 2200),
|
|
||||||
chatMaxRetries: int(process.env.CHAT_MAX_RETRIES, 5),
|
|
||||||
modelApiBaseUrl: process.env.MODEL_API_BASE_URL || "https://ai.krishd.ch/api",
|
|
||||||
modelRequestYTimeoutMs: int(process.env.MODEL_REQUEST_TIMEOUT_MS, 180000),
|
|
||||||
modelConnectTimeoutMs: Math.max(1, int(process.env.MODEL_CONNECT_TIMEOUT_MS, 30000)),
|
|
||||||
modelInactivityTimeoutMs: Math.max(1, int(process.env.MODEL_INACTIVITY_TIMEOUT_MS, int(process.env.MODEL_REQUEST_TIMEOUT_MS, 180000))),
|
|
||||||
modelMaxGenerationMs: Math.max(1, int(process.env.MODEL_MAX_GENERATION_MS, 600000)),
|
|
||||||
ollamaKeepAlive: process.env.OLLAMA_KEEP_ALIVE || "30m",
|
|
||||||
searxngUrl: process.env.SEARXNG_URL || "",
|
|
||||||
webSearchTimeoutMs: Math.max(1, int(process.env.WEB_SEARCH_TIMEOUT_MS, 8000)),
|
|
||||||
modelListTimeoutMs: int(process.env.MODEL_LIST_TIMEOUT_MS, 4000),
|
|
||||||
modelUnloadAfterMs: int(process.env.MODEL_UNLOAD_AFTER_MS, 30 * 1000),
|
|
||||||
modelsCacheTtlMs: int(process.env.MODELS_CACHE_TTL_MS, 5 * 60 * 1000),
|
|
||||||
authRateLimitWindowMs: int(process.env.AUTH_RATE_LIMIT_WINDOW_MS, 15 * 60 * 1000),
|
|
||||||
authRateLimitMaxPerIp: int(process.env.AUTH_RATE_LIMIT_MAX_PER_IP, 20),
|
|
||||||
authRateLimitMaxPerAccount: int(process.env.AUTH_RATE_LIMIT_MAX_PER_ACCOUNT, 10),
|
|
||||||
chatRateLimitWindowMs: int(process.env.CHAT_RATE_LIMIT_WINDOW_MS, 60 * 1000),
|
|
||||||
chatRateLimitMaxPerIp: int(process.env.CHAT_RATE_LIMIT_MAX_PER_IP, 60),
|
|
||||||
chatRateLimitMaxPerAccount: int(process.env.CHAT_RATE_LIMIT_MAX_PER_ACCOUNT, 30),
|
|
||||||
rateLimitCleanupIntervalMs: int(process.env.RATE_LIMIT_CLEANUP_INTERVAL_MS, 60 * 1000),
|
|
||||||
testMode,
|
|
||||||
dropLegacyChats: bool(process.env.DROP_LEGACY_CHATS, true)
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolved = {...config, ...overrides};
|
|
||||||
if (process.env.NODE_ENV === "production" && !resolved.testMode) {
|
|
||||||
if (resolved.sessionSecret === "change-me-session-secret") {
|
|
||||||
throw new Error("SESSION_SECRET must be set in production.");
|
|
||||||
}
|
|
||||||
if (!resolved.cookieSecure && !resolved.allowInsecureCookies) {
|
|
||||||
throw new Error("COOKIE_SECURE must be true in production unless ALLOW_INSECURE_COOKIES=true is set.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
+282
@@ -0,0 +1,282 @@
|
|||||||
|
import path from "path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mögliche Werte für die Express-Einstellung `trust proxy`.
|
||||||
|
*
|
||||||
|
* - `boolean`: allen oder keinem Proxy vertrauen
|
||||||
|
* - `number`: Anzahl der vertrauenswürdigen Proxy-Hops
|
||||||
|
* - `string`: z. B. `"loopback"` oder eine IP-Adresse/Subnetz
|
||||||
|
*
|
||||||
|
* Todo:
|
||||||
|
* Fallback soll soll Fehler loggen.
|
||||||
|
*/
|
||||||
|
export type TrustProxyValue = boolean | number | string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vollständige Anwendungskonfiguration.
|
||||||
|
*
|
||||||
|
* Wird von {@link createConfig} aus Umgebungsvariablen und optionalen
|
||||||
|
* Overrides zusammengesetzt.
|
||||||
|
*/
|
||||||
|
export interface Config {
|
||||||
|
/** Wurzelverzeichnis der Anwendung. */
|
||||||
|
rootDir: string;
|
||||||
|
/** Verzeichnis für statische Dateien. */
|
||||||
|
publicDir: string;
|
||||||
|
/** Verzeichnis für persistente Daten. */
|
||||||
|
dataDir: string;
|
||||||
|
/** Zielverzeichnis für hochgeladene Dateien. */
|
||||||
|
uploadDir: string;
|
||||||
|
/** Maximale Upload-Größe pro Datei in MB (mindestens 1). */
|
||||||
|
uploadMaxSizeMb: number;
|
||||||
|
/** Maximale Anzahl Dateien pro Upload (mindestens 1). */
|
||||||
|
uploadMaxFiles: number;
|
||||||
|
/** Maximale Pixelanzahl eines hochgeladenen Bildes (mindestens 1). */
|
||||||
|
uploadMaxPixels: number;
|
||||||
|
/** Name des Vision-Modells. */
|
||||||
|
visionModel: string;
|
||||||
|
/** Verzeichnis für Session-Dateien. */
|
||||||
|
sessionsDir: string;
|
||||||
|
/** Port, auf dem der Server lauscht. */
|
||||||
|
port: number;
|
||||||
|
/** Pfad zur SQLite-Datenbank. */
|
||||||
|
dbPath: string;
|
||||||
|
/** Secret zum Signieren von Session-Cookies. */
|
||||||
|
sessionSecret: string;
|
||||||
|
/** Ob Cookies nur über HTTPS gesendet werden. */
|
||||||
|
cookieSecure: boolean;
|
||||||
|
/** Erlaubt unsichere Cookies in Produktion (nicht empfohlen). */
|
||||||
|
allowInsecureCookies: boolean;
|
||||||
|
/** Wert für die Express-Einstellung `trust proxy`. */
|
||||||
|
trustProxy: TrustProxyValue;
|
||||||
|
/** Maximale Session-Dauer in Millisekunden. */
|
||||||
|
sessionMaxAgeMs: number;
|
||||||
|
/** Einladungscode für die Registrierung (leer = kein Code nötig). */
|
||||||
|
registrationInviteCode: string;
|
||||||
|
/** Aktiviert den Same-Origin-Schutz. */
|
||||||
|
sameOriginProtectionEnabled: boolean;
|
||||||
|
/** Cache-Dauer für statische Dateien (z. B. `"1h"` oder `0`). */
|
||||||
|
staticMaxAge: string | number;
|
||||||
|
/** Ob ETags für statische Dateien gesendet werden. */
|
||||||
|
staticEtag: boolean;
|
||||||
|
/** Ob nicht gefundene statische Dateien an die nächste Middleware weitergereicht werden. */
|
||||||
|
staticFallthrough: boolean;
|
||||||
|
/** Aktiviert die Überwachung der Event-Loop-Verzögerung. */
|
||||||
|
eventLoopLagMonitorEnabled: boolean;
|
||||||
|
/** Schwellwert für Event-Loop-Warnungen in Millisekunden. */
|
||||||
|
eventLoopLagWarnMs: number;
|
||||||
|
/** Aktiviert das Logging langsamer Requests. */
|
||||||
|
slowRequestLoggingEnabled: boolean;
|
||||||
|
/** Schwellwert für langsame Requests in Millisekunden. */
|
||||||
|
slowRequestWarnMs: number;
|
||||||
|
/** Aktiviert strukturiertes Request-Logging. */
|
||||||
|
structuredRequestLoggingEnabled: boolean;
|
||||||
|
/** Loggt SHA-256-Hashes von Vision-Eingaben zu Debug-Zwecken. */
|
||||||
|
visionDebugSha256Enabled: boolean;
|
||||||
|
/** Maximale Anzahl Chats pro Benutzer. */
|
||||||
|
chatLimit: number;
|
||||||
|
/** Anzahl Nachrichten, die als Verlauf ans Modell gesendet werden. */
|
||||||
|
chatHistoryLimit: number;
|
||||||
|
/** Nach wie vielen Nachrichten die Chat-Zusammenfassung aktualisiert wird. */
|
||||||
|
chatSummaryUpdateEveryMessages: number;
|
||||||
|
/** Maximale Länge der Chat-Zusammenfassung in Zeichen. */
|
||||||
|
chatSummaryMaxChars: number;
|
||||||
|
/** Maximale Anzahl Wiederholungsversuche bei Modellanfragen. */
|
||||||
|
chatMaxRetries: number;
|
||||||
|
/** Basis-URL der Modell-API. */
|
||||||
|
modelApiBaseUrl: string;
|
||||||
|
/** Gesamt-Timeout einer Modellanfrage in Millisekunden. */
|
||||||
|
modelRequestTimeoutMs: number;
|
||||||
|
/** Timeout für den Verbindungsaufbau zur Modell-API in Millisekunden. */
|
||||||
|
modelConnectTimeoutMs: number;
|
||||||
|
/** Maximale Zeit ohne neue Daten vom Modell in Millisekunden. */
|
||||||
|
modelInactivityTimeoutMs: number;
|
||||||
|
/** Maximale Generierungsdauer in Millisekunden. */
|
||||||
|
modelMaxGenerationMs: number;
|
||||||
|
/** Wie lange Ollama ein Modell im Speicher hält (z. B. `"30m"`). */
|
||||||
|
ollamaKeepAlive: string;
|
||||||
|
/** URL der SearXNG-Instanz (leer = Websuche deaktiviert). */
|
||||||
|
searxngUrl: string;
|
||||||
|
/** Timeout für Websuchen in Millisekunden. */
|
||||||
|
webSearchTimeoutMs: number;
|
||||||
|
/** Timeout beim Abrufen der Modellliste in Millisekunden. */
|
||||||
|
modelListTimeoutMs: number;
|
||||||
|
/** Nach wie vielen Millisekunden Inaktivität ein Modell entladen wird. */
|
||||||
|
modelUnloadAfterMs: number;
|
||||||
|
/** Cache-Dauer der Modellliste in Millisekunden. */
|
||||||
|
modelsCacheTtlMs: number;
|
||||||
|
/** Zeitfenster für das Auth-Rate-Limit in Millisekunden. */
|
||||||
|
authRateLimitWindowMs: number;
|
||||||
|
/** Maximale Auth-Versuche pro IP im Zeitfenster. */
|
||||||
|
authRateLimitMaxPerIp: number;
|
||||||
|
/** Maximale Auth-Versuche pro Account im Zeitfenster. */
|
||||||
|
authRateLimitMaxPerAccount: number;
|
||||||
|
/** Zeitfenster für das Chat-Rate-Limit in Millisekunden. */
|
||||||
|
chatRateLimitWindowMs: number;
|
||||||
|
/** Maximale Chat-Anfragen pro IP im Zeitfenster. */
|
||||||
|
chatRateLimitMaxPerIp: number;
|
||||||
|
/** Maximale Chat-Anfragen pro Account im Zeitfenster. */
|
||||||
|
chatRateLimitMaxPerAccount: number;
|
||||||
|
/** Intervall zum Aufräumen der Rate-Limit-Daten in Millisekunden. */
|
||||||
|
rateLimitCleanupIntervalMs: number;
|
||||||
|
/** Ob die Anwendung im Testmodus läuft. */
|
||||||
|
testMode: boolean;
|
||||||
|
/** Ob Chats im alten Format beim Start gelöscht werden. */
|
||||||
|
dropLegacyChats: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wandelt einen Wert (typischerweise eine Umgebungsvariable) in einen Boolean um.
|
||||||
|
*
|
||||||
|
* Als `true` gelten `"1"`, `"true"`, `"yes"` und `"on"` (Groß-/Kleinschreibung egal).
|
||||||
|
* Alle anderen Werte ergeben `false`.
|
||||||
|
*
|
||||||
|
* @param value - Der umzuwandelnde Wert.
|
||||||
|
* @param fallback - Rückgabewert, wenn `value` `null`, `undefined` oder leer ist.
|
||||||
|
* @returns Der interpretierte Boolean.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* bool("YES"); // true
|
||||||
|
* bool("0"); // false
|
||||||
|
* bool(undefined, true); // true (Fallback)
|
||||||
|
*/
|
||||||
|
const bool = (value: unknown, fallback: boolean = false): boolean => {
|
||||||
|
if (value == null || value === "") return fallback;
|
||||||
|
return ["1", "true", "yes", "on"].includes(String(value).toLowerCase());
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wandelt einen Wert (typischerweise eine Umgebungsvariable) in eine Ganzzahl um.
|
||||||
|
*
|
||||||
|
* @param value - Der umzuwandelnde Wert.
|
||||||
|
* @param fallback - Rückgabewert, wenn `value` keine gültige Ganzzahl ergibt.
|
||||||
|
* @returns Die geparste Ganzzahl oder `fallback`.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* int("3000", 80); // 3000
|
||||||
|
* int("abc", 80); // 80
|
||||||
|
* int(undefined, 80); // 80
|
||||||
|
*/
|
||||||
|
const int = (value: unknown, fallback: number): number => {
|
||||||
|
const parsed = Number.parseInt(String(value), 10);
|
||||||
|
return Number.isInteger(parsed) ? parsed : fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wandelt einen Wert in eine gültige Einstellung für Express' `trust proxy` um.
|
||||||
|
*
|
||||||
|
* Reihenfolge der Auswertung:
|
||||||
|
* 1. leer/nicht gesetzt → `fallback`
|
||||||
|
* 2. `"1"`, `"true"`, `"yes"`, `"on"` → `true`
|
||||||
|
* 3. `"0"`, `"false"`, `"no"`, `"off"` → `false`
|
||||||
|
* 4. Ganzzahl → Anzahl vertrauenswürdiger Proxys
|
||||||
|
* 5. sonst → Originalwert als String (z. B. `"loopback"` oder eine IP)
|
||||||
|
*
|
||||||
|
* @param value - Der umzuwandelnde Wert.
|
||||||
|
* @param fallback - Rückgabewert, wenn `value` `null`, `undefined` oder leer ist.
|
||||||
|
* @returns Boolean, Zahl oder String für `trust proxy`.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* trustProxy("on"); // true
|
||||||
|
* trustProxy("2"); // 2
|
||||||
|
* trustProxy("loopback"); // "loopback"
|
||||||
|
*/
|
||||||
|
const trustProxy = (value: unknown, fallback: boolean = false): TrustProxyValue => {
|
||||||
|
if (value == null || value === "") return fallback;
|
||||||
|
const normalized = String(value).toLowerCase();
|
||||||
|
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
||||||
|
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
||||||
|
const parsed = Number.parseInt(normalized, 10);
|
||||||
|
return Number.isInteger(parsed) ? parsed : String(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt die Anwendungskonfiguration aus Umgebungsvariablen.
|
||||||
|
*
|
||||||
|
* Werte aus `overrides` haben Vorrang vor Umgebungsvariablen und Standardwerten.
|
||||||
|
* In Produktion (ausser im Testmodus) wird zusätzlich geprüft, ob
|
||||||
|
* sicherheitsrelevante Einstellungen korrekt gesetzt sind.
|
||||||
|
*
|
||||||
|
* @param overrides - Einzelne Konfigurationswerte, die überschrieben werden sollen.
|
||||||
|
* @returns Die vollständige, aufgelöste Konfiguration.
|
||||||
|
* @throws {Error} Wenn in Produktion kein `SESSION_SECRET` gesetzt ist.
|
||||||
|
* @throws {Error} Wenn in Produktion Cookies unsicher sind, ohne dass
|
||||||
|
* `ALLOW_INSECURE_COOKIES` gesetzt ist.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const config = createConfig({ port: 4000, testMode: true });
|
||||||
|
*/
|
||||||
|
export function createConfig(overrides: Partial<Config> = {}): Config {
|
||||||
|
const rootDir = overrides.rootDir || process.cwd();
|
||||||
|
const dataDir = overrides.dataDir || path.resolve(rootDir, "data");
|
||||||
|
const sessionsDir = overrides.sessionsDir || path.resolve(rootDir, "sessions");
|
||||||
|
const testMode = bool(process.env.TEST_MODE, process.env.NODE_ENV === "test");
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
rootDir,
|
||||||
|
publicDir: path.resolve(rootDir, "public"),
|
||||||
|
dataDir,
|
||||||
|
uploadDir: process.env.UPLOAD_DIR || (process.env.NODE_ENV === "production" ? "/app/data/uploads" : path.resolve(dataDir, "uploads")),
|
||||||
|
uploadMaxSizeMb: Math.max(1, int(process.env.UPLOAD_MAX_SIZE_MB, 10)),
|
||||||
|
uploadMaxFiles: Math.max(1, int(process.env.UPLOAD_MAX_FILES, 5)),
|
||||||
|
uploadMaxPixels: Math.max(1, int(process.env.UPLOAD_MAX_PIXELS, 30000000)),
|
||||||
|
visionModel: process.env.VISION_MODEL || "gemma3:4b",
|
||||||
|
sessionsDir,
|
||||||
|
port: int(process.env.PORT, 3000),
|
||||||
|
dbPath: process.env.DB_PATH || path.resolve(dataDir, "app.db"),
|
||||||
|
sessionSecret: process.env.SESSION_SECRET || "change-me-session-secret",
|
||||||
|
cookieSecure: bool(process.env.COOKIE_SECURE, false),
|
||||||
|
allowInsecureCookies: bool(process.env.ALLOW_INSECURE_COOKIES, false),
|
||||||
|
trustProxy: trustProxy(process.env.TRUST_PROXY, false),
|
||||||
|
sessionMaxAgeMs: int(process.env.SESSION_MAX_AGE_MS, 7 * 24 * 60 * 60 * 1000),
|
||||||
|
registrationInviteCode: process.env.REGISTRATION_INVITE_CODE || "",
|
||||||
|
sameOriginProtectionEnabled: bool(process.env.SAME_ORIGIN_PROTECTION_ENABLED, true),
|
||||||
|
staticMaxAge: process.env.STATIC_MAX_AGE || (process.env.NODE_ENV === "production" ? "1h" : 0),
|
||||||
|
staticEtag: bool(process.env.STATIC_ETAG, true),
|
||||||
|
staticFallthrough: bool(process.env.STATIC_FALLTHROUGH, true),
|
||||||
|
eventLoopLagMonitorEnabled: bool(process.env.EVENT_LOOP_LAG_MONITOR_ENABLED, !testMode),
|
||||||
|
eventLoopLagWarnMs: int(process.env.EVENT_LOOP_LAG_WARN_MS, 50),
|
||||||
|
slowRequestLoggingEnabled: bool(process.env.SLOW_REQUEST_LOGGING_ENABLED, !testMode),
|
||||||
|
slowRequestWarnMs: int(process.env.SLOW_REQUEST_WARN_MS, 50),
|
||||||
|
structuredRequestLoggingEnabled: bool(process.env.STRUCTURED_REQUEST_LOGGING_ENABLED, !testMode),
|
||||||
|
visionDebugSha256Enabled: bool(process.env.VISION_DEBUG_SHA256_ENABLED, false),
|
||||||
|
chatLimit: int(process.env.CHAT_LIMIT, 10),
|
||||||
|
chatHistoryLimit: int(process.env.CHAT_HISTORY_LIMIT, 20),
|
||||||
|
chatSummaryUpdateEveryMessages: int(process.env.CHAT_SUMMARY_UPDATE_EVERY_MESSAGES, 8),
|
||||||
|
chatSummaryMaxChars: int(process.env.CHAT_SUMMARY_MAX_CHARS, 2200),
|
||||||
|
chatMaxRetries: int(process.env.CHAT_MAX_RETRIES, 5),
|
||||||
|
modelApiBaseUrl: process.env.MODEL_API_BASE_URL || "https://ai.krishd.ch/api",
|
||||||
|
modelRequestTimeoutMs: int(process.env.MODEL_REQUEST_TIMEOUT_MS, 180000),
|
||||||
|
modelConnectTimeoutMs: Math.max(1, int(process.env.MODEL_CONNECT_TIMEOUT_MS, 30000)),
|
||||||
|
modelInactivityTimeoutMs: Math.max(1, int(process.env.MODEL_INACTIVITY_TIMEOUT_MS, int(process.env.MODEL_REQUEST_TIMEOUT_MS, 180000))),
|
||||||
|
modelMaxGenerationMs: Math.max(1, int(process.env.MODEL_MAX_GENERATION_MS, 600000)),
|
||||||
|
ollamaKeepAlive: process.env.OLLAMA_KEEP_ALIVE || "30m",
|
||||||
|
searxngUrl: process.env.SEARXNG_URL || "",
|
||||||
|
webSearchTimeoutMs: Math.max(1, int(process.env.WEB_SEARCH_TIMEOUT_MS, 8000)),
|
||||||
|
modelListTimeoutMs: int(process.env.MODEL_LIST_TIMEOUT_MS, 4000),
|
||||||
|
modelUnloadAfterMs: int(process.env.MODEL_UNLOAD_AFTER_MS, 30 * 1000),
|
||||||
|
modelsCacheTtlMs: int(process.env.MODELS_CACHE_TTL_MS, 5 * 60 * 1000),
|
||||||
|
authRateLimitWindowMs: int(process.env.AUTH_RATE_LIMIT_WINDOW_MS, 15 * 60 * 1000),
|
||||||
|
authRateLimitMaxPerIp: int(process.env.AUTH_RATE_LIMIT_MAX_PER_IP, 20),
|
||||||
|
authRateLimitMaxPerAccount: int(process.env.AUTH_RATE_LIMIT_MAX_PER_ACCOUNT, 10),
|
||||||
|
chatRateLimitWindowMs: int(process.env.CHAT_RATE_LIMIT_WINDOW_MS, 60 * 1000),
|
||||||
|
chatRateLimitMaxPerIp: int(process.env.CHAT_RATE_LIMIT_MAX_PER_IP, 60),
|
||||||
|
chatRateLimitMaxPerAccount: int(process.env.CHAT_RATE_LIMIT_MAX_PER_ACCOUNT, 30),
|
||||||
|
rateLimitCleanupIntervalMs: int(process.env.RATE_LIMIT_CLEANUP_INTERVAL_MS, 60 * 1000),
|
||||||
|
testMode,
|
||||||
|
dropLegacyChats: bool(process.env.DROP_LEGACY_CHATS, true)
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolved: Config = { ...config, ...overrides };
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === "production" && !resolved.testMode) {
|
||||||
|
if (resolved.sessionSecret === "change-me-session-secret") {
|
||||||
|
throw new Error("SESSION_SECRET must be set in production.");
|
||||||
|
}
|
||||||
|
if (!resolved.cookieSecure && !resolved.allowInsecureCookies) {
|
||||||
|
throw new Error("COOKIE_SECURE must be true in production unless ALLOW_INSECURE_COOKIES=true is set.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": false,
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"include": ["**/*.js", "src/*.ts"],
|
||||||
|
"exclude": ["node_modules", "public"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user