Added Typescript and rewritten config.js into config.ts (use AI)
This commit is contained in:
@@ -1,2 +1,4 @@
|
||||
node_modules/
|
||||
data/uploads/
|
||||
package-lock.json
|
||||
.gitignore
|
||||
+24
-2
@@ -1,7 +1,29 @@
|
||||
import js from "@eslint/js";
|
||||
import jsdoc from "eslint-plugin-jsdoc";
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import globals from "globals";
|
||||
import { defineConfig } from "eslint/config";
|
||||
|
||||
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"],
|
||||
]);
|
||||
+8
-2
@@ -9,7 +9,9 @@
|
||||
"scripts": {
|
||||
"start": "node 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"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -25,7 +27,11 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/node": "^26.6.2",
|
||||
"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