import fs from "node:fs/promises"; import { resolveLaunchAgentPlistPath } from "./launchd.js"; import { resolveSystemdUserUnitPath } from "./systemd.js"; export type GatewayServiceCommand = { programArguments: string[]; workingDirectory?: string; environment?: Record; sourcePath?: string; } | null; export type ServiceConfigIssue = { code: string; message: string; detail?: string; level?: "recommended" | "aggressive"; }; export type ServiceConfigAudit = { ok: boolean; issues: ServiceConfigIssue[]; }; function hasGatewaySubcommand(programArguments?: string[]): boolean { return Boolean(programArguments?.some((arg) => arg === "gateway")); } function parseSystemdUnit(content: string): { after: Set; wants: Set; restartSec?: string; } { const after = new Set(); const wants = new Set(); let restartSec: string | undefined; for (const rawLine of content.split(/\r?\n/)) { const line = rawLine.trim(); if (!line) continue; if (line.startsWith("#") || line.startsWith(";")) continue; if (line.startsWith("[")) continue; const idx = line.indexOf("="); if (idx <= 0) continue; const key = line.slice(0, idx).trim(); const value = line.slice(idx + 1).trim(); if (!value) continue; if (key === "After") { for (const entry of value.split(/\s+/)) { if (entry) after.add(entry); } } else if (key === "Wants") { for (const entry of value.split(/\s+/)) { if (entry) wants.add(entry); } } else if (key === "RestartSec") { restartSec = value; } } return { after, wants, restartSec }; } function isRestartSecPreferred(value: string | undefined): boolean { if (!value) return false; const parsed = Number.parseFloat(value); if (!Number.isFinite(parsed)) return false; return Math.abs(parsed - 5) < 0.01; } async function auditSystemdUnit( env: Record, issues: ServiceConfigIssue[], ) { const unitPath = resolveSystemdUserUnitPath(env); let content = ""; try { content = await fs.readFile(unitPath, "utf8"); } catch { return; } const parsed = parseSystemdUnit(content); if (!parsed.after.has("network-online.target")) { issues.push({ code: "systemd-after-network-online", message: "Missing systemd After=network-online.target", detail: unitPath, level: "recommended", }); } if (!parsed.wants.has("network-online.target")) { issues.push({ code: "systemd-wants-network-online", message: "Missing systemd Wants=network-online.target", detail: unitPath, level: "recommended", }); } if (!isRestartSecPreferred(parsed.restartSec)) { issues.push({ code: "systemd-restart-sec", message: "RestartSec does not match the recommended 5s", detail: unitPath, level: "recommended", }); } } async function auditLaunchdPlist( env: Record, issues: ServiceConfigIssue[], ) { const plistPath = resolveLaunchAgentPlistPath(env); let content = ""; try { content = await fs.readFile(plistPath, "utf8"); } catch { return; } const hasRunAtLoad = /RunAtLoad<\/key>\s*/i.test(content); const hasKeepAlive = /KeepAlive<\/key>\s*/i.test(content); if (!hasRunAtLoad) { issues.push({ code: "launchd-run-at-load", message: "LaunchAgent is missing RunAtLoad=true", detail: plistPath, level: "recommended", }); } if (!hasKeepAlive) { issues.push({ code: "launchd-keep-alive", message: "LaunchAgent is missing KeepAlive=true", detail: plistPath, level: "recommended", }); } } function auditGatewayCommand( programArguments: string[] | undefined, issues: ServiceConfigIssue[], ) { if (!programArguments || programArguments.length === 0) return; if (!hasGatewaySubcommand(programArguments)) { issues.push({ code: "gateway-command-missing", message: "Service command does not include the gateway subcommand", level: "aggressive", }); } } export async function auditGatewayServiceConfig(params: { env: Record; command: GatewayServiceCommand; platform?: NodeJS.Platform; }): Promise { const issues: ServiceConfigIssue[] = []; const platform = params.platform ?? process.platform; auditGatewayCommand(params.command?.programArguments, issues); if (platform === "linux") { await auditSystemdUnit(params.env, issues); } else if (platform === "darwin") { await auditLaunchdPlist(params.env, issues); } return { ok: issues.length === 0, issues }; }