309 lines
8.9 KiB
TypeScript
309 lines
8.9 KiB
TypeScript
import fs from 'fs';
|
|
import path from 'path';
|
|
import { resolveRuntimePath } from '../runtime-paths';
|
|
import { buildSignalServerPublicUrl, resolveSignalServerTag } from './signal-server-tag';
|
|
|
|
export type ServerHttpProtocol = 'http' | 'https';
|
|
|
|
export interface LinkPreviewConfig {
|
|
enabled: boolean;
|
|
cacheTtlMinutes: number;
|
|
maxCacheSizeMb: number;
|
|
}
|
|
|
|
export interface OpenApiDocsConfig {
|
|
enabled: boolean;
|
|
}
|
|
|
|
export interface ServerVariablesConfig {
|
|
klipyApiKey: string;
|
|
rawgApiKey: string;
|
|
releaseManifestUrl: string;
|
|
serverPort: number;
|
|
serverProtocol: ServerHttpProtocol;
|
|
serverHost: string;
|
|
serverTag: string;
|
|
corsAllowlist: string[];
|
|
linkPreview: LinkPreviewConfig;
|
|
openApiDocs: OpenApiDocsConfig;
|
|
}
|
|
|
|
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 normalizeRawgApiKey(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 normalizeServerTag(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<string, unknown>
|
|
: {};
|
|
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 normalizeCorsAllowlist(value: unknown): string[] {
|
|
if (!Array.isArray(value)) {
|
|
return [];
|
|
}
|
|
|
|
return value
|
|
.filter((entry): entry is string => typeof entry === 'string')
|
|
.map((entry) => entry.trim())
|
|
.filter((entry) => entry.length > 0);
|
|
}
|
|
|
|
function normalizeOpenApiDocsConfig(value: unknown): OpenApiDocsConfig {
|
|
const raw = (value && typeof value === 'object' && !Array.isArray(value))
|
|
? value as Record<string, unknown>
|
|
: {};
|
|
|
|
return { enabled: raw.enabled === true };
|
|
}
|
|
|
|
function hasEnvironmentOverride(value: string | undefined): value is string {
|
|
return typeof value === 'string' && value.trim().length > 0;
|
|
}
|
|
|
|
function readRawVariables(): { rawContents: string; parsed: Record<string, unknown> } {
|
|
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<string, unknown> };
|
|
}
|
|
} 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),
|
|
rawgApiKey: normalizeRawgApiKey(remainingParsed.rawgApiKey),
|
|
releaseManifestUrl: normalizeReleaseManifestUrl(remainingParsed.releaseManifestUrl),
|
|
serverPort: normalizeServerPort(remainingParsed.serverPort),
|
|
serverProtocol: normalizeServerProtocol(remainingParsed.serverProtocol),
|
|
serverHost: normalizeServerHost(remainingParsed.serverHost ?? legacyServerIpAddress),
|
|
serverTag: normalizeServerTag(remainingParsed.serverTag),
|
|
corsAllowlist: normalizeCorsAllowlist(remainingParsed.corsAllowlist),
|
|
linkPreview: normalizeLinkPreviewConfig(remainingParsed.linkPreview),
|
|
openApiDocs: normalizeOpenApiDocsConfig(remainingParsed.openApiDocs)
|
|
};
|
|
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,
|
|
rawgApiKey: normalized.rawgApiKey,
|
|
releaseManifestUrl: normalized.releaseManifestUrl,
|
|
serverPort: normalized.serverPort,
|
|
serverProtocol: normalized.serverProtocol,
|
|
serverHost: normalized.serverHost,
|
|
serverTag: normalized.serverTag,
|
|
corsAllowlist: normalized.corsAllowlist,
|
|
linkPreview: normalized.linkPreview,
|
|
openApiDocs: normalized.openApiDocs
|
|
};
|
|
}
|
|
|
|
export function getCorsAllowlist(): string[] {
|
|
if (hasEnvironmentOverride(process.env.CORS_ALLOWLIST)) {
|
|
return (process.env.CORS_ALLOWLIST ?? '')
|
|
.split(',')
|
|
.map((entry) => entry.trim())
|
|
.filter((entry) => entry.length > 0);
|
|
}
|
|
|
|
return getVariablesConfig().corsAllowlist;
|
|
}
|
|
|
|
export function getVariablesConfig(): ServerVariablesConfig {
|
|
return ensureVariablesConfig();
|
|
}
|
|
|
|
export function getKlipyApiKey(): string {
|
|
return getVariablesConfig().klipyApiKey;
|
|
}
|
|
|
|
export function getRawgApiKey(): string {
|
|
if (hasEnvironmentOverride(process.env.RAWG_API_KEY)) {
|
|
return process.env.RAWG_API_KEY.trim();
|
|
}
|
|
|
|
return getVariablesConfig().rawgApiKey;
|
|
}
|
|
|
|
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 getSignalServerPublicUrl(): string {
|
|
const config = getVariablesConfig();
|
|
|
|
return buildSignalServerPublicUrl(config.serverProtocol, config.serverHost, config.serverPort);
|
|
}
|
|
|
|
export function getServerTag(): string {
|
|
const config = getVariablesConfig();
|
|
|
|
return resolveSignalServerTag(config.serverTag, getSignalServerPublicUrl());
|
|
}
|
|
|
|
export function isHttpsServerEnabled(): boolean {
|
|
return getServerProtocol() === 'https';
|
|
}
|
|
|
|
export function areOpenApiDocsEnabled(): boolean {
|
|
if (hasEnvironmentOverride(process.env.OPENAPI_DOCS_ENABLED)) {
|
|
return process.env.OPENAPI_DOCS_ENABLED.trim().toLowerCase() === 'true';
|
|
}
|
|
|
|
return getVariablesConfig().openApiDocs.enabled;
|
|
}
|
|
|
|
export function setOpenApiDocsEnabled(enabled: boolean): OpenApiDocsConfig {
|
|
if (!fs.existsSync(DATA_DIR)) {
|
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
}
|
|
|
|
const { parsed } = readRawVariables();
|
|
const next = {
|
|
...parsed,
|
|
openApiDocs: { enabled }
|
|
};
|
|
|
|
fs.writeFileSync(VARIABLES_FILE, JSON.stringify(next, null, 2) + '\n', 'utf8');
|
|
ensureVariablesConfig();
|
|
|
|
return { enabled };
|
|
}
|
|
|
|
export function getLinkPreviewConfig(): LinkPreviewConfig {
|
|
return getVariablesConfig().linkPreview;
|
|
}
|