From d13758221ee62da8a59fab414397ae540bd8d9a0 Mon Sep 17 00:00:00 2001 From: jubnl Date: Sat, 25 Jul 2026 15:29:13 +0200 Subject: [PATCH] refactor(server): remaining nest domains read env via app-config --- server/src/nest/backup/backup.controller.ts | 3 ++- .../booking-import/kitinerary-extractor.service.ts | 3 ++- .../src/nest/collections/collections.controller.ts | 5 +++-- server/src/nest/feeds/feeds.controller.ts | 4 +++- server/src/nest/files/files.controller.ts | 3 ++- server/src/nest/places/places.controller.ts | 3 ++- server/src/nest/platform/spa-fallback.filter.ts | 4 +++- server/src/nest/plugins/dev-link.ts | 8 +++++--- server/src/nest/plugins/host/create-rpc-host.ts | 3 ++- server/src/nest/plugins/host/rate-limit.ts | 13 ++++++++----- server/src/nest/plugins/install/host-compat.ts | 3 ++- server/src/nest/plugins/paths.ts | 7 ++++--- .../src/nest/plugins/registry/registry.service.ts | 6 +++--- .../nest/plugins/supervisor/plugin-supervisor.ts | 10 ++++++---- server/src/nest/trips/trips.controller.ts | 3 ++- 15 files changed, 49 insertions(+), 29 deletions(-) diff --git a/server/src/nest/backup/backup.controller.ts b/server/src/nest/backup/backup.controller.ts index 07da9e9e9..d9a5b8104 100644 --- a/server/src/nest/backup/backup.controller.ts +++ b/server/src/nest/backup/backup.controller.ts @@ -17,6 +17,7 @@ import { import { FileInterceptor } from '@nestjs/platform-express'; import type { Request, Response } from 'express'; import fs from 'fs'; +import { readEnv } from '../../app-config'; import type { User } from '../../types'; import { BackupService } from './backup.service'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; @@ -148,7 +149,7 @@ export class BackupController { } catch (err) { console.error('[backup] PUT auto-settings:', err); const msg = err instanceof Error ? err.message : String(err); - throw new HttpException({ error: 'Could not save auto-backup settings', detail: process.env.NODE_ENV?.toLowerCase() !== 'production' ? msg : undefined }, 500); + throw new HttpException({ error: 'Could not save auto-backup settings', detail: !readEnv().app.isProduction ? msg : undefined }, 500); } } diff --git a/server/src/nest/booking-import/kitinerary-extractor.service.ts b/server/src/nest/booking-import/kitinerary-extractor.service.ts index fbb476a02..9fc6999eb 100644 --- a/server/src/nest/booking-import/kitinerary-extractor.service.ts +++ b/server/src/nest/booking-import/kitinerary-extractor.service.ts @@ -4,6 +4,7 @@ import { existsSync, readdirSync, writeFileSync, unlinkSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, extname } from 'node:path'; import { randomUUID } from 'node:crypto'; +import { readEnv } from '../../app-config'; import { execSync } from 'node:child_process'; import { promisify } from 'node:util'; import type { KiReservation } from './kitinerary.types'; @@ -78,7 +79,7 @@ export class KitineraryExtractorService implements OnModuleInit { } private findBinary(): string | null { - const envPath = process.env.KITINERARY_EXTRACTOR_PATH; + const envPath = readEnv().integrations.kitineraryExtractorPath; if (envPath) { if (existsSync(envPath)) return envPath; console.warn(`[KItinerary] KITINERARY_EXTRACTOR_PATH="${envPath}" not found`); diff --git a/server/src/nest/collections/collections.controller.ts b/server/src/nest/collections/collections.controller.ts index a2bbc070c..593bb213d 100644 --- a/server/src/nest/collections/collections.controller.ts +++ b/server/src/nest/collections/collections.controller.ts @@ -20,6 +20,7 @@ import type { Request } from 'express'; import { diskStorage } from 'multer'; import path from 'path'; import fs from 'fs'; +import { readEnv } from '../../app-config'; import { v4 as uuidv4 } from 'uuid'; import type { User } from '../../types'; import { CollectionsService } from './collections.service'; @@ -195,7 +196,7 @@ export class CollectionsController { @UploadedFile() file: Express.Multer.File | undefined, @Headers('x-socket-id') socketId?: string, ) { - if (process.env.DEMO_MODE?.toLowerCase() === 'true' && isDemoEmail(user.email)) { + if (readEnv().demo.enabled && isDemoEmail(user.email)) { throw new HttpException({ error: 'Uploads are disabled in demo mode. Self-host TREK for full functionality.' }, 403); } if (!file) throw new HttpException({ error: 'No image uploaded' }, 400); @@ -366,7 +367,7 @@ export class CollectionsController { @Post(':id/cover') @UseInterceptors(FileInterceptor('cover', COVER_UPLOAD)) uploadCover(@CurrentUser() user: User, @Param('id') id: string, @UploadedFile() file: Express.Multer.File | undefined, @Headers('x-socket-id') socketId?: string) { - if (process.env.DEMO_MODE?.toLowerCase() === 'true' && isDemoEmail(user.email)) { + if (readEnv().demo.enabled && isDemoEmail(user.email)) { throw new HttpException({ error: 'Uploads are disabled in demo mode. Self-host TREK for full functionality.' }, 403); } if (!file) throw new HttpException({ error: 'No image uploaded' }, 400); diff --git a/server/src/nest/feeds/feeds.controller.ts b/server/src/nest/feeds/feeds.controller.ts index 7e094bfbe..ebb25cf11 100644 --- a/server/src/nest/feeds/feeds.controller.ts +++ b/server/src/nest/feeds/feeds.controller.ts @@ -11,6 +11,7 @@ import { UseGuards, } from '@nestjs/common'; import type { Request, Response } from 'express'; +import { readEnv } from '../../app-config'; import { FeedsService } from './feeds.service'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { CurrentUser } from '../auth/current-user.decorator'; @@ -23,7 +24,8 @@ import { db } from '../../db/database'; // the link is still absolute and copy-pasteable as webcal:// instead of a dead // relative path. function resolveFeedBase(req: Request): string { - const configured = (process.env.APP_URL || '').replace(/\/$/, ''); + // Single trailing slash stripped on purpose (legacy parity; notifications strips all). + const configured = (readEnv().app.appUrl || '').replace(/\/$/, ''); if (configured) return configured; const host = req.get('host'); return host ? `${req.protocol}://${host}` : ''; diff --git a/server/src/nest/files/files.controller.ts b/server/src/nest/files/files.controller.ts index 1df59ffc6..669f5fa02 100644 --- a/server/src/nest/files/files.controller.ts +++ b/server/src/nest/files/files.controller.ts @@ -20,6 +20,7 @@ import { diskStorage } from 'multer'; import path from 'path'; import fs from 'fs'; import { v4 as uuidv4 } from 'uuid'; +import { readEnv } from '../../app-config'; import type { User } from '../../types'; import { FilesService } from './files.service'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; @@ -106,7 +107,7 @@ export class FilesController { const cleanup = () => { if (file?.path) { try { fs.unlinkSync(file.path); } catch { /* best-effort */ } } }; try { const trip = this.requireTrip(tripId, user); - if (process.env.DEMO_MODE?.toLowerCase() === 'true' && isDemoEmail(user.email)) { + if (readEnv().demo.enabled && isDemoEmail(user.email)) { throw new HttpException({ error: 'Uploads are disabled in demo mode. Self-host TREK for full functionality.' }, 403); } if (!this.files.can('file_upload', trip, user)) { diff --git a/server/src/nest/places/places.controller.ts b/server/src/nest/places/places.controller.ts index f15891c26..51b5d0017 100644 --- a/server/src/nest/places/places.controller.ts +++ b/server/src/nest/places/places.controller.ts @@ -16,6 +16,7 @@ import { } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { memoryStorage } from 'multer'; +import { readEnv } from '../../app-config'; import type { User } from '../../types'; import { PlacesService } from './places.service'; import { isUpdateConflict } from '../../services/conflictResult'; @@ -283,7 +284,7 @@ export class PlacesController { ) { const trip = this.requireTrip(tripId, user); this.requireEdit(trip, user); - if (process.env.DEMO_MODE?.toLowerCase() === 'true' && isDemoEmail(user.email)) { + if (readEnv().demo.enabled && isDemoEmail(user.email)) { throw new HttpException({ error: 'Uploads are disabled in demo mode. Self-host TREK for full functionality.' }, 403); } if (!file) { diff --git a/server/src/nest/platform/spa-fallback.filter.ts b/server/src/nest/platform/spa-fallback.filter.ts index d1a12be26..0cde149ba 100644 --- a/server/src/nest/platform/spa-fallback.filter.ts +++ b/server/src/nest/platform/spa-fallback.filter.ts @@ -1,6 +1,7 @@ import { ArgumentsHost, Catch, ExceptionFilter, NotFoundException } from '@nestjs/common'; import type { Request, Response } from 'express'; import path from 'node:path'; +import { readEnv } from '../../app-config'; import { PUBLIC_DIR } from './platform.routes'; /** @@ -24,7 +25,8 @@ export class SpaFallbackFilter implements ExceptionFilter { const req = ctx.getRequest(); const res = ctx.getResponse(); - if (process.env.NODE_ENV === 'production' && req.method === 'GET') { + // Case-sensitive on purpose (legacy parity). + if (readEnv().app.nodeEnv === 'production' && req.method === 'GET') { res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); res.sendFile(path.join(PUBLIC_DIR, 'index.html')); return; diff --git a/server/src/nest/plugins/dev-link.ts b/server/src/nest/plugins/dev-link.ts index 812ca8135..e4893abc7 100644 --- a/server/src/nest/plugins/dev-link.ts +++ b/server/src/nest/plugins/dev-link.ts @@ -11,11 +11,13 @@ * touches that gate — but the two properties above are why loading unsigned local * code must be gated behind an explicit flag on top of the admin + kill-switch gates. * - * Enable ONLY by setting `TREK_PLUGINS_DEV_LINK=1`. Off (absent/any other value) - * by default, so it can never be silently on in a shared or production deployment. + * Enable ONLY by setting `TREK_PLUGINS_DEV_LINK` to an explicit truthy value + * (1/true/on/yes). Off (absent/any other value) by default, so it can never be + * silently on in a shared or production deployment. */ +import { readEnv } from '../../app-config'; export function devLinkEnabled(): boolean { - return process.env.TREK_PLUGINS_DEV_LINK === '1'; + return readEnv().plugins.devLink; } /** Provenance marker stamped on a dev-linked plugin row (free-text `source_repo`). */ diff --git a/server/src/nest/plugins/host/create-rpc-host.ts b/server/src/nest/plugins/host/create-rpc-host.ts index 5b6e50301..6d15c81dc 100644 --- a/server/src/nest/plugins/host/create-rpc-host.ts +++ b/server/src/nest/plugins/host/create-rpc-host.ts @@ -1,3 +1,4 @@ +import { readEnv } from '../../../app-config'; import { db, canAccessTrip } from '../../../db/database'; import { broadcast, broadcastToUser } from '../../../websocket'; import { listBudgetItems } from '../../../services/budgetService'; @@ -279,7 +280,7 @@ export function createRealRpcHost(id: string, granted: ReadonlySet, rout // bytes to the shared demo instance, even through a plugin's db:write:files. // Only resolve the email when demo mode is actually on — keeps the hot path // (and the schema surface) untouched for self-hosted installs. - if (process.env.DEMO_MODE?.toLowerCase() === 'true') { + if (readEnv().demo.enabled) { const uploader = db.prepare('SELECT email FROM users WHERE id = ?').get(actingUserId) as { email?: string } | undefined; if (isDemoEmail(uploader?.email)) throw new ForbiddenResource('Uploads are disabled in demo mode.'); } diff --git a/server/src/nest/plugins/host/rate-limit.ts b/server/src/nest/plugins/host/rate-limit.ts index 091dc5fea..21641cf7d 100644 --- a/server/src/nest/plugins/host/rate-limit.ts +++ b/server/src/nest/plugins/host/rate-limit.ts @@ -11,6 +11,7 @@ * * Pure + dependency-free (a clock is injected) so it unit-tests without timers. */ +import { readEnv } from '../../../app-config'; /** A refilling token bucket: `capacity` tokens, refilled at `refillPerSec`. */ export class TokenBucket { @@ -51,10 +52,12 @@ export interface RpcLimitConfig { // Generous defaults: a legitimate plugin handling a request makes a handful of ctx // calls; these only bite a runaway loop. Overridable via env for tuning. +// Frozen at import on purpose (legacy timing) — the limits are boot-stable. +const pluginTuning = readEnv().plugins; export const DEFAULT_RPC_LIMIT: RpcLimitConfig = { - burst: Number(process.env.TREK_PLUGIN_RPC_BURST) || 60, - perSec: Number(process.env.TREK_PLUGIN_RPC_PER_SEC) || 20, - maxInFlight: Number(process.env.TREK_PLUGIN_RPC_INFLIGHT) || 16, + burst: pluginTuning.rpcBurst, + perSec: pluginTuning.rpcPerSec, + maxInFlight: pluginTuning.rpcInflight, }; // Plugin log volume (ctx.log.*, stdout/stderr, unknown evt topics) reaches the @@ -63,8 +66,8 @@ export const DEFAULT_RPC_LIMIT: RpcLimitConfig = { // `while (true) ctx.log.error(...)` loop can't pin the host thread; a legitimate // plugin logs far below this and never notices. Excess lines are dropped. export const DEFAULT_LOG_LIMIT = { - burst: Number(process.env.TREK_PLUGIN_LOG_BURST) || 50, - perSec: Number(process.env.TREK_PLUGIN_LOG_PER_SEC) || 10, + burst: pluginTuning.logBurst, + perSec: pluginTuning.logPerSec, }; /** Per-plugin limiter: a token bucket + an in-flight gauge. */ diff --git a/server/src/nest/plugins/install/host-compat.ts b/server/src/nest/plugins/install/host-compat.ts index bb11c8539..f9d214a9c 100644 --- a/server/src/nest/plugins/install/host-compat.ts +++ b/server/src/nest/plugins/install/host-compat.ts @@ -1,4 +1,5 @@ import semver from 'semver'; +import { readEnv } from '../../../app-config'; /** * Host-version compatibility for plugins (#plugins). A manifest declares the TREK @@ -11,7 +12,7 @@ import semver from 'semver'; /** The running TREK version (same source as the rest of the app). */ export function hostVersion(): string { - return process.env.APP_VERSION || (require('../../../../package.json') as { version: string }).version; + return readEnv().app.appVersion || (require('../../../../package.json') as { version: string }).version; } let warnedUnparseable = false; diff --git a/server/src/nest/plugins/paths.ts b/server/src/nest/plugins/paths.ts index a0ebc9e86..9fa8ac517 100644 --- a/server/src/nest/plugins/paths.ts +++ b/server/src/nest/plugins/paths.ts @@ -1,5 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; +import { readEnv } from '../../app-config'; /** * Filesystem layout for the plugin system (#plugins). Code and data are two @@ -16,10 +17,10 @@ const DATA_ROOT = path.resolve(__dirname, '../../../data'); // Read lazily so a deployment (or a test) can point these at dedicated volumes // via env without import-order surprises. export function pluginsCodeRoot(): string { - return process.env.TREK_PLUGINS_DIR || path.join(DATA_ROOT, 'plugins'); + return readEnv().plugins.dir || path.join(DATA_ROOT, 'plugins'); } export function pluginsDataRoot(): string { - return process.env.TREK_PLUGINS_DATA_DIR || path.join(DATA_ROOT, 'plugins-data'); + return readEnv().plugins.dataDir || path.join(DATA_ROOT, 'plugins-data'); } /** A plugin's installed code directory (contains trek-plugin.json + server/index.js). */ @@ -112,7 +113,7 @@ export function serverCodeRoot(): string { */ let warnedPermissionsOff = false; export function pluginPermissionArgs(pluginId: string): string[] { - if ((process.env.TREK_PLUGIN_PERMISSIONS ?? 'on').toLowerCase() === 'off') { + if (readEnv().plugins.permissionsOff) { // The OS permission jail is the boundary that stops an installed plugin from // reading trek.db / the secret files / shelling out. Turning it off makes plugin // isolation crash-only — surface that loudly once so an operator can't leave it diff --git a/server/src/nest/plugins/registry/registry.service.ts b/server/src/nest/plugins/registry/registry.service.ts index 6961e97c9..d6a3d685a 100644 --- a/server/src/nest/plugins/registry/registry.service.ts +++ b/server/src/nest/plugins/registry/registry.service.ts @@ -1,3 +1,4 @@ +import { readEnv } from '../../../app-config'; import { db } from '../../../db/database'; import { discoverPlugins } from '../install/discovery'; import { hostSatisfies, hostVersion, normalizedHost } from '../install/host-compat'; @@ -24,9 +25,8 @@ import semver from 'semver'; * (registers INACTIVE). Nothing executes on install; activation is separate. */ -const REGISTRY_URL = - process.env.TREK_PLUGIN_REGISTRY_URL || - 'https://raw.githubusercontent.com/liketrek/TREK-Plugins/main/dist/index.json'; +// Frozen at import on purpose (legacy timing) — the registry URL is boot-stable. +const REGISTRY_URL = readEnv().plugins.registryUrl; const CACHE_TTL = 30 * 60 * 1000; const MANIFEST_MAX_BYTES = 256 * 1024; // Sideload upload ceiling — matches the SDK `pack` limit (50 MB) plus zip overhead. diff --git a/server/src/nest/plugins/supervisor/plugin-supervisor.ts b/server/src/nest/plugins/supervisor/plugin-supervisor.ts index 7ed07ecfc..a77a8b570 100644 --- a/server/src/nest/plugins/supervisor/plugin-supervisor.ts +++ b/server/src/nest/plugins/supervisor/plugin-supervisor.ts @@ -1,6 +1,7 @@ import fs from 'node:fs'; import { randomUUID } from 'node:crypto'; import { fork, type ChildProcess } from 'node:child_process'; +import { readEnv } from '../../../app-config'; import { resolveChildEntry, pluginCodeDir, pluginRealCodeDir, pluginPermissionArgs, ensurePluginModuleType } from '../paths'; import type { Envelope, RpcError, RpcRequest } from '../protocol/envelope'; import type { PluginRpcHost } from '../host/rpc-host'; @@ -89,7 +90,7 @@ const DEFAULTS: Required = { // Hard RSS ceiling — the real memory cap. --max-old-space-size only bounds the // V8 heap; Buffers/ArrayBuffers/native allocations sail past it, so a plugin // could OOM the box while staying "under" the heap limit. Overridable via env. - maxRssBytes: (Number(process.env.TREK_PLUGIN_MAX_RSS_MB) || 300) * 1024 * 1024, + maxRssBytes: readEnv().plugins.maxRssMb * 1024 * 1024, }; // A plugin may only act as a provider for a hook it BOTH implements (reported by @@ -482,9 +483,10 @@ export class PluginSupervisor { // code and a plugin can never reach a self-hoster's LAN service (a Gotify, an // ntfy, an Ollama) no matter what the admin sets. Forwarded only when set, so // the default stays the secure block-private policy. - ...(process.env.TREK_PLUGIN_ALLOW_PRIVATE_EGRESS - ? { TREK_PLUGIN_ALLOW_PRIVATE_EGRESS: process.env.TREK_PLUGIN_ALLOW_PRIVATE_EGRESS } - : {}), + // Normalized to the literal 'on': the child and plugin-sdk keep their + // `=== 'on'` check (they are exempt from app-config), so the host-side + // boolean-family coercion must collapse to the one literal they accept. + ...(readEnv().plugins.allowPrivateEgress ? { TREK_PLUGIN_ALLOW_PRIVATE_EGRESS: 'on' } : {}), }, }); sup.child = child; diff --git a/server/src/nest/trips/trips.controller.ts b/server/src/nest/trips/trips.controller.ts index 6eb7b1bb0..f275ec582 100644 --- a/server/src/nest/trips/trips.controller.ts +++ b/server/src/nest/trips/trips.controller.ts @@ -21,6 +21,7 @@ import type { Request, Response } from 'express'; import { diskStorage } from 'multer'; import path from 'path'; import fs from 'fs'; +import { readEnv } from '../../app-config'; import { v4 as uuidv4 } from 'uuid'; import type { User } from '../../types'; import { TripsService } from './trips.service'; @@ -177,7 +178,7 @@ export class TripsController { @Post(':id/cover') @UseInterceptors(FileInterceptor('cover', COVER_UPLOAD)) cover(@CurrentUser() user: User, @Param('id') id: string, @UploadedFile() file: Express.Multer.File | undefined) { - if (process.env.DEMO_MODE?.toLowerCase() === 'true' && isDemoEmail(user.email)) { + if (readEnv().demo.enabled && isDemoEmail(user.email)) { throw new HttpException({ error: 'Uploads are disabled in demo mode. Self-host TREK for full functionality.' }, 403); } const access = this.trips.canAccessTrip(id, user.id);