import fs from 'fs'; import path from 'path'; import { resolveRuntimePath } from '../runtime-paths'; export type ServerHttpProtocol = 'http' | 'https'; export interface LinkPreviewConfig { enabled: boolean; cacheTtlMinutes: number; maxCacheSizeMb: number; } export interface ServerVariablesConfig { klipyApiKey: string; releaseManifestUrl: string; serverPort: number; serverProtocol: ServerHttpProtocol; serverHost: string; linkPreview: LinkPreviewConfig; } const DATA_DIR = resolveRuntimePath('data'); const VARIABLES_FILE = path.join(DATA_DIR, 'variables.json'); const DEFAULT_SERVER_PORT = 3001; const DEFAULT_SERVER_PROTOCOL: ServerHttpProtocol = 'http'; const DEFAULT_LINK_PREVIEW_CACHE_TTL_MINUTES = 7200; const DEFAULT_LINK_PREVIEW_MAX_CACHE_SIZE_MB = 50; const HARD_MAX_CACHE_SIZE_MB = 50; function normalizeKlipyApiKey(value: unknown): string { return typeof value === 'string' ? value.trim() : ''; } function normalizeReleaseManifestUrl(value: unknown): string { return typeof value === 'string' ? value.trim() : ''; } function normalizeServerHost(value: unknown): string { return typeof value === 'string' ? value.trim() : ''; } function normalizeServerProtocol( value: unknown, fallback: ServerHttpProtocol = DEFAULT_SERVER_PROTOCOL ): ServerHttpProtocol { if (typeof value === 'boolean') { return value ? 'https' : 'http'; } if (typeof value !== 'string') { return fallback; } const normalized = value.trim().toLowerCase(); if (normalized === 'https' || normalized === 'true') { return 'https'; } if (normalized === 'http' || normalized === 'false') { return 'http'; } return fallback; } function normalizeServerPort(value: unknown, fallback = DEFAULT_SERVER_PORT): number { const parsed = typeof value === 'number' ? value : typeof value === 'string' ? Number.parseInt(value.trim(), 10) : Number.NaN; return Number.isInteger(parsed) && parsed >= 1 && parsed <= 65535 ? parsed : fallback; } function normalizeLinkPreviewConfig(value: unknown): LinkPreviewConfig { const raw = (value && typeof value === 'object' && !Array.isArray(value)) ? value as Record : {}; const enabled = typeof raw.enabled === 'boolean' ? raw.enabled : true; const cacheTtl = typeof raw.cacheTtlMinutes === 'number' && Number.isFinite(raw.cacheTtlMinutes) && raw.cacheTtlMinutes >= 0 ? raw.cacheTtlMinutes : DEFAULT_LINK_PREVIEW_CACHE_TTL_MINUTES; const maxSize = typeof raw.maxCacheSizeMb === 'number' && Number.isFinite(raw.maxCacheSizeMb) && raw.maxCacheSizeMb >= 0 ? Math.min(raw.maxCacheSizeMb, HARD_MAX_CACHE_SIZE_MB) : DEFAULT_LINK_PREVIEW_MAX_CACHE_SIZE_MB; return { enabled, cacheTtlMinutes: cacheTtl, maxCacheSizeMb: maxSize }; } function hasEnvironmentOverride(value: string | undefined): value is string { return typeof value === 'string' && value.trim().length > 0; } function readRawVariables(): { rawContents: string; parsed: Record } { if (!fs.existsSync(VARIABLES_FILE)) { return { rawContents: '', parsed: {} }; } const rawContents = fs.readFileSync(VARIABLES_FILE, 'utf8'); if (!rawContents.trim()) { return { rawContents, parsed: {} }; } try { const parsed = JSON.parse(rawContents) as unknown; if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { return { rawContents, parsed: parsed as Record }; } } catch (error) { console.warn('[Config] Failed to parse variables.json. Recreating it with defaults.', error); } return { rawContents, parsed: {} }; } export function getVariablesConfigPath(): string { return VARIABLES_FILE; } export function ensureVariablesConfig(): ServerVariablesConfig { if (!fs.existsSync(DATA_DIR)) { fs.mkdirSync(DATA_DIR, { recursive: true }); } const { rawContents, parsed } = readRawVariables(); const { serverIpAddress: legacyServerIpAddress, ...remainingParsed } = parsed; const normalized = { ...remainingParsed, klipyApiKey: normalizeKlipyApiKey(remainingParsed.klipyApiKey), releaseManifestUrl: normalizeReleaseManifestUrl(remainingParsed.releaseManifestUrl), serverPort: normalizeServerPort(remainingParsed.serverPort), serverProtocol: normalizeServerProtocol(remainingParsed.serverProtocol), serverHost: normalizeServerHost(remainingParsed.serverHost ?? legacyServerIpAddress), linkPreview: normalizeLinkPreviewConfig(remainingParsed.linkPreview) }; const nextContents = JSON.stringify(normalized, null, 2) + '\n'; if (!fs.existsSync(VARIABLES_FILE) || rawContents !== nextContents) { fs.writeFileSync(VARIABLES_FILE, nextContents, 'utf8'); } return { klipyApiKey: normalized.klipyApiKey, releaseManifestUrl: normalized.releaseManifestUrl, serverPort: normalized.serverPort, serverProtocol: normalized.serverProtocol, serverHost: normalized.serverHost, linkPreview: normalized.linkPreview }; } export function getVariablesConfig(): ServerVariablesConfig { return ensureVariablesConfig(); } export function getKlipyApiKey(): string { return getVariablesConfig().klipyApiKey; } export function hasKlipyApiKey(): boolean { return getKlipyApiKey().length > 0; } export function getReleaseManifestUrl(): string { return getVariablesConfig().releaseManifestUrl; } export function getServerProtocol(): ServerHttpProtocol { if (hasEnvironmentOverride(process.env.SSL)) { return normalizeServerProtocol(process.env.SSL); } return getVariablesConfig().serverProtocol; } export function getServerPort(): number { if (hasEnvironmentOverride(process.env.PORT)) { return normalizeServerPort(process.env.PORT); } return getVariablesConfig().serverPort; } export function getServerHost(): string | undefined { const serverHost = getVariablesConfig().serverHost; return serverHost || undefined; } export function isHttpsServerEnabled(): boolean { return getServerProtocol() === 'https'; } export function getLinkPreviewConfig(): LinkPreviewConfig { return getVariablesConfig().linkPreview; }