import { ApiError } from '../../lib/errors.js'; import type { GameServerProvider } from '../pterodactyl/types.js'; const CONFIG_MAX_BYTES = 256 * 1024; export function asRecord(value: unknown): Record | null { return typeof value === 'object' && value !== null && !Array.isArray(value) ? (value as Record) : null; } /** * Shared read-modify-write access to the server's config.json: size-guarded * download + parse, and a write path that backs the previous content up to * `.bak` and verifies the upload by downloading it again. Callers * mutate only their own keys on the parsed document so everything else in the * file passes through untouched. */ export class ConfigFileGateway { constructor( private readonly provider: GameServerProvider, readonly configPath: string, ) {} async download( providerServerId: string, ): Promise<{ raw: string; root: Record }> { const file = await this.provider.downloadTextFile( providerServerId, this.configPath, CONFIG_MAX_BYTES, ); if (file.truncated) { throw ApiError.upstream('Server config.json is unexpectedly large; refusing to modify it.'); } let parsed: unknown; try { parsed = JSON.parse(file.content.replace(/^\uFEFF/, '')); } catch { throw ApiError.upstream('Server config.json is not valid JSON.'); } const root = asRecord(parsed); if (!root || !asRecord(root.game)) { throw ApiError.upstream('Server config.json has no "game" section; refusing to modify it.'); } return { raw: file.content, root }; } /** * Backs up `previousRaw`, writes the mutated document, downloads it again * and hands the verified parsed result to `verify` (throw there to fail). */ async write( providerServerId: string, root: Record, previousRaw: string, verify: (readBack: Record) => void, ): Promise> { await this.provider.writeTextFile(providerServerId, `${this.configPath}.bak`, previousRaw); const serialized = `${JSON.stringify(root, null, 4)}\n`; await this.provider.writeTextFile(providerServerId, this.configPath, serialized); const readBack = await this.download(providerServerId); verify(readBack.root); return readBack.root; } }