Added Typescript and rewritten config.js into config.ts (use AI)

This commit is contained in:
2026-09-23 16:50:29 +02:00
parent 53160b3ea4
commit 1ecb44faa8
7 changed files with 353 additions and 100 deletions
+2
View File
@@ -1,2 +1,4 @@
node_modules/ node_modules/
data/uploads/ data/uploads/
package-lock.json
.gitignore
+24 -2
View File
@@ -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"],
]); ]);
+8 -2
View File
@@ -9,7 +9,9 @@
"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"
} }
} }
+15
View File
@@ -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"]
}
-94
View File
@@ -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
View File
@@ -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;
}
+20
View File
@@ -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"]
}