@studiometa/trafic-agent 0.1.47 → 0.1.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +0,0 @@
1
- {"version":3,"file":"server-EPU5dtPx.js","names":[],"sources":["../src/utils/config.ts","../src/utils/auth.ts","../src/utils/ddev.ts","../src/setup/steps.ts","../src/setup/io.ts","../src/utils/db.ts","../src/utils/project-config.ts","../src/setup/ddev.ts","../src/utils/traefik.ts","../src/utils/tls.ts","../src/server.ts"],"sourcesContent":["import { readFileSync, existsSync } from \"node:fs\";\nimport { parse } from \"smol-toml\";\nimport type { AgentConfig, AuthConfig, AuthRule, TlsConfig } from \"../types.js\";\n\nconst DEFAULT_CONFIG_PATHS = [\n \"/etc/trafic/config.toml\",\n \"./config.toml\",\n \"./trafic.toml\",\n];\n\nconst DEFAULT_CONFIG: AgentConfig = {\n tld: \"\",\n port: 9876,\n dbPath: \"/var/lib/trafic/db.sqlite\",\n projectListPath: \"/home/ddev/.ddev/project_list.yaml\",\n projectsDir: \"/home/ddev/www\",\n idleTimeout: \"30m\",\n idleCheckInterval: \"5m\",\n auth: {\n defaultPolicy: \"basic\",\n allowedIps: [],\n tokens: [],\n basicAuth: [],\n rules: [],\n trustedProxyHops: 1,\n },\n tls: {\n dnsEnv: {},\n },\n};\n\n/**\n * Parse a duration string (e.g., \"30m\", \"1h\", \"2h30m\") to milliseconds\n */\nexport function parseDuration(duration: string): number {\n const regex = /(?:(\\d+)h)?(?:(\\d+)m)?(?:(\\d+)s)?/;\n const match = duration.match(regex);\n if (!match) return 0;\n\n const hours = parseInt(match[1] ?? \"0\", 10);\n const minutes = parseInt(match[2] ?? \"0\", 10);\n const seconds = parseInt(match[3] ?? \"0\", 10);\n\n return (hours * 3600 + minutes * 60 + seconds) * 1000;\n}\n\n/**\n * Load configuration from TOML file\n */\nexport function loadConfig(configPath?: string): AgentConfig {\n // Find config file\n let path = configPath;\n if (!path) {\n path = DEFAULT_CONFIG_PATHS.find((p) => existsSync(p));\n }\n\n if (!path || !existsSync(path)) {\n console.warn(\"No config file found, using defaults\");\n return DEFAULT_CONFIG;\n }\n\n const content = readFileSync(path, \"utf-8\");\n const parsed = parse(content) as Record<string, unknown>;\n\n // Merge with defaults\n const auth = mergeAuthConfig(parsed.auth as Record<string, unknown>);\n const tls = mergeTlsConfig(parsed.tls as Record<string, unknown>);\n\n return {\n tld: (parsed.tld as string) ?? DEFAULT_CONFIG.tld,\n port: (parsed.port as number) ?? DEFAULT_CONFIG.port,\n dbPath: (parsed.db_path as string) ?? DEFAULT_CONFIG.dbPath,\n projectListPath:\n (parsed.project_list_path as string) ?? DEFAULT_CONFIG.projectListPath,\n projectsDir: (parsed.projects_dir as string) ?? DEFAULT_CONFIG.projectsDir,\n idleTimeout:\n (parsed.idle_timeout as string) ?? DEFAULT_CONFIG.idleTimeout,\n idleCheckInterval:\n (parsed.idle_check_interval as string) ??\n DEFAULT_CONFIG.idleCheckInterval,\n auth,\n tls,\n };\n}\n\n/**\n * Read the [tls] section.\n *\n * `dns_env` holds provider credentials, so its values are taken as-is and\n * only checked for being strings — validateConfig reports the rest.\n */\nfunction mergeTlsConfig(raw?: Record<string, unknown>): TlsConfig {\n if (!raw) return DEFAULT_CONFIG.tls;\n\n const env = (raw.dns_env as Record<string, unknown> | undefined) ?? {};\n const dnsEnv: Record<string, string> = {};\n\n for (const [key, value] of Object.entries(env)) {\n // Kept as read: a number or a boolean here is a config mistake, and\n // validateConfig says so rather than coercing it into a credential\n dnsEnv[key] = value as string;\n }\n\n return {\n dnsProvider: (raw.dns_provider as string | undefined) || undefined,\n caServer: (raw.ca_server as string | undefined) || undefined,\n dnsEnv,\n };\n}\n\n/**\n * Parse basic_auth entries from TOML.\n * Accepts either plain strings (\"user:pass\") or objects ({username, password}).\n */\nfunction parseBasicAuthEntries(raw: unknown): string[] | null {\n if (!Array.isArray(raw)) return null;\n return raw.map((entry) => {\n if (typeof entry === \"string\") return entry;\n if (typeof entry === \"object\" && entry !== null) {\n const { username, password } = entry as Record<string, unknown>;\n return `${username}:${password}`;\n }\n return String(entry);\n });\n}\n\nfunction mergeAuthConfig(raw?: Record<string, unknown>): AuthConfig {\n if (!raw) return DEFAULT_CONFIG.auth;\n\n const rules = (raw.rules as Array<Record<string, unknown>>) ?? [];\n\n return {\n defaultPolicy:\n (raw.default_policy as AuthConfig[\"defaultPolicy\"]) ??\n DEFAULT_CONFIG.auth.defaultPolicy,\n allowedIps:\n (raw.allowed_ips as string[]) ?? DEFAULT_CONFIG.auth.allowedIps,\n tokens: (raw.tokens as string[]) ?? DEFAULT_CONFIG.auth.tokens,\n basicAuth: parseBasicAuthEntries(raw.basic_auth) ?? DEFAULT_CONFIG.auth.basicAuth,\n rules: rules.map(\n (r): AuthRule => ({\n match: r.match as string,\n policy: r.policy as AuthRule[\"policy\"],\n tokens: r.tokens as string[] | undefined,\n allowedIps: r.allowed_ips as string[] | undefined,\n }),\n ),\n trustedProxyHops: parseTrustedProxyHops(raw.trusted_proxy_hops),\n };\n}\n\n/**\n * Parse trusted_proxy_hops, ignoring values that cannot be a hop count.\n *\n * A bad value falls back to the default rather than 0, because 0 would read\n * the socket peer — always the proxy — and quietly stop the IP allowlist and\n * per-hostname IP rules from ever matching.\n */\nfunction parseTrustedProxyHops(raw: unknown): number {\n const hops = typeof raw === \"number\" ? raw : Number(raw);\n\n if (!Number.isInteger(hops) || hops < 1) {\n return DEFAULT_CONFIG.auth.trustedProxyHops;\n }\n\n return hops;\n}\n\n/**\n * Validate configuration\n */\nexport function validateConfig(config: AgentConfig): string[] {\n const errors: string[] = [];\n\n if (!config.tld) {\n errors.push(\"tld is required\");\n }\n\n if (config.port < 1 || config.port > 65535) {\n errors.push(\"port must be between 1 and 65535\");\n }\n\n if (\n config.auth.defaultPolicy !== \"allow\" &&\n config.auth.defaultPolicy !== \"deny\" &&\n config.auth.defaultPolicy !== \"basic\" &&\n config.auth.defaultPolicy !== \"token\"\n ) {\n errors.push('auth.default_policy must be \"allow\", \"deny\", \"basic\", or \"token\"');\n }\n\n for (const [key, value] of Object.entries(config.tls.dnsEnv)) {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {\n errors.push(`tls.dns_env key \"${key}\" is not a valid environment variable name`);\n }\n\n if (typeof value !== \"string\") {\n errors.push(`tls.dns_env.${key} must be a string`);\n }\n }\n\n return errors;\n}\n","import type { AuthConfig, AuthRequest, AuthResult, AuthRule } from \"../types.js\";\n\n/**\n * Check if an IP matches a pattern (supports CIDR notation)\n */\nexport function matchIp(ip: string, pattern: string): boolean {\n // Exact match\n if (ip === pattern) return true;\n\n // CIDR notation\n if (pattern.includes(\"/\")) {\n return matchCidr(ip, pattern);\n }\n\n // Wildcard (e.g., 192.168.*)\n if (pattern.includes(\"*\")) {\n const regex = new RegExp(\n \"^\" + pattern.replace(/\\./g, \"\\\\.\").replace(/\\*/g, \".*\") + \"$\",\n );\n return regex.test(ip);\n }\n\n return false;\n}\n\n/**\n * Match IP against CIDR range\n */\nfunction matchCidr(ip: string, cidr: string): boolean {\n const [range, bits] = cidr.split(\"/\");\n const mask = parseInt(bits, 10);\n\n const ipNum = ipToNumber(ip);\n const rangeNum = ipToNumber(range);\n\n if (ipNum === null || rangeNum === null) return false;\n\n const maskNum = ~((1 << (32 - mask)) - 1);\n return (ipNum & maskNum) === (rangeNum & maskNum);\n}\n\n/**\n * Convert IP string to number\n */\nfunction ipToNumber(ip: string): number | null {\n const parts = ip.split(\".\");\n if (parts.length !== 4) return null;\n\n let num = 0;\n for (const part of parts) {\n const n = parseInt(part, 10);\n if (isNaN(n) || n < 0 || n > 255) return null;\n num = (num << 8) | n;\n }\n return num >>> 0; // Convert to unsigned\n}\n\n/**\n * Match hostname against a glob pattern\n */\nexport function matchHostname(hostname: string, pattern: string): boolean {\n // Exact match\n if (hostname === pattern) return true;\n\n // Glob pattern\n const regex = new RegExp(\n \"^\" +\n pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\*/g, \".*\")\n .replace(/\\?/g, \".\") +\n \"$\",\n );\n return regex.test(hostname);\n}\n\n/**\n * Parse Basic auth header\n */\nexport function parseBasicAuth(\n header: string,\n): { username: string; password: string } | null {\n if (!header.startsWith(\"Basic \")) return null;\n\n try {\n const encoded = header.slice(6);\n const decoded = Buffer.from(encoded, \"base64\").toString(\"utf-8\");\n const colonIndex = decoded.indexOf(\":\");\n if (colonIndex === -1) return null;\n\n return {\n username: decoded.slice(0, colonIndex),\n password: decoded.slice(colonIndex + 1),\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Parse Bearer token from header\n */\nexport function parseBearerToken(header: string): string | null {\n if (!header.startsWith(\"Bearer \")) return null;\n return header.slice(7);\n}\n\n/**\n * Resolve the client address from the socket peer and X-Forwarded-For.\n *\n * Every proxy appends the address it saw, so the entries grow left to right\n * and the rightmost ones were added by the proxies closest to us. Counting\n * `trustedHops` from the right therefore lands on the client, while anything\n * further left is whatever the client sent and must be ignored.\n *\n * Reading the leftmost entry instead — as this used to — let a client set\n * `X-Forwarded-For: <allowed-ip>` and match the IP allowlist, which grants\n * access unconditionally.\n *\n * Falls back to the socket peer whenever the header cannot be trusted to\n * hold that many entries.\n */\nexport function resolveClientIp(\n socketIp: string,\n forwardedFor: string | undefined,\n trustedHops = 1,\n): string {\n if (!forwardedFor || trustedHops < 1) {\n return socketIp;\n }\n\n const entries = forwardedFor\n .split(\",\")\n .map((entry) => entry.trim())\n .filter(Boolean);\n\n // Fewer entries than trusted proxies means the chain is shorter than\n // configured — the socket peer is the only address we can rely on.\n if (entries.length < trustedHops) {\n return socketIp;\n }\n\n return entries[entries.length - trustedHops] ?? socketIp;\n}\n\n/**\n * Check authentication for a request\n */\nexport function checkAuth(request: AuthRequest, config: AuthConfig): AuthResult {\n const { hostname, ip, authorization } = request;\n\n const realIp = resolveClientIp(ip, request.forwardedFor, config.trustedProxyHops);\n\n // Check IP whitelist first (always allows)\n for (const pattern of config.allowedIps) {\n if (matchIp(realIp, pattern)) {\n return { allowed: true, reason: \"ip\" };\n }\n }\n\n // Check per-hostname rules\n for (const rule of config.rules) {\n if (matchHostname(hostname, rule.match)) {\n return checkRule(rule, realIp, authorization, config);\n }\n }\n\n // Apply default policy\n return applyDefaultPolicy(config, realIp, authorization);\n}\n\n/**\n * Check a specific rule\n */\nfunction checkRule(\n rule: AuthRule,\n ip: string,\n authorization: string | undefined,\n config: AuthConfig,\n): AuthResult {\n // Rule-specific IP whitelist\n if (rule.allowedIps) {\n for (const pattern of rule.allowedIps) {\n if (matchIp(ip, pattern)) {\n return { allowed: true, reason: \"rule\" };\n }\n }\n }\n\n switch (rule.policy) {\n case \"allow\":\n return { allowed: true, reason: \"rule\" };\n\n case \"deny\":\n return { allowed: false, reason: \"rule\" };\n\n case \"token\": {\n if (!authorization) return { allowed: false, reason: \"rule\" };\n const token = parseBearerToken(authorization);\n const tokens = rule.tokens ?? config.tokens;\n if (token && tokens.includes(token)) {\n return { allowed: true, reason: \"token\" };\n }\n return { allowed: false, reason: \"rule\" };\n }\n\n case \"basic\": {\n if (!authorization) return { allowed: false, reason: \"rule\" };\n const creds = parseBasicAuth(authorization);\n if (creds) {\n const credString = `${creds.username}:${creds.password}`;\n if (config.basicAuth.includes(credString)) {\n return { allowed: true, reason: \"basic\" };\n }\n }\n return { allowed: false, reason: \"rule\" };\n }\n\n default:\n return { allowed: false, reason: \"rule\" };\n }\n}\n\n/**\n * Apply default policy\n */\nfunction applyDefaultPolicy(\n config: AuthConfig,\n ip: string,\n authorization: string | undefined,\n): AuthResult {\n switch (config.defaultPolicy) {\n case \"allow\":\n return { allowed: true, reason: \"default\" };\n\n case \"deny\":\n return { allowed: false, reason: \"default\" };\n\n case \"basic\": {\n if (!authorization) return { allowed: false, reason: \"default\" };\n const creds = parseBasicAuth(authorization);\n if (creds) {\n const credString = `${creds.username}:${creds.password}`;\n if (config.basicAuth.includes(credString)) {\n return { allowed: true, reason: \"basic\" };\n }\n }\n // Also check tokens as fallback\n const token = parseBearerToken(authorization);\n if (token && config.tokens.includes(token)) {\n return { allowed: true, reason: \"token\" };\n }\n return { allowed: false, reason: \"default\" };\n }\n\n case \"token\": {\n if (!authorization) return { allowed: false, reason: \"default\" };\n const token = parseBearerToken(authorization);\n if (token && config.tokens.includes(token)) {\n return { allowed: true, reason: \"token\" };\n }\n return { allowed: false, reason: \"default\" };\n }\n\n default:\n return { allowed: false, reason: \"default\" };\n }\n}\n","import { readFileSync, existsSync, watch } from \"node:fs\";\nimport { execSync, execFile } from \"node:child_process\";\nimport type { DdevProject } from \"../types.js\";\n\n/** Strip a single pair of surrounding quotes. */\nfunction unquote(value: string): string {\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n return value.slice(1, -1);\n }\n\n return value;\n}\n\n/** A project name at the start of a line, e.g. `my-project:` */\nconst PROJECT_LINE = /^(\\S[^:]*):\\s*$/;\n\n/** An indented `approot: /path` belonging to the project above it */\nconst APPROOT_LINE = /^\\s+approot:\\s*(.+?)\\s*$/;\n\n/**\n * Load projects from DDEV's project_list.yaml\n * Returns a map of project name -> app root path\n *\n * The file nests the path under the project name:\n *\n * scalian:\n * approot: /home/ddev/www/scalian\n *\n * Indentation therefore has to be read. A flat key/value pass produces one\n * entry per line instead: the project mapped to an empty string, plus a\n * phantom project called `approot`. Both did damage — the empty path meant\n * `loadProjectConfig` looked for `.ddev/config.trafic.yaml` relative to the\n * working directory and never found it, so per-project settings were silently\n * ignored, and the phantom entered the hostname index, where the TLS ask\n * endpoint would vouch for `approot.<tld>` and have a certificate issued for\n * a project that does not exist.\n */\nexport function loadProjectList(\n projectListPath: string,\n): Map<string, string> {\n if (!existsSync(projectListPath)) {\n return new Map();\n }\n\n const projects = new Map<string, string>();\n let current: string | undefined;\n\n for (const line of readFileSync(projectListPath, \"utf-8\").split(\"\\n\")) {\n if (!line.trim() || line.trimStart().startsWith(\"#\")) {\n continue;\n }\n\n const project = PROJECT_LINE.exec(line);\n\n if (project) {\n current = unquote(project[1]!.trim());\n // Recorded now so a project keeps its hostname even if DDEV writes no\n // approot for it\n projects.set(current, \"\");\n continue;\n }\n\n const approot = APPROOT_LINE.exec(line);\n\n if (approot && current) {\n projects.set(current, unquote(approot[1]!));\n }\n }\n\n return projects;\n}\n\n/**\n * Build a hostname -> project name index\n */\nexport function buildHostnameIndex(\n projects: Map<string, string>,\n tld: string,\n): Map<string, string> {\n const index = new Map<string, string>();\n\n for (const [name] of projects) {\n // Primary hostname: project-name.tld\n index.set(`${name}.${tld}`, name);\n }\n\n return index;\n}\n\n/**\n * Get project name from hostname\n */\nexport function getProjectFromHostname(\n hostname: string,\n index: Map<string, string>,\n): string | undefined {\n return index.get(hostname);\n}\n\n/**\n * Watch project_list.yaml for changes\n */\nexport function watchProjectList(\n projectListPath: string,\n onChange: () => void,\n): void {\n if (!existsSync(projectListPath)) {\n console.warn(`Project list not found: ${projectListPath}`);\n return;\n }\n\n watch(projectListPath, (eventType) => {\n if (eventType === \"change\") {\n onChange();\n }\n });\n}\n\n/**\n * Run a ddev command without blocking the event loop.\n *\n * The agent is single-threaded and serves forward auth for every project, so\n * an `execSync` here stops the whole server for the duration of the command.\n * `ddev start` takes over a minute, which froze auth for every unrelated\n * project and produced request timeouts on a live server. Nothing about\n * \"background\" work with setTimeout helps: the timer callback still runs on\n * the same thread.\n */\nexport interface DdevResult {\n ok: boolean;\n stdout: string;\n}\n\n/**\n * Runs a ddev command. Injected so tests drive the outcome directly instead\n * of replacing node:child_process for the whole module.\n */\nexport type DdevRunner = (\n args: string[],\n timeoutMs: number,\n) => Promise<DdevResult>;\n\n/** The real runner, used unless a caller passes its own. */\nexport const nodeDdevRunner: DdevRunner = (args, timeoutMs) =>\n new Promise((resolve) => {\n execFile(\n \"ddev\",\n args,\n { encoding: \"utf-8\", timeout: timeoutMs },\n (error, stdout) => {\n if (error) {\n console.error(`ddev ${args.join(\" \")} failed:`, error.message);\n resolve({ ok: false, stdout: stdout ?? \"\" });\n return;\n }\n\n resolve({ ok: true, stdout: stdout ?? \"\" });\n },\n );\n });\n\n/**\n * Get detailed project info using ddev describe\n */\nexport async function getProjectInfo(\n name: string,\n run: DdevRunner = nodeDdevRunner,\n): Promise<DdevProject | undefined> {\n const { ok, stdout } = await run([\"describe\", name, \"-j\"], 10000);\n\n if (!ok) {\n return undefined;\n }\n\n try {\n const data = JSON.parse(stdout);\n\n if (!data.raw) return undefined;\n\n const raw = data.raw;\n return {\n name: raw.name,\n status: raw.status,\n appRoot: raw.approot,\n httpURLs: raw.httpURLs ?? [],\n httpsURLs: raw.httpsURLs ?? [],\n type: raw.type,\n phpVersion: raw.php_version,\n dbType: raw.dbinfo?.dbType,\n };\n } catch {\n return undefined;\n }\n}\n\n/**\n * Start a DDEV project.\n *\n * Resolves when the project is up. Callers in the request path must not await\n * it — respond first, then let this settle and record the outcome.\n */\nexport async function startProject(\n name: string,\n run: DdevRunner = nodeDdevRunner,\n): Promise<boolean> {\n const { ok } = await run([\"start\", name], 120000);\n return ok;\n}\n\n/**\n * Stop a DDEV project\n */\nexport async function stopProject(\n name: string,\n run: DdevRunner = nodeDdevRunner,\n): Promise<boolean> {\n const { ok } = await run([\"stop\", name], 60000);\n return ok;\n}\n\n/**\n * List all DDEV projects with their status\n */\nexport function listProjects(): DdevProject[] {\n try {\n const output = execSync(\"ddev list -j\", {\n encoding: \"utf-8\",\n timeout: 30000,\n });\n const data = JSON.parse(output);\n\n if (!Array.isArray(data)) return [];\n\n return data.map((p: Record<string, unknown>) => ({\n name: p.name as string,\n status: p.status as DdevProject[\"status\"],\n appRoot: p.approot as string,\n httpURLs: (p.httpURLs as string[]) ?? [],\n httpsURLs: (p.httpsURLs as string[]) ?? [],\n type: p.type as string,\n }));\n } catch {\n return [];\n }\n}\n","import { execSync } from \"node:child_process\";\nimport { writeFileSync as fsWriteFile } from \"node:fs\";\n\nlet stepNumber = 0;\nlet dryRun = false;\n\n/**\n * Set dry-run mode\n */\nexport function setDryRun(value: boolean): void {\n dryRun = value;\n}\n\n/**\n * Reset step counter\n */\nexport function resetSteps(): void {\n stepNumber = 0;\n}\n\n/**\n * Log a step\n */\nexport function step(message: string): void {\n stepNumber++;\n console.log(`\\n\\x1b[36m[${stepNumber}]\\x1b[0m ${message}`);\n}\n\n/**\n * Log success\n */\nexport function success(message: string): void {\n console.log(` \\x1b[32m✓\\x1b[0m ${message}`);\n}\n\n/**\n * Log warning\n */\nexport function warn(message: string): void {\n console.log(` \\x1b[33m⚠\\x1b[0m ${message}`);\n}\n\n/**\n * Log error\n */\nexport function error(message: string): void {\n console.log(` \\x1b[31m✗\\x1b[0m ${message}`);\n}\n\n/**\n * Log info\n */\nexport function info(message: string): void {\n console.log(` ${message}`);\n}\n\n/**\n * Execute a command (or show in dry-run mode)\n */\nexport function exec(command: string, options?: { silent?: boolean }): string {\n if (dryRun) {\n if (!options?.silent) {\n console.log(` \\x1b[90m$ ${command}\\x1b[0m`);\n }\n return \"\";\n }\n\n try {\n return execSync(command, {\n encoding: \"utf-8\",\n stdio: options?.silent ? \"pipe\" : \"inherit\",\n });\n } catch (err) {\n const e = err as { status?: number; stderr?: string };\n throw new Error(`Command failed: ${command}\\n${e.stderr ?? \"\"}`);\n }\n}\n\n/**\n * Execute a command and return output (silent)\n */\nexport function execSilent(command: string): string {\n if (dryRun) {\n return \"\";\n }\n\n try {\n return execSync(command, {\n encoding: \"utf-8\",\n stdio: \"pipe\",\n }).trim();\n } catch {\n return \"\";\n }\n}\n\n/**\n * Check if a command exists\n */\nexport function commandExists(command: string): boolean {\n try {\n execSync(`which ${command}`, { stdio: \"pipe\" });\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Check if running as root\n */\nexport function isRoot(): boolean {\n return process.getuid?.() === 0;\n}\n\n/**\n * Write a file (or show in dry-run mode)\n */\nexport function writeFile(path: string, content: string): void {\n if (dryRun) {\n console.log(` \\x1b[90mWrite ${path}:\\x1b[0m`);\n const lines = content.split(\"\\n\").slice(0, 5);\n for (const line of lines) {\n console.log(` \\x1b[90m ${line}\\x1b[0m`);\n }\n if (content.split(\"\\n\").length > 5) {\n console.log(` \\x1b[90m ...\\x1b[0m`);\n }\n return;\n }\n\n fsWriteFile(path, content);\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { commandExists, exec, execSilent, writeFile } from \"./steps.js\";\n\n/**\n * The effectful operations the setup steps need.\n *\n * Injected into every setup function so tests can drive them with plain\n * fakes — no module mocking — and so dry-run has one place to intercept\n * writes instead of each module reaching for `node:fs` itself.\n */\nexport interface SetupIo {\n /** Run a shell command. Throws when it exits non-zero. */\n exec(command: string, options?: { silent?: boolean }): string;\n /**\n * Run a probe. Returns trimmed stdout, or \"\" when the command fails.\n *\n * Use this wherever a non-zero exit is an answer rather than an error —\n * `id -u ddev` on a server without that user, for instance.\n */\n execSilent(command: string): string;\n /** Whether a command is on the PATH. */\n commandExists(command: string): boolean;\n /** Write a file. Honours dry-run. */\n writeFile(path: string, content: string): void;\n /** Whether a path exists. */\n fileExists(path: string): boolean;\n /** Read a UTF-8 file. */\n readFile(path: string): string;\n /** Process environment, for values like SUDO_USER. */\n env: Record<string, string | undefined>;\n}\n\n/**\n * The real implementation, used when no io is injected.\n *\n * `writeFile` comes from steps.js rather than `node:fs` so writes are\n * skipped in dry-run mode. Reads stay direct: they change nothing.\n */\nexport const nodeIo: SetupIo = {\n exec,\n execSilent,\n commandExists,\n writeFile,\n fileExists: existsSync,\n readFile: (path) => readFileSync(path, \"utf-8\"),\n env: process.env,\n};\n","import { DatabaseSync } from \"node:sqlite\";\nimport type { ProjectRecord, AccessLog } from \"../types.js\";\n\n/**\n * Reads the current time in epoch milliseconds.\n *\n * Injected so a test can place a project's last access at a chosen moment\n * without freezing the clock for everything else in the process.\n */\nexport type Clock = () => number;\n\n/** The real clock, used unless a caller passes its own. */\nexport const systemClock: Clock = () => Date.now();\n\nlet db: DatabaseSync | null = null;\n\n/**\n * Initialize the database connection and schema\n */\nexport function initDb(dbPath: string): DatabaseSync {\n db = new DatabaseSync(dbPath);\n db.exec(\"PRAGMA journal_mode=WAL\");\n\n // Create tables\n db.exec(`\n CREATE TABLE IF NOT EXISTS projects (\n name TEXT PRIMARY KEY,\n last_access INTEGER NOT NULL,\n status TEXT NOT NULL DEFAULT 'stopped'\n );\n\n CREATE TABLE IF NOT EXISTS access_logs (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project TEXT NOT NULL,\n timestamp INTEGER NOT NULL,\n ip TEXT NOT NULL,\n user_agent TEXT NOT NULL,\n path TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_access_logs_project ON access_logs(project);\n CREATE INDEX IF NOT EXISTS idx_access_logs_timestamp ON access_logs(timestamp);\n `);\n\n return db;\n}\n\n/**\n * Get the database instance\n */\nexport function getDb(): DatabaseSync {\n if (!db) {\n throw new Error(\"Database not initialized. Call initDb first.\");\n }\n return db;\n}\n\n/**\n * Close the database connection\n */\nexport function closeDb(): void {\n if (db) {\n db.close();\n db = null;\n }\n}\n\n/**\n * Update project last access time\n */\nexport function updateProjectAccess(name: string, now: Clock = systemClock): void {\n const database = getDb();\n const stmt = database.prepare(`\n INSERT INTO projects (name, last_access, status)\n VALUES (?, ?, 'running')\n ON CONFLICT(name) DO UPDATE SET last_access = excluded.last_access\n `);\n stmt.run(name, now());\n}\n\n/**\n * Get project record\n */\nexport function getProject(name: string): ProjectRecord | undefined {\n const database = getDb();\n const stmt = database.prepare(\n \"SELECT name, last_access as lastAccess, status FROM projects WHERE name = ?\",\n );\n return stmt.get(name) as unknown as ProjectRecord | undefined;\n}\n\n/**\n * Set project status\n */\nexport function setProjectStatus(\n name: string,\n status: ProjectRecord[\"status\"],\n now: Clock = systemClock,\n): void {\n const database = getDb();\n const stmt = database.prepare(`\n INSERT INTO projects (name, last_access, status)\n VALUES (?, ?, ?)\n ON CONFLICT(name) DO UPDATE SET status = excluded.status\n `);\n stmt.run(name, now(), status);\n}\n\n/**\n * Get idle projects (last access older than threshold)\n */\nexport function getIdleProjects(\n thresholdMs: number,\n now: Clock = systemClock,\n): ProjectRecord[] {\n const database = getDb();\n const cutoff = now() - thresholdMs;\n const stmt = database.prepare(`\n SELECT name, last_access as lastAccess, status\n FROM projects\n WHERE last_access < ? AND status = 'running'\n `);\n return stmt.all(cutoff) as unknown as ProjectRecord[];\n}\n\n/**\n * Log an access\n */\nexport function logAccess(log: Omit<AccessLog, \"id\">): void {\n const database = getDb();\n const stmt = database.prepare(`\n INSERT INTO access_logs (project, timestamp, ip, user_agent, path)\n VALUES (?, ?, ?, ?, ?)\n `);\n stmt.run(log.project, log.timestamp, log.ip, log.userAgent, log.path);\n}\n\n/**\n * Get recent access logs for a project\n */\nexport function getAccessLogs(project: string, limit = 100): AccessLog[] {\n const database = getDb();\n const stmt = database.prepare(`\n SELECT id, project, timestamp, ip, user_agent as userAgent, path\n FROM access_logs\n WHERE project = ?\n ORDER BY timestamp DESC\n LIMIT ?\n `);\n return stmt.all(project, limit) as unknown as AccessLog[];\n}\n\n/**\n * Clean old access logs (older than given days)\n */\nexport function cleanOldLogs(days: number, now: Clock = systemClock): number {\n const database = getDb();\n const cutoff = now() - days * 24 * 60 * 60 * 1000;\n const stmt = database.prepare(\"DELETE FROM access_logs WHERE timestamp < ?\");\n const result = stmt.run(cutoff);\n return Number(result.changes);\n}\n","import { readFileSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/**\n * Per-project Trafic configuration\n * Read from .ddev/config.trafic.yaml\n */\nexport interface ProjectConfig {\n /** Auth policy override: allow | deny | basic | token */\n auth_policy?: \"allow\" | \"deny\" | \"basic\" | \"token\";\n /** Idle timeout override: \"never\" or duration like \"30m\", \"4h\" */\n idle_timeout?: string;\n}\n\n/**\n * Simple YAML parser for config.trafic.yaml\n * Only supports simple key: value pairs (no nesting, no arrays)\n */\nfunction parseSimpleYaml(content: string): Record<string, string> {\n const result: Record<string, string> = {};\n const lines = content.split(\"\\n\");\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Skip empty lines and comments\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n\n const colonIndex = trimmed.indexOf(\":\");\n if (colonIndex === -1) continue;\n\n const key = trimmed.slice(0, colonIndex).trim();\n let value = trimmed.slice(colonIndex + 1).trim();\n\n // Remove quotes if present\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n value = value.slice(1, -1);\n }\n\n result[key] = value;\n }\n\n return result;\n}\n\n/**\n * Load project-specific Trafic config from .ddev/config.trafic.yaml\n */\nexport function loadProjectConfig(projectDir: string): ProjectConfig {\n const configPath = join(projectDir, \".ddev\", \"config.trafic.yaml\");\n\n if (!existsSync(configPath)) {\n return {};\n }\n\n try {\n const content = readFileSync(configPath, \"utf-8\");\n const parsed = parseSimpleYaml(content);\n\n const config: ProjectConfig = {};\n\n // Validate auth_policy\n if (parsed.auth_policy) {\n const policy = parsed.auth_policy.toLowerCase();\n if ([\"allow\", \"deny\", \"basic\", \"token\"].includes(policy)) {\n config.auth_policy = policy as ProjectConfig[\"auth_policy\"];\n }\n }\n\n // Validate idle_timeout\n if (parsed.idle_timeout) {\n config.idle_timeout = parsed.idle_timeout;\n }\n\n return config;\n } catch (error) {\n console.warn(`Failed to load project config from ${configPath}:`, error);\n return {};\n }\n}\n\n/**\n * Check if a project should never be stopped (idle_timeout = \"never\")\n */\nexport function shouldNeverStop(config: ProjectConfig): boolean {\n return config.idle_timeout?.toLowerCase() === \"never\";\n}\n\n/**\n * Get idle timeout in milliseconds, or undefined to use global default\n */\nexport function getIdleTimeoutMs(config: ProjectConfig): number | undefined {\n if (!config.idle_timeout) return undefined;\n if (shouldNeverStop(config)) return undefined;\n\n // Parse duration string (e.g., \"30m\", \"4h\", \"2h30m\")\n const regex = /(?:(\\d+)h)?(?:(\\d+)m)?(?:(\\d+)s)?/;\n const match = config.idle_timeout.match(regex);\n if (!match) return undefined;\n\n const hours = parseInt(match[1] ?? \"0\", 10);\n const minutes = parseInt(match[2] ?? \"0\", 10);\n const seconds = parseInt(match[3] ?? \"0\", 10);\n\n const ms = (hours * 3600 + minutes * 60 + seconds) * 1000;\n return ms > 0 ? ms : undefined;\n}\n","import { nodeIo, type SetupIo } from \"./io.js\";\nimport { step, success, info } from \"./steps.js\";\nimport { loadConfig } from \"../utils/config.js\";\nimport type { TlsConfig } from \"../types.js\";\n\n/**\n * Install system dependencies required by Trafic\n */\nexport function installSystemDeps(io: SetupIo = nodeIo): void {\n step(\"Install system dependencies\");\n\n io.exec(\"apt-get update -qq\", { silent: true });\n // gnupg and ca-certificates are needed to add the DDEV and NodeSource\n // apt repositories — a minimal Ubuntu image ships with neither\n io.exec(\n \"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y jq curl rsync gnupg ca-certificates\",\n { silent: true },\n );\n\n success(\"System dependencies installed: jq, curl, rsync, gnupg, ca-certificates\");\n}\n\n/**\n * Add the official DDEV apt repository and GPG key.\n * Idempotent: safe to call even if the repository is already configured.\n */\nexport function addDdevAptRepo(io: SetupIo = nodeIo): void {\n io.exec(\"install -m 0755 -d /etc/apt/keyrings\", { silent: true });\n io.exec(\n \"curl -fsSL https://pkg.ddev.com/apt/gpg.key | gpg --dearmor | tee /etc/apt/keyrings/ddev.gpg > /dev/null\",\n { silent: true },\n );\n io.exec(\"chmod a+r /etc/apt/keyrings/ddev.gpg\", { silent: true });\n io.exec(\n \"echo \\\"deb [signed-by=/etc/apt/keyrings/ddev.gpg] https://pkg.ddev.com/apt/ * *\\\" | tee /etc/apt/sources.list.d/ddev.list > /dev/null\",\n { silent: true },\n );\n}\n\n/**\n * Install DDEV\n */\nexport function installDdev(io: SetupIo = nodeIo): void {\n step(\"Install DDEV\");\n\n if (io.commandExists(\"ddev\")) {\n const version = io.exec(\"ddev --version 2>/dev/null | head -1\", { silent: true });\n info(`DDEV already installed: ${version?.trim() ?? \"unknown version\"}`);\n return;\n }\n\n // Install DDEV via the official apt repository\n // See https://docs.ddev.com/en/stable/users/install/ddev-installation/#debianubuntu\n info(\"Adding DDEV apt repository...\");\n addDdevAptRepo(io);\n\n info(\"Installing DDEV...\");\n io.exec(\"apt-get update -qq\", { silent: true });\n io.exec(\"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ddev\", { silent: true });\n\n // Initialize mkcert certificate authority for the ddev user.\n // mkcert installs the CA into the home directory of the current user.\n // Since setup runs as root, we must point HOME to the ddev user's home\n // so the CA is trusted by DDEV when it runs as that user.\n io.exec(\"HOME=/home/ddev mkcert -install\", { silent: true });\n io.exec(\"chown -R ddev:ddev /home/ddev/.local/share/mkcert\", { silent: true });\n\n const version = io.exec(\"ddev --version 2>/dev/null | head -1\", { silent: true });\n success(`DDEV installed: ${version?.trim() ?? \"unknown version\"}`);\n}\n\n/** The Docker volume Traefik keeps its state in. */\nconst GLOBAL_CACHE_VOLUME = \"ddev-global-cache\";\n\n/**\n * Delete Traefik's ACME storage.\n *\n * Turning `use_letsencrypt` off stops DDEV asking for new certificates, but\n * it does not discard the ones already issued: Traefik keeps them in\n * acme.json inside the ddev-global-cache volume and serves them again after a\n * restart. On a server where a host ingress terminates TLS and verifies the\n * hop against the mkcert root, that resurrected certificate fails validation\n * and every request answers 502 — seen on a live server.\n *\n * The volume's mountpoint is resolved rather than reached through a\n * container, so this works whether or not ddev-router is running and needs no\n * extra image. Setup and migrations already run as root, which is what\n * reading a Docker volume from the host needs.\n *\n * Returns true when something was removed.\n */\nexport function purgeTraefikAcmeStorage(io: SetupIo = nodeIo): boolean {\n const mountpoint = io\n .execSilent(`docker volume inspect ${GLOBAL_CACHE_VOLUME} --format '{{.Mountpoint}}'`)\n .trim();\n\n if (!mountpoint) {\n // No volume yet — a fresh server has nothing to purge\n return false;\n }\n\n const acmeDir = `${mountpoint}/traefik`;\n\n // acme.json.* covers the copies a hand-fix or an upgrade may have left\n const found = io.execSilent(`ls ${acmeDir}/acme.json ${acmeDir}/acme.json.* 2>/dev/null`).trim();\n\n if (!found) {\n return false;\n }\n\n io.exec(`rm -f ${acmeDir}/acme.json ${acmeDir}/acme.json.*`, { silent: true });\n return true;\n}\n\n/**\n * Configure DDEV global settings for production\n *\n * Note on DNS and /etc/hosts:\n * When using a custom TLD (not ddev.site), DDEV may try to edit /etc/hosts\n * if it can't resolve project hostnames via DNS. To avoid this:\n * 1. Configure DNS so *.{tld} resolves to the server's IP (recommended)\n * 2. Or install dnsmasq for local wildcard DNS resolution\n *\n * With proper DNS, DDEV won't need sudo for hostname management.\n */\nexport function configureDdev(\n tld: string,\n email?: string,\n io: SetupIo = nodeIo,\n): void {\n step(\"Configure DDEV\");\n\n // Run as ddev user\n const ddevCmd = (cmd: string) => `su - ddev -c '${cmd}'`;\n\n // Configure all global settings in a single call\n io.exec(\n ddevCmd(\n `ddev config global --project-tld=${tld} --router-http-port=80 --router-https-port=443 --router-bind-all-interfaces=true --use-letsencrypt=${email ? \"true\" : \"false\"} ${email ? `--letsencrypt-email=${email}` : \"\"} --instrumentation-opt-in=false`,\n ),\n { silent: true },\n );\n\n success(`DDEV configured with TLD: ${tld}`);\n info(`Ensure DNS is configured: *.${tld} → server IP`);\n\n if (email) {\n success(`Let's Encrypt enabled with email: ${email}`);\n } else if (purgeTraefikAcmeStorage(io)) {\n // Otherwise a certificate issued before it was turned off comes back on\n // the next router restart, and a proxy verifying against mkcert rejects it\n success(\"Removed Traefik's Let's Encrypt storage\");\n }\n\n success(\"DDEV global settings configured\");\n}\n\n/**\n * Install dnsmasq for local DNS resolution (optional)\n * This allows the server itself to resolve *.{tld} hostnames\n * without needing to edit /etc/hosts for each project\n */\nexport function installDnsmasq(tld: string, io: SetupIo = nodeIo): void {\n step(\"Install dnsmasq for local DNS\");\n\n io.exec(\"apt-get update && apt-get install -y dnsmasq\", { silent: true });\n\n // Get server's public IP\n const serverIp = io.exec(\n \"curl -4 -s ifconfig.me || hostname -I | awk '{print $1}'\",\n { silent: true },\n )?.trim() || \"127.0.0.1\";\n\n // Configure dnsmasq to resolve *.{tld} to server IP\n const dnsmasqConfig = `# Trafic: Local DNS for DDEV projects\n# Resolve all *.${tld} to this server\naddress=/${tld}/${serverIp}\n\n# Don't read /etc/resolv.conf\nno-resolv\n\n# Use upstream DNS for everything else\nserver=1.1.1.1\nserver=8.8.8.8\n\n# Listen on localhost only\nlisten-address=127.0.0.1\nbind-interfaces\n`;\n\n io.writeFile(\"/etc/dnsmasq.d/trafic.conf\", dnsmasqConfig);\n\n // Configure system to use local dnsmasq\n io.exec(\"systemctl restart dnsmasq\");\n io.exec(\"systemctl enable dnsmasq\");\n\n // Update resolv.conf to use local DNS first\n info(\"Configure /etc/resolv.conf to use 127.0.0.1 as primary DNS\");\n\n success(`dnsmasq configured: *.${tld} → ${serverIp}`);\n}\n\n/**\n * Get the Docker gateway IP reachable from inside DDEV containers on Linux.\n * DDEV runs containers on the `ddev_default` network — we inspect that first.\n * Falls back to the default `bridge` network, then to 172.17.0.1.\n * `host.docker.internal` is not available on Linux without extra configuration.\n */\nexport function getDockerGatewayIp(io: SetupIo = nodeIo): string {\n // Prefer the ddev_default network gateway — that's the network ddev-router is on\n const ddevIp = io.execSilent(\n \"docker network inspect ddev_default --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}'\",\n );\n if (ddevIp) return ddevIp;\n\n // Fall back to the default bridge network\n const bridgeIp = io.execSilent(\n \"docker network inspect bridge --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}'\",\n );\n return bridgeIp || \"172.17.0.1\";\n}\n\n/** DDEV tool ports, used when the global config does not say otherwise */\nconst DEFAULT_TOOL_PORTS = {\n \"mailpit-http-port\": \"8025\",\n \"mailpit-https-port\": \"8026\",\n \"xhgui-http-port\": \"8143\",\n \"xhgui-https-port\": \"8142\",\n} as const;\n\n/**\n * Read the ports DDEV publishes its tools on.\n *\n * ddev-router publishes these on 0.0.0.0, and Docker bypasses UFW for\n * published ports, so a firewall rule cannot protect them — the forward auth\n * middleware is the only thing that can. Read them rather than hardcode: they\n * are configurable, and an entry point Traefik does not know about is ignored.\n */\nexport function readToolPorts(io: SetupIo = nodeIo): string[] {\n const output = io.execSilent(\"su - ddev -c 'ddev config global'\");\n const ports: string[] = [];\n\n for (const [key, fallback] of Object.entries(DEFAULT_TOOL_PORTS)) {\n const match = new RegExp(`^${key}=(\\\\d+)$`, \"m\").exec(output);\n ports.push(match?.[1] ?? fallback);\n }\n\n return [...new Set(ports)];\n}\n\n/** The ports ddev-router serves projects on. */\nexport interface RouterPorts {\n http: string;\n https: string;\n}\n\n/** Used when the global config does not say otherwise */\nconst DEFAULT_ROUTER_PORTS: RouterPorts = { http: \"80\", https: \"443\" };\n\n/**\n * Read the ports ddev-router serves projects on.\n *\n * Traefik names an entry point after its port, so these decide what the\n * catch-all routers and the auth middleware must attach to. Hardcoding 80 and\n * 443 is wrong on any server where the router was moved — which is every\n * server running a host ingress in front of it.\n */\nexport function readRouterPorts(io: SetupIo = nodeIo): RouterPorts {\n const output = io.execSilent(\"su - ddev -c 'ddev config global'\");\n\n const read = (key: string, fallback: string): string =>\n new RegExp(`^${key}=(\\\\d+)$`, \"m\").exec(output)?.[1] ?? fallback;\n\n return {\n http: read(\"router-http-port\", DEFAULT_ROUTER_PORTS.http),\n https: read(\"router-https-port\", DEFAULT_ROUTER_PORTS.https),\n };\n}\n\n/** Where the agent writes the Traefik files it owns. */\nexport const TRAEFIK_DIR = \"/home/ddev/.ddev/traefik\";\n\n/** Static config the agent owns, merged by DDEV into .static_config.yaml. */\nexport const STATIC_CONFIG = `${TRAEFIK_DIR}/static_config.trafic.yaml`;\n\n/** Wildcard TLS store, read before DDEV's own default_config.yaml. */\nexport const TLS_STORE_CONFIG = `${TRAEFIK_DIR}/custom-global-config/0-trafic-tls.yaml`;\n\n/** Router compose override carrying the DNS provider credentials. */\nexport const ROUTER_COMPOSE_OVERRIDE = \"/home/ddev/.ddev/router-compose.trafic.yaml\";\n\n/** Name of the DNS-01 resolver the agent adds to Traefik. */\nexport const DNS_RESOLVER = \"acme-dns\";\n\n/** What buildStaticConfig needs to emit the DNS-01 resolver. */\nexport interface TlsResolverOptions {\n /** lego provider name, e.g. \"cloudflare\" */\n provider: string;\n /** Account email — Let's Encrypt refuses a registration without one */\n email: string;\n /** ACME CA directory URL; the provider default is used when omitted */\n caServer?: string;\n}\n\n/**\n * Build the static config attaching forward auth to every entry point.\n *\n * The error page middleware stays on the web entry points only: a 502 from a\n * stopped project should show the waiting page, while a tool port has nothing\n * to wait for.\n *\n * With `tls`, a DNS-01 certificate resolver is added. It is only ever used by\n * the default TLS store (see buildTlsStoreConfig): DDEV keeps writing\n * `certResolver: acme-tlsChallenge` on every project router, and Traefik\n * skips those requests once the default store holds a certificate matching\n * the host with wildcard semantics.\n *\n * The propagation check is disabled on purpose. lego sets two TXT values at\n * the same name, one for `<tld>` and one for `*.<tld>`, then asks public\n * resolvers for each. A resolver that answered between the two writes caches\n * the partial answer for the record TTL — 120s on Cloudflare, longer than\n * lego's check window — and the order fails with \"NS 9.9.9.9:53 did not\n * return the expected TXT record\" although both records were correct. That\n * was seen on a live server; the staging run had passed by timing luck.\n * Let's Encrypt resolves authoritatively itself, so the check adds a failure\n * mode without adding safety. `PropagationWait(delay, skipCheck)` sleeps the\n * delay and then skips the check, so the 30s covers propagation to the\n * provider's edges before the CA looks.\n */\nexport function buildStaticConfig(\n toolPorts: string[],\n routerPorts: RouterPorts = DEFAULT_ROUTER_PORTS,\n tls?: TlsResolverOptions,\n): string {\n const webEntryPoint = (name: string) => ` ${name}:\n http:\n middlewares:\n - trafic-auth@file\n - trafic-errors@file\n`;\n\n const toolEntryPoint = (port: string) => ` http-${port}:\n http:\n middlewares:\n - trafic-auth@file\n`;\n\n // A tool port that happens to equal a router port must not be emitted\n // twice: a duplicate key makes the whole static config invalid\n const web = [routerPorts.http, routerPorts.https];\n const tools = toolPorts.filter((port) => !web.includes(port));\n\n const entryPoints = `entryPoints:\n${web.map((port) => webEntryPoint(`http-${port}`)).join(\"\")}${tools.map(toolEntryPoint).join(\"\")}`;\n\n if (!tls) {\n return entryPoints;\n }\n\n return `${entryPoints}\ncertificatesResolvers:\n ${DNS_RESOLVER}:\n acme:\n email: \"${escapeYamlDouble(tls.email)}\"\n storage: /mnt/ddev-global-cache/traefik/${DNS_RESOLVER}.json\n${tls.caServer ? ` caServer: \"${escapeYamlDouble(tls.caServer)}\"\\n` : \"\"} dnsChallenge:\n provider: \"${escapeYamlDouble(tls.provider)}\"\n propagation:\n delayBeforeChecks: 30s\n disableChecks: true\n`;\n}\n\n/**\n * Build the default TLS store holding the wildcard certificate.\n *\n * Traefik's file provider keeps the first `tls.stores.default` it reads in\n * directory order and logs \"TLS store default already configured, skipping\"\n * for every later file. DDEV writes an empty one in `default_config.yaml`,\n * so this has to be read first — hence the `0-` prefix on the file name.\n */\nexport function buildTlsStoreConfig(tld: string): string {\n return `# Trafic: wildcard certificate served to every host without its own\n#\n# Read before DDEV's default_config.yaml — Traefik keeps the first default\n# store it sees, so the file name has to sort first.\ntls:\n stores:\n default:\n defaultGeneratedCert:\n resolver: ${DNS_RESOLVER}\n domain:\n main: \"${escapeYamlDouble(tld)}\"\n sans:\n - \"*.${escapeYamlDouble(tld)}\"\n`;\n}\n\n/**\n * Build the router compose override carrying the provider credentials.\n *\n * lego reads them from the router container's environment, so they cannot be\n * passed through a config file. DDEV merges `~/.ddev/router-compose.*.yaml`\n * into the router compose and recreates the container when it changes, on the\n * next `ddev start`.\n */\nexport function buildRouterComposeOverride(env: Record<string, string>): string {\n const entries = Object.entries(env);\n\n return `# Trafic: DNS-01 provider credentials for the Traefik router\n# Contains secrets — this file is mode 600.\nservices:\n ddev-router:\n environment:\n${entries.map(([key, value]) => ` - \"${key}=${escapeYamlDouble(value)}\"`).join(\"\\n\")}\n`;\n}\n\n/** Escape a value for a double-quoted YAML scalar. */\nfunction escapeYamlDouble(value: string): string {\n return value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n}\n\n/**\n * Build the dynamic config: the middlewares, the agent service, and the\n * catch-all routers that hand a stopped project to the waiting page.\n *\n * One catch-all per web entry point, each naming the entry point it serves.\n * A router that names none is instantiated by Traefik on *every* entry point,\n * and DDEV's router health check compares the number of router definitions in\n * its config files against the number Traefik reports. Two unpinned\n * definitions became fourteen instances on a server with six tool entry\n * points, so the counts never matched, the health check waited out its\n * timeout, and every `ddev start` failed after 60 seconds.\n */\nexport function buildDynamicConfig(\n gatewayIp: string,\n routerPorts: RouterPorts = DEFAULT_ROUTER_PORTS,\n): string {\n return `# Trafic: Forward auth middleware definition\nhttp:\n middlewares:\n trafic-auth:\n forwardAuth:\n address: \"http://${gatewayIp}:9876/__auth__\"\n authResponseHeaders:\n - \"X-Trafic-Project\"\n\n trafic-errors:\n errors:\n status:\n - \"502\"\n - \"503\"\n service: trafic-service\n query: \"/\"\n\n routers:\n # The lowest priority above DDEV's own fallback routers, which sit at\n # priority 1 since DDEV 1.25.4, and still far below any project router —\n # so a DDEV project router always wins. These only match when none does —\n # which is what a stopped project produces, because DDEV removes that\n # project's router and Traefik would otherwise answer 404. Without them\n # the waiting page never appears and scale-to-zero never restarts\n # anything. Auth is unaffected: it is attached at the entry point, not\n # here, so this cannot reintroduce the bypass that #27 fixed.\n #\n # Priority 1 is not enough any more: DDEV 1.25.4 ships\n # ddev-router-fallback-http and ddev-router-fallback-https with the same\n # PathPrefix(\\`/\\`) rule at priority 1, pointing at a 404 responder. With\n # equal priority and identical rules Traefik picked DDEV's, so a stopped\n # project answered DDEV's 404 page instead of the waiting page.\n #\n # Each names exactly one entry point. Leaving entryPoints out attaches a\n # router to all of them, which breaks DDEV's router health check — it\n # counts definitions and compares them to what Traefik loaded.\n trafic-catchall:\n rule: \"PathPrefix(\\`/\\`)\"\n entryPoints:\n - http-${routerPorts.http}\n priority: 2\n service: trafic-service\n\n # Carries tls, so it only matches the https entry point. The plain one\n # above cannot serve it, and one alone would leave HTTP answering 404.\n trafic-catchall-tls:\n rule: \"PathPrefix(\\`/\\`)\"\n entryPoints:\n - http-${routerPorts.https}\n priority: 2\n service: trafic-service\n tls: {}\n\n services:\n trafic-service:\n loadBalancer:\n servers:\n - url: \"http://${gatewayIp}:9876\"\n`;\n}\n\n/** What DDEV knows about Let's Encrypt, read from its global config. */\nexport interface LetsEncryptSettings {\n enabled: boolean;\n email?: string;\n}\n\n/**\n * Read DDEV's Let's Encrypt settings.\n *\n * DDEV owns them — `ddev config global --use-letsencrypt --letsencrypt-email`\n * — so they are the single source of truth for the ACME account, wildcard\n * mode included.\n */\nexport function readLetsEncryptSettings(io: SetupIo = nodeIo): LetsEncryptSettings {\n const output = io.execSilent(\"su - ddev -c 'ddev config global'\");\n\n return {\n enabled: /^use-letsencrypt=true$/m.test(output),\n email: /^letsencrypt-email=(.+)$/m.exec(output)?.[1]?.trim() || undefined,\n };\n}\n\n/** Read the project TLD from DDEV's global config. */\nexport function readProjectTld(io: SetupIo = nodeIo): string {\n const output = io.execSilent(\"su - ddev -c 'ddev config global'\");\n\n return /^project-tld=(.+)$/m.exec(output)?.[1]?.trim() ?? \"\";\n}\n\n/** Options for configureTraefik. */\nexport interface ConfigureTraefikOptions {\n /**\n * TLS settings. Read from /etc/trafic/config.toml when omitted, which is\n * what migrations do — setup passes them, since it writes that file later.\n */\n tls?: TlsConfig;\n /** Project TLD. Read from DDEV's global config when omitted. */\n tld?: string;\n}\n\n/**\n * Configure Traefik for forward auth, and for the wildcard certificate when\n * a DNS provider is configured.\n *\n * Everything the agent owns in Traefik's configuration is written here, so\n * setup and the migrations share one code path and cannot drift apart.\n */\nexport function configureTraefik(\n options: ConfigureTraefikOptions = {},\n io: SetupIo = nodeIo,\n): void {\n step(\"Configure Traefik for forward auth\");\n\n // Create custom Traefik config directories\n io.exec(`mkdir -p ${TRAEFIK_DIR}/custom-global-config`, { silent: true });\n io.exec(\"chown -R ddev:ddev /home/ddev/.ddev\", { silent: true });\n\n // Use the Docker bridge gateway IP instead of host.docker.internal —\n // the latter is not available on Linux without extra Docker configuration.\n const gatewayIp = getDockerGatewayIp(io);\n\n // Entry point names come from the ports, so both configs need them\n const routerPorts = readRouterPorts(io);\n\n // Dynamic configuration: the middlewares, the agent service and the\n // catch-all routers. Written to custom-global-config/, which DDEV copies\n // into the ddev-global-cache volume on project start. It used to be written\n // to the traefik root as well, for DDEV versions that watched it — nothing\n // reads that copy on 1.25, and having two made debugging harder.\n const dynamicConfig = buildDynamicConfig(gatewayIp, routerPorts);\n\n io.writeFile(`${TRAEFIK_DIR}/custom-global-config/trafic.yaml`, dynamicConfig);\n io.exec(`chown ddev:ddev ${TRAEFIK_DIR}/custom-global-config/trafic.yaml`, { silent: true });\n\n const tls = options.tls ?? loadConfig().tls;\n const resolver = tls.dnsProvider\n ? buildTlsResolverOptions(tls, io)\n : undefined;\n\n // Read and check the TLD before anything is written: a static config that\n // names the resolver without the TLS store behind it is a broken pairing,\n // and there is no reason to leave one on disk when the TLD is missing.\n const tld = resolver ? readWildcardTld(options.tld, io) : \"\";\n\n // Static configuration: attaches trafic-auth to every entry point DDEV\n // publishes, and trafic-errors to the web ones, so every request goes\n // through auth regardless of which project router handles it.\n // DDEV merges all static_config.*.yaml files into .static_config.yaml on start.\n const staticConfig = buildStaticConfig(readToolPorts(io), routerPorts, resolver);\n\n io.writeFile(STATIC_CONFIG, staticConfig);\n io.exec(`chown ddev:ddev ${STATIC_CONFIG}`, { silent: true });\n\n info(`Entry points: http-${routerPorts.http}, http-${routerPorts.https} (web) plus tool ports`);\n\n if (resolver) {\n writeWildcardFiles(tls, tld, io);\n } else {\n removeWildcardFiles(io);\n }\n\n success(\"Traefik configured with Trafic middleware\");\n}\n\n/**\n * Name of one running DDEV project, or null when none runs.\n *\n * `ddev list -j` is the only source that knows: `project_list.yaml`, which the\n * agent watches for discovery, records every project DDEV has ever seen but\n * carries no live status — a project stopped an hour ago still sits there.\n * `ddev list -j` asks Docker, and wraps the entries in a `raw` array.\n *\n * Used to push a changed global Traefik config into the running router: DDEV\n * copies `custom-global-config/*.yaml` into it on any `ddev start`, so\n * starting a project that is already running is enough, and nothing that is\n * serving traffic has to be stopped.\n */\nexport function findRunningProject(io: SetupIo = nodeIo): string | null {\n const output = io.execSilent(\"su - ddev -c 'ddev list -j'\");\n\n if (!output) {\n return null;\n }\n\n try {\n const parsed = JSON.parse(output) as { raw?: { name?: string; status?: string }[] };\n const running = (parsed.raw ?? []).find((project) => project.status === \"running\");\n return running?.name ?? null;\n } catch {\n // A DDEV that answers something other than JSON is not worth guessing at\n return null;\n }\n}\n\n/**\n * Turn the agent's TLS config into what the static config needs.\n *\n * The ACME account comes from DDEV: a resolver without an email is refused by\n * Let's Encrypt, and `use_letsencrypt=false` means DDEV is also stripping the\n * certificate resolver from the project routers.\n */\nfunction buildTlsResolverOptions(tls: TlsConfig, io: SetupIo): TlsResolverOptions {\n const letsEncrypt = readLetsEncryptSettings(io);\n\n if (!letsEncrypt.enabled || !letsEncrypt.email) {\n throw new Error(\n \"tls.dns_provider needs Let's Encrypt enabled in DDEV with an account email.\\n\" +\n \" Run setup again with --email=<address>, or unset tls.dns_provider.\",\n );\n }\n\n return {\n provider: tls.dnsProvider!,\n email: letsEncrypt.email,\n caServer: tls.caServer,\n };\n}\n\n/** Read the TLD the wildcard certificate covers, or fail before any write. */\nfunction readWildcardTld(configured: string | undefined, io: SetupIo): string {\n const tld = configured || readProjectTld(io);\n\n if (!tld) {\n throw new Error(\n \"Cannot configure the wildcard certificate: no project TLD is set in DDEV\",\n );\n }\n\n return tld;\n}\n\n/** Write the wildcard TLS store and the router credentials. */\nfunction writeWildcardFiles(tls: TlsConfig, tld: string, io: SetupIo): void {\n io.writeFile(TLS_STORE_CONFIG, buildTlsStoreConfig(tld));\n io.exec(`chown ddev:ddev ${TLS_STORE_CONFIG}`, { silent: true });\n\n // Holds the provider token: created empty and mode 600 first, so the\n // credentials never exist on disk under the umask default. `install`\n // truncates an existing file and resets its mode, and the later write\n // keeps it.\n io.exec(`install -m 600 -o ddev -g ddev /dev/null ${ROUTER_COMPOSE_OVERRIDE}`, {\n silent: true,\n });\n io.writeFile(ROUTER_COMPOSE_OVERRIDE, buildRouterComposeOverride(tls.dnsEnv));\n\n success(`Wildcard certificate: *.${tld} via the ${tls.dnsProvider} DNS-01 provider`);\n}\n\n/** Remove the wildcard files where no DNS provider is configured. */\nfunction removeWildcardFiles(io: SetupIo): void {\n if (io.fileExists(TLS_STORE_CONFIG)) {\n io.exec(`rm -f ${TLS_STORE_CONFIG}`, { silent: true });\n }\n\n if (io.fileExists(ROUTER_COMPOSE_OVERRIDE)) {\n io.exec(`rm -f ${ROUTER_COMPOSE_OVERRIDE}`, { silent: true });\n }\n}\n","import { dirname, join } from \"node:path\";\nimport { getDockerGatewayIp } from \"../setup/ddev.js\";\nimport { nodeIo, type SetupIo } from \"../setup/io.js\";\n\n/**\n * Files DDEV reads the dynamic Traefik configuration from, relative to the\n * .ddev directory.\n *\n * Only custom-global-config: DDEV copies that into the ddev-global-cache\n * volume, which is where Traefik actually reads from. Setup used to write a\n * second copy at the traefik root for older DDEV versions; migration 0010\n * removes it, and nothing reads it on 1.25.\n */\nconst DYNAMIC_CONFIGS = [\"traefik/custom-global-config/trafic.yaml\"];\n\n/** Matches the host in an agent URL, e.g. http://172.17.0.1:9876/__auth__ */\nconst AGENT_URL = /http:\\/\\/(\\d+\\.\\d+\\.\\d+\\.\\d+):(\\d+)/g;\n\n/**\n * The address the forward-auth middleware currently points at.\n */\nexport function readForwardAuthAddress(content: string): string | undefined {\n const match = /http:\\/\\/(\\d+\\.\\d+\\.\\d+\\.\\d+):\\d+/.exec(content);\n return match?.[1];\n}\n\n/**\n * Point every agent URL in a dynamic config at a different host, keeping\n * each port as it was.\n */\nexport function rewriteForwardAuthAddress(\n content: string,\n gatewayIp: string,\n): string {\n return content.replace(AGENT_URL, `http://${gatewayIp}:$2`);\n}\n\n/**\n * Correct the forward-auth address if the Docker gateway has moved.\n *\n * setup runs `configureTraefik` before any DDEV project exists, so the\n * `ddev_default` network is absent and the address falls back to the default\n * bridge. Once a project starts, that address is stale. Migration 0006 exists\n * to fix it on servers provisioned earlier, but `markAllMigrationsApplied()`\n * marks it applied on fresh ones, so nothing ever recomputed it.\n *\n * Traefik watches the dynamic config, so rewriting the file is enough — no\n * restart. Any failure is logged and ignored: a stale address is survivable,\n * an agent that will not start is not.\n */\nexport function syncForwardAuthAddress(\n projectListPath: string,\n io: SetupIo = nodeIo,\n): void {\n try {\n const ddevDir = dirname(projectListPath);\n const gatewayIp = getDockerGatewayIp(io);\n const changed: string[] = [];\n\n for (const relative of DYNAMIC_CONFIGS) {\n const path = join(ddevDir, relative);\n\n if (!io.fileExists(path)) {\n continue;\n }\n\n const content = io.readFile(path);\n const current = readForwardAuthAddress(content);\n\n if (!current || current === gatewayIp) {\n continue;\n }\n\n io.writeFile(path, rewriteForwardAuthAddress(content, gatewayIp));\n changed.push(path);\n }\n\n if (changed.length > 0) {\n console.log(\n `Traefik forward auth: corrected address to ${gatewayIp} in ${changed.length} file(s)`,\n );\n }\n } catch (error) {\n console.warn(\n `Could not check the Traefik forward auth address: ${error instanceof Error ? error.message : error}`,\n );\n }\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { DNS_RESOLVER, STATIC_CONFIG } from \"../setup/ddev.js\";\nimport type { AgentConfig } from \"../types.js\";\n\n/**\n * Warn when the config asks for a wildcard certificate that Traefik never got.\n *\n * The files are written by setup and by migration 0016, not by the running\n * agent: writing them needs root, and applying them needs a project start.\n * So this only reports, and says which command fixes it.\n */\nexport function warnIfWildcardNotApplied(\n config: AgentConfig,\n staticConfigPath: string = STATIC_CONFIG,\n): boolean {\n if (!config.tls.dnsProvider) {\n return false;\n }\n\n const configured =\n existsSync(staticConfigPath) &&\n readFileSync(staticConfigPath, \"utf-8\").includes(`${DNS_RESOLVER}:`);\n\n if (configured) {\n return false;\n }\n\n console.warn(\n `tls.dns_provider is set to \"${config.tls.dnsProvider}\" but Traefik has no ${DNS_RESOLVER} resolver.`,\n );\n console.warn(\n \" The wildcard certificate is not being requested. Run `sudo trafic-agent upgrade`, or re-run setup.\",\n );\n\n return true;\n}\n","import { createServer, type IncomingMessage, type ServerResponse } from \"node:http\";\nimport { readFileSync } from \"node:fs\";\nimport { resolve, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { AgentConfig, AuthConfig } from \"./types.js\";\nimport { checkAuth, resolveClientIp } from \"./utils/auth.js\";\nimport {\n loadProjectList,\n buildHostnameIndex,\n watchProjectList,\n startProject,\n getProjectInfo,\n} from \"./utils/ddev.js\";\nimport {\n initDb,\n updateProjectAccess,\n logAccess,\n setProjectStatus,\n getProject,\n} from \"./utils/db.js\";\nimport { loadProjectConfig } from \"./utils/project-config.js\";\nimport { syncForwardAuthAddress } from \"./utils/traefik.js\";\nimport { warnIfWildcardNotApplied } from \"./utils/tls.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n// State\nlet config: AgentConfig;\nlet projectList: Map<string, string>;\nlet hostnameIndex: Map<string, string>;\n// Cache of per-project configs (project name -> config)\nconst projectConfigs = new Map<string, ReturnType<typeof loadProjectConfig>>();\n\n/**\n * What the request handlers need from the outside world.\n *\n * Gathered into one seam so a test can drive the handlers with a known\n * project list and record what they did, without a listening socket, a real\n * database or a DDEV install. `startServer` builds the real one.\n */\nexport interface ServerDeps {\n auth: AuthConfig;\n /** hostname -> project name */\n hostnameIndex: Map<string, string>;\n /** project name -> its own config, where it has one */\n projectConfigs: Map<string, ReturnType<typeof loadProjectConfig>>;\n loadTemplate: (name: string) => string;\n updateProjectAccess: (name: string) => void;\n logAccess: (log: Parameters<typeof logAccess>[0]) => void;\n getProject: (name: string) => ReturnType<typeof getProject>;\n setProjectStatus: (name: string, status: \"running\" | \"stopped\" | \"starting\") => void;\n startProject: (name: string) => Promise<boolean>;\n getProjectInfo: (name: string) => ReturnType<typeof getProjectInfo>;\n}\n\n/**\n * Load HTML template\n */\nfunction loadTemplate(name: string): string {\n const templatePath = resolve(__dirname, \"..\", \"templates\", `${name}.html`);\n try {\n return readFileSync(templatePath, \"utf-8\");\n } catch {\n return `<html><body><h1>${name}</h1></body></html>`;\n }\n}\n\n/**\n * Get effective auth config for a project (merges global + per-project)\n */\nexport function getEffectiveAuthConfig(\n projectName: string | undefined,\n deps: Pick<ServerDeps, \"auth\" | \"projectConfigs\">,\n): AuthConfig {\n if (!projectName) return deps.auth;\n\n const projectConfig = deps.projectConfigs.get(projectName);\n if (!projectConfig?.auth_policy) return deps.auth;\n\n // Override default policy with project-specific policy\n return {\n ...deps.auth,\n defaultPolicy: projectConfig.auth_policy,\n };\n}\n\n/**\n * Handle forward auth requests from Traefik\n * Traefik sends the original request headers, we return 200 (allow) or 401 (deny)\n */\nexport function handleAuth(\n req: IncomingMessage,\n res: ServerResponse,\n deps: ServerDeps,\n): void {\n const hostname = req.headers[\"x-forwarded-host\"] as string ?? \"\";\n // Keep the two apart: the socket peer cannot be forged, X-Forwarded-For\n // partly can. checkAuth decides which entry to trust.\n const socketIp = req.socket.remoteAddress ?? \"\";\n const forwardedFor = req.headers[\"x-forwarded-for\"] as string | undefined;\n const authorization = req.headers[\"authorization\"];\n const path = req.headers[\"x-forwarded-uri\"] as string ?? \"/\";\n\n // Find project from hostname\n const projectName = deps.hostnameIndex.get(hostname);\n\n // Get effective auth config (global + per-project overrides)\n const authConfig = getEffectiveAuthConfig(projectName, deps);\n\n const clientIp = resolveClientIp(\n socketIp,\n forwardedFor,\n authConfig.trustedProxyHops,\n );\n\n const result = checkAuth(\n {\n hostname,\n ip: socketIp,\n authorization,\n forwardedFor,\n },\n authConfig,\n );\n\n if (result.allowed) {\n // Log access and update last access time\n if (projectName) {\n deps.updateProjectAccess(projectName);\n deps.logAccess({\n project: projectName,\n timestamp: Date.now(),\n ip: clientIp,\n userAgent: req.headers[\"user-agent\"] ?? \"\",\n path,\n });\n }\n\n res.writeHead(200);\n res.end();\n } else {\n // Return 401 with WWW-Authenticate header for basic auth\n res.writeHead(401, {\n \"WWW-Authenticate\": 'Basic realm=\"Trafic\"',\n });\n res.end(\"Unauthorized\");\n }\n}\n\n/**\n * Handle errors middleware requests (502 from Traefik)\n * When a project is stopped, Traefik returns 502. We show a waiting page and start the project.\n */\nexport async function handleErrors(\n req: IncomingMessage,\n res: ServerResponse,\n deps: ServerDeps,\n): Promise<void> {\n const hostname = req.headers[\"x-forwarded-host\"] as string ?? req.headers.host ?? \"\";\n const projectName = deps.hostnameIndex.get(hostname);\n\n if (!projectName) {\n // Unknown project\n const template = deps.loadTemplate(\"error\");\n res.writeHead(404, { \"Content-Type\": \"text/html\" });\n res.end(template.replace(\"{{message}}\", \"Project not found\"));\n return;\n }\n\n // Check if project is already starting\n const record = deps.getProject(projectName);\n if (record?.status === \"starting\") {\n // Show waiting page\n const template = deps.loadTemplate(\"wait\");\n res.writeHead(503, {\n \"Content-Type\": \"text/html\",\n \"Retry-After\": \"5\",\n });\n res.end(\n template\n .replace(/\\{\\{project\\}\\}/g, projectName)\n .replace(/\\{\\{hostname\\}\\}/g, hostname),\n );\n return;\n }\n\n // Mark as starting\n deps.setProjectStatus(projectName, \"starting\");\n\n // Show waiting page immediately\n const template = deps.loadTemplate(\"wait\");\n res.writeHead(503, {\n \"Content-Type\": \"text/html\",\n \"Retry-After\": \"5\",\n });\n res.end(\n template\n .replace(/\\{\\{project\\}\\}/g, projectName)\n .replace(/\\{\\{hostname\\}\\}/g, hostname),\n );\n\n // Start the project without waiting for it. The response is already sent,\n // and `startProject` no longer blocks the event loop, so the agent keeps\n // serving forward auth for every other project while this runs. The status\n // is recorded when it settles, which is what stops a second request from\n // starting the same project again.\n void deps.startProject(projectName)\n .then((success) => {\n deps.setProjectStatus(projectName, success ? \"running\" : \"stopped\");\n })\n .catch((error: unknown) => {\n // Leaving it \"starting\" forever would wedge the waiting page\n deps.setProjectStatus(projectName, \"stopped\");\n console.error(`Could not start ${projectName}:`, error);\n });\n}\n\n/**\n * Handle status polling requests\n */\nexport async function handleStatus(\n req: IncomingMessage,\n res: ServerResponse,\n deps: ServerDeps,\n): Promise<void> {\n const url = new URL(req.url ?? \"/\", `http://${req.headers.host}`);\n const projectName = url.searchParams.get(\"project\");\n\n if (!projectName) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Missing project parameter\" }));\n return;\n }\n\n const info = await deps.getProjectInfo(projectName);\n const record = deps.getProject(projectName);\n\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n name: projectName,\n status: info?.status ?? record?.status ?? \"unknown\",\n ready: info?.status === \"running\",\n }),\n );\n}\n\n/**\n * The path a request should be routed on, without the query string.\n *\n * `req.url` is a target, not a URL: it carries the query and, for a proxied\n * request, may be absolute. Parsing against a fixed base keeps a malformed or\n * absolute target from deciding the route, and a trailing slash is dropped so\n * `/__auth__/` and `/__auth__` are the same endpoint.\n */\nexport function routePath(target: string | undefined): string {\n let path: string;\n\n try {\n path = new URL(target ?? \"/\", \"http://localhost\").pathname;\n } catch {\n return \"/\";\n }\n\n return path.length > 1 && path.endsWith(\"/\") ? path.slice(0, -1) : path;\n}\n\n/**\n * Request handler\n */\nexport async function handleRequest(\n req: IncomingMessage,\n res: ServerResponse,\n deps: ServerDeps,\n): Promise<void> {\n // Route on the path alone. Matching the raw URL meant a query string threw\n // every internal route off: Traefik's catch-all and errors middleware pass\n // the original request through, so `/?s=term` on a stopped project missed\n // every branch and fell to the waiting page — which then started an\n // already-running project. `/__status__?project=` never reached its handler\n // for the same reason: it needs the query it was being routed away from.\n const path = routePath(req.url);\n\n try {\n // Route requests\n if (path === \"/__auth__\" || path.startsWith(\"/__auth__/\")) {\n handleAuth(req, res, deps);\n } else if (path === \"/__status__\" || path.startsWith(\"/__status__/\")) {\n await handleStatus(req, res, deps);\n } else if (path === \"/__health__\") {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"ok\", version: \"__VERSION__\" }));\n } else {\n // Default: errors middleware\n await handleErrors(req, res, deps);\n }\n } catch (error) {\n console.error(\"Request error:\", error);\n res.writeHead(500, { \"Content-Type\": \"text/plain\" });\n res.end(\"Internal Server Error\");\n }\n}\n\n/**\n * Reload project list and their configs\n */\nfunction reloadProjects(): void {\n projectList = loadProjectList(config.projectListPath);\n hostnameIndex = buildHostnameIndex(projectList, config.tld);\n\n // Load per-project configs\n projectConfigs.clear();\n for (const [name, projectDir] of projectList) {\n const projectConfig = loadProjectConfig(projectDir);\n if (projectConfig.auth_policy || projectConfig.idle_timeout) {\n projectConfigs.set(name, projectConfig);\n console.log(` ${name}: auth=${projectConfig.auth_policy ?? \"default\"}, idle=${projectConfig.idle_timeout ?? \"default\"}`);\n }\n }\n\n console.log(`Loaded ${projectList.size} projects (${projectConfigs.size} with custom config)`);\n}\n\n/**\n * Start the agent server\n */\nexport function startServer(agentConfig: AgentConfig): void {\n config = agentConfig;\n\n // Initialize database\n initDb(config.dbPath);\n\n // setup wrote the forward auth address before any DDEV network existed, so\n // correct it now that one may have appeared\n syncForwardAuthAddress(config.projectListPath);\n\n // A DNS provider in the config that never reached Traefik means the\n // wildcard certificate is not being requested at all\n warnIfWildcardNotApplied(config);\n\n // Load projects\n reloadProjects();\n\n // Watch for changes\n watchProjectList(config.projectListPath, () => {\n console.log(\"Project list changed, reloading...\");\n reloadProjects();\n });\n\n // The real dependencies. Rebuilt per request for the two maps, which\n // reloadProjects replaces wholesale when the project list changes.\n const deps = (): ServerDeps => ({\n auth: config.auth,\n hostnameIndex,\n projectConfigs,\n loadTemplate,\n updateProjectAccess,\n logAccess,\n getProject,\n setProjectStatus,\n startProject,\n getProjectInfo,\n });\n\n // Create HTTP server\n const server = createServer((req, res) => {\n handleRequest(req, res, deps()).catch((error) => {\n console.error(\"Unhandled error:\", error);\n if (!res.headersSent) {\n res.writeHead(500);\n res.end(\"Internal Server Error\");\n }\n });\n });\n\n server.listen(config.port, () => {\n console.log(`Trafic agent listening on port ${config.port}`);\n console.log(`TLD: ${config.tld}`);\n console.log(`Projects: ${projectList.size}`);\n });\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"setup-4zlNJqDe.js","names":[],"sources":["../src/setup/docker-firewall.ts","../src/tasks/stop-idle.ts","../src/setup/user.ts","../src/setup/docker.ts","../src/setup/agent.ts","../src/setup/hardening.ts","../src/setup/audit.ts","../src/setup/migrations/0001__ddev_apt_repo.ts","../src/setup/migrations/0002__mkcert_ddev_user.ts","../src/setup/migrations/0003__ddev_router_bind_all_interfaces.ts","../src/setup/migrations/0004__ddev_hostname_sudoers.ts","../src/setup/migrations/0005__traefik_gateway_ip.ts","../src/setup/migrations/0006__traefik_gateway_ip_fix.ts","../src/setup/migrations/0007__ufw_docker_trafic_port.ts","../src/setup/migrations/0008__traefik_entrypoint_middleware.ts","../src/setup/migrations/0009__scale_to_zero_fixes.ts","../src/setup/migrations/0010__catchall_entrypoints.ts","../src/setup/migrations/0011__purge_acme_when_disabled.ts","../src/setup/migrations/0012__docker_user_tool_ports.ts","../src/setup/migrations/0013__ssh_kex_defaults.ts","../src/setup/migrations/0014__unattended_upgrades_docker_origin.ts","../src/setup/migrations/0015__catchall_priority.ts","../src/setup/migrations/0016__wildcard_dns_challenge.ts","../src/setup/migrations/0018__docker_address_pools.ts","../src/setup/migrations/index.ts","../src/setup/index.ts"],"sourcesContent":["import { nodeIo, type SetupIo } from \"./io.js\";\nimport { step, success, info } from \"./steps.js\";\n\n/** Where the rule script lives, so systemd and a human can both run it. */\nexport const DOCKER_FIREWALL_SCRIPT = \"/usr/local/sbin/trafic-docker-firewall\";\n\n/** The unit that reapplies the rules after Docker starts. */\nexport const DOCKER_FIREWALL_UNIT =\n \"/etc/systemd/system/trafic-docker-firewall.service\";\n\n/**\n * Docker's own bridge ranges, exempted so containers and the host gateway can\n * still reach a published port. Containers normally talk to each other by\n * container address rather than through the published port, so this is a\n * safety valve rather than a requirement.\n */\nconst DOCKER_IPV4_NETS = \"172.16.0.0/12\";\nconst DOCKER_IPV6_NETS = \"fd00::/8\";\n\n/**\n * Build the script that restricts the ports ddev-router publishes for DDEV's\n * tools (Mailpit, xhgui) to Docker's own networks.\n *\n * UFW cannot express this. Docker puts its DNAT in `nat/PREROUTING` and its\n * filtering in the `DOCKER` chain of `FORWARD`, both evaluated before UFW's\n * chains, so `ufw default deny incoming` never sees these packets.\n * `DOCKER-USER` is the hook Docker provides for exactly this case: it is\n * jumped to first in `FORWARD` and Docker never flushes it.\n *\n * A network-edge firewall is not a substitute. Measured on an OVH server with\n * its Edge Firewall enabled and a correct deny-all rule: a published tool port\n * still accepted a connection from another host inside OVH in 114ms, while a\n * port with nothing listening was dropped and timed out at 12s. Anyone who can\n * rent a VM from the same provider is inside that blind spot. These rules\n * apply to every packet regardless of where it came from.\n *\n * The match is on the *original* destination port. By the time a packet\n * reaches `FORWARD`, DNAT has rewritten the destination to the container, so\n * `--dport` would be matching the container's port — the same number today,\n * but not if a published port and a container port ever differ.\n */\nexport function buildDockerFirewallScript(toolPorts: string[]): string {\n return `#!/bin/sh\n# Trafic: restrict the ports ddev-router publishes for DDEV's tools.\n#\n# Generated by trafic-agent setup. Reapplied at boot by\n# trafic-docker-firewall.service, because Docker recreates DOCKER-USER empty\n# and nothing else restores what we put there.\n#\n# Why not UFW: Docker's DNAT sits in nat/PREROUTING and its filtering in the\n# DOCKER chain of FORWARD, both evaluated before UFW's chains, so\n# \"ufw default deny incoming\" never sees these packets. DOCKER-USER is the\n# hook Docker provides — jumped to first in FORWARD, never flushed by Docker.\n#\n# Why not only a provider firewall: measured on OVH with its Edge Firewall\n# enabled and denying these ports, a connection from another host inside OVH\n# still succeeded. A network-edge filter can have blind spots for traffic that\n# never crosses the edge. These rules do not.\n#\n# The match is on the ORIGINAL destination port: after DNAT the destination is\n# the container, so --dport would match the wrong side of the mapping.\nset -eu\n\nPORTS=\"${toolPorts.join(\" \")}\"\n\napply() {\n cmd=\"$1\"\n nets=\"$2\"\n\n command -v \"$cmd\" >/dev/null 2>&1 || return 0\n\n for port in $PORTS; do\n # Drop any previous copy first, so running this twice does not stack rules\n while \"$cmd\" -C DOCKER-USER -p tcp -m conntrack --ctorigdstport \"$port\" \\\\\n --ctdir ORIGINAL -j DROP 2>/dev/null; do\n \"$cmd\" -D DOCKER-USER -p tcp -m conntrack --ctorigdstport \"$port\" \\\\\n --ctdir ORIGINAL -j DROP\n done\n while \"$cmd\" -C DOCKER-USER -p tcp -s \"$nets\" -m conntrack \\\\\n --ctorigdstport \"$port\" --ctdir ORIGINAL -j RETURN 2>/dev/null; do\n \"$cmd\" -D DOCKER-USER -p tcp -s \"$nets\" -m conntrack \\\\\n --ctorigdstport \"$port\" --ctdir ORIGINAL -j RETURN\n done\n\n # Insert the DROP first, then the RETURN above it, so Docker's own\n # networks keep working and everything else is dropped\n \"$cmd\" -I DOCKER-USER -p tcp -m conntrack --ctorigdstport \"$port\" \\\\\n --ctdir ORIGINAL -j DROP\n \"$cmd\" -I DOCKER-USER -p tcp -s \"$nets\" -m conntrack \\\\\n --ctorigdstport \"$port\" --ctdir ORIGINAL -j RETURN\n done\n}\n\napply iptables ${DOCKER_IPV4_NETS}\napply ip6tables ${DOCKER_IPV6_NETS}\n`;\n}\n\n/** The systemd unit that reapplies the rules after Docker starts. */\nexport function buildDockerFirewallUnit(): string {\n return `[Unit]\nDescription=Trafic - restrict Docker-published DDEV tool ports\n# Docker recreates DOCKER-USER empty on start, so the rules have to go back\n# afterwards. PartOf means a docker restart reapplies them too.\nAfter=docker.service\nRequires=docker.service\nPartOf=docker.service\n\n[Service]\nType=oneshot\nRemainAfterExit=yes\nExecStart=${DOCKER_FIREWALL_SCRIPT}\n\n[Install]\nWantedBy=multi-user.target\n`;\n}\n\n/**\n * Install and enable the DOCKER-USER rules.\n */\nexport function configureDockerFirewall(\n toolPorts: string[],\n io: SetupIo = nodeIo,\n): void {\n step(\"Restrict Docker-published tool ports\");\n\n io.writeFile(DOCKER_FIREWALL_SCRIPT, buildDockerFirewallScript(toolPorts));\n io.exec(`chmod 0755 ${DOCKER_FIREWALL_SCRIPT}`, { silent: true });\n\n io.writeFile(DOCKER_FIREWALL_UNIT, buildDockerFirewallUnit());\n io.exec(\"systemctl daemon-reload\", { silent: true });\n io.exec(\"systemctl enable --now trafic-docker-firewall.service\", {\n silent: true,\n });\n\n success(`Tool ports restricted to Docker networks: ${toolPorts.join(\", \")}`);\n info(\"These live in DOCKER-USER, not UFW — `ufw status` will not show them\");\n}\n","import { getIdleProjects, setProjectStatus } from \"../utils/db.js\";\nimport { stopProject, getProjectInfo, loadProjectList } from \"../utils/ddev.js\";\nimport { parseDuration } from \"../utils/config.js\";\nimport { loadProjectConfig, shouldNeverStop, getIdleTimeoutMs } from \"../utils/project-config.js\";\nimport type { AgentConfig } from \"../types.js\";\n\n/**\n * Stop idle projects that haven't been accessed recently\n * Respects per-project idle_timeout settings from .ddev/config.trafic.yaml\n */\nexport async function stopIdleProjects(config: AgentConfig): Promise<void> {\n const globalThresholdMs = parseDuration(config.idleTimeout);\n\n // Get all projects to check their individual configs\n const projects = loadProjectList(config.projectListPath);\n\n // Get projects idle according to global threshold\n const idleProjects = getIdleProjects(globalThresholdMs);\n\n for (const project of idleProjects) {\n const projectDir = projects.get(project.name);\n\n // Load per-project config if available\n if (projectDir) {\n const projectConfig = loadProjectConfig(projectDir);\n\n // Skip if project is configured to never stop\n if (shouldNeverStop(projectConfig)) {\n continue;\n }\n\n // Check project-specific idle timeout\n const projectThresholdMs = getIdleTimeoutMs(projectConfig);\n if (projectThresholdMs !== undefined) {\n const idleDuration = Date.now() - project.lastAccess;\n if (idleDuration < projectThresholdMs) {\n // Not idle according to project-specific timeout\n continue;\n }\n }\n }\n\n // Double-check project is actually running via DDEV\n const info = await getProjectInfo(project.name);\n if (info?.status !== \"running\") {\n // Already stopped, just update our record\n setProjectStatus(project.name, \"stopped\");\n continue;\n }\n\n console.log(`Stopping idle project: ${project.name}`);\n const success = await stopProject(project.name);\n\n if (success) {\n setProjectStatus(project.name, \"stopped\");\n console.log(`Stopped: ${project.name}`);\n } else {\n console.error(`Failed to stop: ${project.name}`);\n }\n }\n}\n\n/**\n * Start the idle check scheduler\n */\n/** An idle sweep that throws must not take the scheduler down with it. */\nfunction reportSweepError(error: unknown): void {\n console.error(\"Idle sweep failed:\", error);\n}\n\nexport function startIdleScheduler(config: AgentConfig): NodeJS.Timeout {\n const intervalMs = parseDuration(config.idleCheckInterval);\n\n console.log(\n `Idle scheduler started: checking every ${config.idleCheckInterval}, timeout ${config.idleTimeout}`,\n );\n\n // Run immediately on start\n void stopIdleProjects(config).catch(reportSweepError);\n\n // Then run on interval\n return setInterval(() => {\n void stopIdleProjects(config).catch(reportSweepError);\n }, intervalMs);\n}\n","import { nodeIo, type SetupIo } from \"./io.js\";\nimport { step, success, info, warn } from \"./steps.js\";\n\n/**\n * Write /etc/sudoers.d/trafic-ddev allowing the ddev user to run\n * ddev-hostname without a password. Idempotent — safe to call multiple times.\n */\nexport function configureDdevSudoers(io: SetupIo = nodeIo): void {\n const sudoersContent = `# Trafic: allow ddev user to run ddev-hostname without a password.\n# ddev-hostname manages /etc/hosts and always calls sudo internally.\nddev ALL=(ALL) NOPASSWD: /usr/bin/ddev-hostname\n`;\n io.writeFile(\"/etc/sudoers.d/trafic-ddev\", sudoersContent);\n io.exec(\"chmod 440 /etc/sudoers.d/trafic-ddev\", { silent: true });\n success(\"Configured passwordless sudo for ddev-hostname\");\n}\n\n/**\n * Create the ddev user with proper permissions\n */\nexport function createDdevUser(io: SetupIo = nodeIo): void {\n step(\"Create ddev user\");\n\n // Check if user exists\n const userExists = io.execSilent(\"id -u ddev\");\n if (userExists) {\n info(\"User 'ddev' already exists\");\n } else {\n io.exec(\"useradd -m -s /bin/bash ddev\");\n success(\"Created user 'ddev'\");\n }\n\n // Add to docker group (will be created by Docker install)\n io.exec(\"usermod -aG docker ddev 2>/dev/null || true\", { silent: true });\n\n // Create www directory\n io.exec(\"mkdir -p /home/ddev/www\");\n io.exec(\"chown ddev:ddev /home/ddev/www\");\n io.exec(\"chmod 750 /home/ddev/www\");\n success(\"Created /home/ddev/www\");\n\n // Create .ssh directory if needed\n io.exec(\"mkdir -p /home/ddev/.ssh\");\n io.exec(\"chmod 700 /home/ddev/.ssh\");\n io.exec(\"chown ddev:ddev /home/ddev/.ssh\");\n\n // Allow ddev user to run ddev-hostname without a password.\n // ddev-hostname manages /etc/hosts entries and always calls sudo internally,\n // even when DDEV_NONINTERACTIVE=true. Without this rule, any ddev command\n // that touches hostname resolution (start, restart, poweroff + start) will\n // fail with \"sudo: a terminal is required to read the password\".\n configureDdevSudoers(io);\n success(\"User ddev configured\");\n}\n\n/**\n * Setup authorized_keys for ddev user\n */\nexport function setupAuthorizedKeys(\n publicKey?: string,\n io: SetupIo = nodeIo,\n): void {\n if (!publicKey) {\n warn(\"No SSH public key provided, skipping authorized_keys setup\");\n return;\n }\n\n step(\"Setup SSH authorized_keys\");\n\n const authKeysPath = \"/home/ddev/.ssh/authorized_keys\";\n io.exec(`touch ${authKeysPath}`);\n io.exec(`chmod 600 ${authKeysPath}`);\n io.exec(`chown ddev:ddev ${authKeysPath}`);\n\n // Check if key already exists\n const existing = io.execSilent(`cat ${authKeysPath}`);\n if (existing.includes(publicKey.trim())) {\n info(\"SSH key already authorized\");\n } else {\n io.exec(`echo '${publicKey}' >> ${authKeysPath}`);\n success(\"Added SSH public key\");\n }\n}\n","import { nodeIo, type SetupIo } from \"./io.js\";\nimport { step, success, info, warn } from \"./steps.js\";\n\n/**\n * Install Docker using the official script\n */\nexport function installDocker(io: SetupIo = nodeIo): void {\n step(\"Install Docker\");\n\n if (io.commandExists(\"docker\")) {\n const version = io.exec(\"docker --version\", { silent: true });\n info(`Docker already installed: ${version?.trim() ?? \"unknown version\"}`);\n return;\n }\n\n // Install Docker using official script\n info(\"Downloading and running Docker install script...\");\n io.exec(\"curl -fsSL https://get.docker.com -o /tmp/get-docker.sh\");\n io.exec(\"sh /tmp/get-docker.sh\", { silent: true });\n io.exec(\"rm /tmp/get-docker.sh\");\n\n // Enable and start Docker\n io.exec(\"systemctl enable docker\", { silent: true });\n io.exec(\"systemctl start docker\", { silent: true });\n\n success(\"Docker installed and started\");\n\n // Add ddev user to docker group\n io.exec(\"usermod -aG docker ddev\", { silent: true });\n success(\"Added ddev user to docker group\");\n}\n\n/** Docker daemon config file */\nexport const DAEMON_CONFIG = \"/etc/docker/daemon.json\";\n\n/**\n * Address pools for the networks Docker creates.\n *\n * Every DDEV project gets its own network. Docker's built-in pools hold only\n * about 31 of them (15 × /16 in 172.17–31.x, 16 × /20 in 192.168.x), so a\n * server with ~30 projects fails with \"all predefined address pools have\n * been fully subnetted\". /24 subnets in 172.16.0.0/12 give 4096 networks,\n * and keep every network inside the range the firewall rules already allow\n * (hardening.ts, docker-firewall.ts) — the built-in 192.168.x pools are not.\n */\nexport const DOCKER_ADDRESS_POOLS = [{ base: \"172.16.0.0/12\", size: 24 }];\n\n/**\n * Add the address pools to an existing daemon config.\n *\n * Returns true when the config was changed. Leaves a config that already\n * sets pools alone (an operator choice), and one it cannot parse.\n */\nexport function addDockerAddressPools(io: SetupIo = nodeIo): boolean {\n let config: Record<string, unknown> = {};\n\n if (io.fileExists(DAEMON_CONFIG)) {\n try {\n config = JSON.parse(io.readFile(DAEMON_CONFIG));\n } catch {\n warn(\"Could not parse existing Docker config, skipping\");\n return false;\n }\n }\n\n if (\"default-address-pools\" in config) {\n return false;\n }\n\n io.exec(\"mkdir -p /etc/docker\");\n io.writeFile(\n DAEMON_CONFIG,\n JSON.stringify({ ...config, \"default-address-pools\": DOCKER_ADDRESS_POOLS }, null, 2),\n );\n return true;\n}\n\n/**\n * Configure Docker for production use\n */\nexport function configureDocker(io: SetupIo = nodeIo): void {\n step(\"Configure Docker\");\n\n // Create daemon.json with production settings\n const daemonConfig = {\n \"log-driver\": \"json-file\",\n \"log-opts\": {\n \"max-size\": \"10m\",\n \"max-file\": \"3\",\n },\n \"storage-driver\": \"overlay2\",\n \"live-restore\": true,\n \"default-address-pools\": DOCKER_ADDRESS_POOLS,\n };\n\n io.exec(\"mkdir -p /etc/docker\");\n\n const configPath = DAEMON_CONFIG;\n\n // Check if config already exists\n if (io.fileExists(configPath)) {\n try {\n const existing = JSON.parse(io.readFile(configPath));\n // Merge with existing config\n const merged = { ...existing, ...daemonConfig };\n io.writeFile(configPath, JSON.stringify(merged, null, 2));\n info(\"Merged with existing Docker config\");\n } catch {\n warn(\"Could not parse existing Docker config, skipping\");\n return;\n }\n } else {\n io.writeFile(configPath, JSON.stringify(daemonConfig, null, 2));\n }\n\n // A reload does not apply default-address-pools, so restart.\n // live-restore keeps running containers up.\n io.exec(\"systemctl restart docker\");\n success(\"Docker configured with production settings\");\n}\n\n/**\n * Setup Docker system prune cron job\n */\nexport function setupDockerPrune(io: SetupIo = nodeIo): void {\n step(\"Setup Docker cleanup cron\");\n\n const cronContent = `# Trafic: Clean up Docker resources weekly\n0 3 * * 0 root docker system prune -af --volumes 2>&1 | logger -t docker-prune\n`;\n\n io.writeFile(\"/etc/cron.d/trafic-docker-prune\", cronContent);\n io.exec(\"chmod 644 /etc/cron.d/trafic-docker-prune\");\n\n success(\"Docker prune scheduled weekly at 3am on Sundays\");\n}\n","import { nodeIo, type SetupIo } from \"./io.js\";\nimport { step, success, info } from \"./steps.js\";\n\n/** Node.js major version required by the agent */\nconst NODE_MAJOR = 24;\n\n/** Proxies in front of the agent when --trusted-proxy-hops is not given */\nexport const DEFAULT_TRUSTED_PROXY_HOPS = 1;\n\n/** NodeSource keyring path */\nconst NODESOURCE_KEYRING = \"/etc/apt/keyrings/nodesource.gpg\";\n\n/**\n * Add the NodeSource apt repository and GPG key.\n * Idempotent: safe to call even if the repository is already configured.\n *\n * nodejs.org points at NodeSource for apt installs. The Ubuntu 24.04\n * `nodejs` package is 18.x, far below what the agent needs.\n */\nexport function addNodeSourceAptRepo(io: SetupIo = nodeIo): void {\n io.exec(\"install -m 0755 -d /etc/apt/keyrings\", { silent: true });\n // Download and dearmor as separate commands: piping curl into gpg into tee\n // would report only tee's exit code and hide a failed key download, which\n // then surfaces as a confusing GPG error on the next apt-get update\n io.exec(\n \"curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o /tmp/trafic-nodesource.key\",\n { silent: true },\n );\n io.exec(\n \"gpg --batch --yes --dearmor -o /tmp/trafic-nodesource.gpg /tmp/trafic-nodesource.key\",\n { silent: true },\n );\n io.exec(`install -m 0644 /tmp/trafic-nodesource.gpg ${NODESOURCE_KEYRING}`, {\n silent: true,\n });\n io.exec(\"rm -f /tmp/trafic-nodesource.key /tmp/trafic-nodesource.gpg\", {\n silent: true,\n });\n io.exec(\n `echo \"deb [signed-by=${NODESOURCE_KEYRING}] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main\" | tee /etc/apt/sources.list.d/nodesource.list > /dev/null`,\n { silent: true },\n );\n}\n\n/**\n * Install Node.js from the NodeSource apt repository.\n *\n * apt keeps node and npm in /usr/bin — on root's PATH, so `which\n * trafic-agent` resolves for the systemd unit — and unattended-upgrades\n * patches them, which a version manager install never gets.\n */\nexport function installNode(io: SetupIo = nodeIo): void {\n step(\"Install Node.js\");\n\n // Check if Node is already installed\n if (io.commandExists(\"node\")) {\n const version = io.exec(\"node --version\", { silent: true });\n info(`Node.js already installed: ${version?.trim()}`);\n return;\n }\n\n info(\"Adding NodeSource apt repository...\");\n addNodeSourceAptRepo(io);\n\n info(\"Installing Node.js...\");\n io.exec(\"apt-get update -qq\", { silent: true });\n io.exec(\n \"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y nodejs\",\n { silent: true },\n );\n\n success(`Node.js ${NODE_MAJOR} installed from apt`);\n}\n\n/**\n * Install the Trafic agent\n */\nexport function installAgent(io: SetupIo = nodeIo): void {\n step(\"Install Trafic agent\");\n\n // Install the agent globally\n io.exec(\"npm install -g @studiometa/trafic-agent\");\n\n success(\"Trafic agent installed\");\n}\n\n/**\n * Create the agent configuration\n */\nexport function createAgentConfig(\n options: {\n tld: string;\n trustedProxyHops?: number;\n dnsProvider?: string;\n dnsEnv?: Record<string, string>;\n },\n io: SetupIo = nodeIo,\n): void {\n const {\n tld,\n trustedProxyHops = DEFAULT_TRUSTED_PROXY_HOPS,\n dnsProvider,\n dnsEnv = {},\n } = options;\n step(\"Create agent configuration\");\n\n // Create directories\n io.exec(\"mkdir -p /etc/trafic\");\n io.exec(\"mkdir -p /var/lib/trafic\");\n io.exec(\"chown ddev:ddev /var/lib/trafic\");\n\n const configPath = \"/etc/trafic/config.toml\";\n\n const tlsSection = buildTlsSection(dnsProvider, dnsEnv);\n\n if (io.fileExists(configPath)) {\n info(\"Configuration already exists at /etc/trafic/config.toml — skipping\");\n info(\"Edit this file to configure authentication\");\n\n if (tlsSection) {\n // Never rewrite an existing config — print what to add instead, or the\n // DNS credentials would silently go nowhere. The values are masked:\n // this output goes to a terminal and to the CI job log of whoever ran\n // `trafic setup`, and a provider token does not belong in either.\n info(\"Add this to /etc/trafic/config.toml, then run `trafic-agent upgrade`:\");\n for (const line of buildTlsSection(dnsProvider, dnsEnv, true).trim().split(\"\\n\")) {\n info(` ${line}`);\n }\n info(\"Replace each REPLACE_ME with the value you passed to --dns-env.\");\n }\n\n return;\n }\n\n const config = `# Trafic agent configuration\n# Generated by trafic setup\n\n# Required: TLD for DDEV projects\ntld = \"${tld}\"\n\n# Agent HTTP server port\nport = 9876\n\n# Database and paths\ndb_path = \"/var/lib/trafic/db.sqlite\"\nproject_list_path = \"/home/ddev/.ddev/project_list.yaml\"\nprojects_dir = \"/home/ddev/www\"\n\n# Scale-to-zero settings\nidle_timeout = \"4h\"\nidle_check_interval = \"5m\"\n\n# Authentication\n[auth]\ndefault_policy = \"basic\"\nallowed_ips = []\ntokens = []\nbasic_auth = []\nrules = []\n\n# Proxies in front of the agent, counted so the client address can be read\n# from X-Forwarded-For. 1 is ddev-router/Traefik alone. Raise it by one for\n# every extra proxy — a CDN or load balancer ahead of Traefik makes it 2.\n# Too low and allowed_ips matches a proxy instead of the client; too high and\n# it can be spoofed by a client-supplied header.\ntrusted_proxy_hops = ${trustedProxyHops}\n${tlsSection}`;\n\n io.writeFile(configPath, config);\n io.exec(\"chmod 640 /etc/trafic/config.toml\");\n io.exec(\"chown root:ddev /etc/trafic/config.toml\");\n\n success(\"Configuration created at /etc/trafic/config.toml\");\n info(\"Edit this file to configure authentication\");\n}\n\n/**\n * Build the [tls] section of the generated config.\n *\n * Empty without a DNS provider: per-host certificates stay the default, and\n * an empty section would only invite a half-filled one.\n */\nfunction buildTlsSection(\n dnsProvider: string | undefined,\n dnsEnv: Record<string, string>,\n redact = false,\n): string {\n if (!dnsProvider) {\n return \"\";\n }\n\n const env = Object.entries(dnsEnv)\n .map(([key, value]) => `${key} = \"${redact ? \"REPLACE_ME\" : escapeTomlString(value)}\"`)\n .join(\"\\n\");\n\n return `\n# Wildcard certificate (DNS-01). One *.<tld> certificate replaces the\n# per-hostname ones, so previews stop consuming the Let's Encrypt quota.\n[tls]\ndns_provider = \"${escapeTomlString(dnsProvider)}\"\n# ca_server = \"https://acme-staging-v02.api.letsencrypt.org/directory\"\n\n# Credentials the lego provider reads. Passed to the ddev-router container.\n[tls.dns_env]\n${env}\n`;\n}\n\n/** Escape a value for a basic (double-quoted) TOML string. */\nfunction escapeTomlString(value: string): string {\n return value\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, \"\\\\n\")\n .replace(/\\r/g, \"\\\\r\")\n .replace(/\\t/g, \"\\\\t\");\n}\n\n/**\n * Create systemd service for the agent\n */\nexport function createSystemdService(io: SetupIo = nodeIo): void {\n step(\"Create systemd service\");\n\n // Find the trafic-agent binary path\n const agentPath = io.exec(\"which trafic-agent\", { silent: true })?.trim() || \"/usr/bin/trafic-agent\";\n\n const serviceContent = `[Unit]\nDescription=Trafic Agent - DDEV preview environment manager\nAfter=network.target docker.service\nRequires=docker.service\n\n[Service]\nType=simple\nUser=ddev\nGroup=ddev\nExecStart=${agentPath} start --config /etc/trafic/config.toml\nRestart=always\nRestartSec=5\nStandardOutput=journal\nStandardError=journal\nSyslogIdentifier=trafic-agent\n\n# Environment\nEnvironment=NODE_ENV=production\n# Skip ddev-hostname /etc/hosts management — on a server, hostnames resolve\n# to the public IP (not localhost), which would trigger ddev-hostname on every\n# ddev start. DDEV_NONINTERACTIVE=true skips that entirely.\nEnvironment=DDEV_NONINTERACTIVE=true\n\n# Security hardening\nNoNewPrivileges=true\nProtectSystem=strict\nProtectHome=read-only\n# ddev start writes across its own home: the project directory\n# (.ddev/.webimageBuild), buildx state in .docker, mutagen data. Listing\n# individual paths is whack-a-mole across DDEV versions, so grant the home\n# and leave the rest of /home read-only.\nReadWritePaths=/var/lib/trafic /home/ddev\nPrivateTmp=true\n\n[Install]\nWantedBy=multi-user.target\n`;\n\n io.writeFile(\"/etc/systemd/system/trafic-agent.service\", serviceContent);\n io.exec(\"chmod 644 /etc/systemd/system/trafic-agent.service\");\n\n // Reload systemd and enable service\n io.exec(\"systemctl daemon-reload\");\n io.exec(\"systemctl enable trafic-agent\");\n io.exec(\"systemctl start trafic-agent\");\n\n success(\"Trafic agent service started\");\n info(\"View logs: journalctl -u trafic-agent -f\");\n}\n","import { nodeIo, type SetupIo } from \"./io.js\";\nimport { step, success, info, warn } from \"./steps.js\";\n\n/** The sshd drop-in written by `setup`, and the only file hardening owns. */\nexport const SSHD_DROPIN = \"/etc/ssh/sshd_config.d/trafic.conf\";\n\n/**\n * Resolve the users listed in AllowUsers.\n *\n * The user who invoked sudo is always included. Without them, the\n * AllowUsers directive locks them out on their next connection, and\n * reloading sshd keeps the current session alive so nothing looks wrong\n * until it is too late. root needs no entry — it is always listed.\n */\nexport function resolveAllowedUsers(\n allowedUsers: string[],\n sudoUser: string | undefined,\n): string[] {\n const users = allowedUsers.map((user) => user.trim()).filter(Boolean);\n\n if (sudoUser && sudoUser !== \"root\" && !users.includes(sudoUser)) {\n users.push(sudoUser);\n }\n\n return users;\n}\n\n/**\n * Configure SSH hardening\n */\nexport function hardenSsh(\n options: { allowedUsers?: string[]; noRootSsh?: boolean } = {},\n io: SetupIo = nodeIo,\n): void {\n step(\"Harden SSH configuration\");\n\n const { allowedUsers = [\"ddev\"], noRootSsh = false } = options;\n const users = resolveAllowedUsers(allowedUsers, io.env.SUDO_USER);\n\n // Dropping root from AllowUsers with nobody left would lock everyone out,\n // and sshd would reject the config anyway\n if (noRootSsh && users.length === 0) {\n throw new Error(\n \"Refusing to disable root SSH with an empty user list — nobody could log in\",\n );\n }\n\n if (noRootSsh && !io.env.SUDO_USER) {\n warn(\n \"Disabling root SSH without a sudo user: make sure ddev has an authorized key,\",\n );\n warn(\" or the only way back in is the provider's rescue mode\");\n }\n\n const allowList = noRootSsh ? users : [\"root\", ...users];\n\n const rootLogin = noRootSsh\n ? `# Root login disabled (--no-root-ssh) — recovery is via rescue mode\nPermitRootLogin no`\n : `# Allow root login via SSH key only (no password) — required for server management\nPermitRootLogin prohibit-password`;\n\n const sshConfig = `# Trafic SSH hardening\n# Generated by trafic setup\n\n${rootLogin}\n\n# Disable password authentication (keys only)\nPasswordAuthentication no\nPubkeyAuthentication yes\n\n# Limit authentication attempts\nMaxAuthTries 3\n\n# Idle timeout (5 minutes)\nClientAliveInterval 300\nClientAliveCountMax 2\n\n# Allowed users\nAllowUsers ${allowList.join(\" \")}\n\n# Disable X11 forwarding\nX11Forwarding no\n\n# Disable TCP forwarding\nAllowTcpForwarding no\n\n# Pin ciphers and MACs to strong AEAD and encrypt-then-MAC algorithms.\n# Key exchange is left to OpenSSH's own defaults, which are kept current and\n# include the post-quantum hybrids a fixed list here would silently exclude.\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com\nMACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com\n`;\n\n io.writeFile(SSHD_DROPIN, sshConfig);\n io.exec(`chmod 644 ${SSHD_DROPIN}`);\n\n // Test config before restarting\n const testResult = io.exec(\"sshd -t 2>&1 || echo 'error'\", { silent: true });\n if (testResult?.includes(\"error\")) {\n warn(\"SSH config test failed, reverting...\");\n io.exec(`rm ${SSHD_DROPIN}`);\n return;\n }\n\n io.exec(\"systemctl reload ssh || systemctl reload sshd\");\n success(\n noRootSsh\n ? \"SSH hardened: password auth disabled, root login disabled\"\n : \"SSH hardened: password auth disabled, root key-only login retained\",\n );\n info(`Allowed users: ${allowList.join(\", \")}`);\n}\n\n/**\n * Drop the KexAlgorithms line from the sshd drop-in.\n *\n * `setup` used to pin key exchange to two classical algorithms. A\n * KexAlgorithms line with no `+`, `-` or `^` prefix replaces OpenSSH's whole\n * default list, so the post-quantum hybrids it enables by itself\n * (mlkem768x25519-sha256, sntrup761x25519-sha512) were excluded, and an\n * OpenSSH 10.1+ client warns on every connection that the session is not\n * using a post-quantum key exchange. Removing the line hands key exchange\n * back to OpenSSH's maintained defaults; Ciphers and MACs stay pinned.\n *\n * Returns whether the file was changed. Idempotent: a drop-in without the\n * line, or no drop-in at all, is left alone.\n */\nexport function removeSshKexAlgorithms(io: SetupIo = nodeIo): boolean {\n if (!io.fileExists(SSHD_DROPIN)) {\n return false;\n }\n\n const original = io.readFile(SSHD_DROPIN);\n const lines = original.split(\"\\n\");\n const kept = lines.filter((line) => !line.startsWith(\"KexAlgorithms\"));\n\n if (kept.length === lines.length) {\n return false;\n }\n\n io.writeFile(SSHD_DROPIN, kept.join(\"\\n\"));\n\n const testResult = io.exec(\"sshd -t 2>&1 || echo 'error'\", { silent: true });\n if (testResult?.includes(\"error\")) {\n warn(\"SSH config test failed after removing KexAlgorithms, reverting...\");\n io.writeFile(SSHD_DROPIN, original);\n return false;\n }\n\n io.exec(\"systemctl reload ssh || systemctl reload sshd\");\n return true;\n}\n\n/**\n * Configure UFW firewall\n */\nexport function configureFirewall(io: SetupIo = nodeIo): void {\n step(\"Configure firewall (UFW)\");\n\n // Install UFW if not present\n if (!io.commandExists(\"ufw\")) {\n io.exec(\"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ufw\", { silent: true });\n }\n\n // Reset and configure\n io.exec(\"ufw --force reset\", { silent: true });\n io.exec(\"ufw default deny incoming\");\n io.exec(\"ufw default allow outgoing\");\n io.exec(\"ufw allow 22/tcp comment 'SSH'\");\n io.exec(\"ufw allow 80/tcp comment 'HTTP'\");\n io.exec(\"ufw allow 443/tcp comment 'HTTPS'\");\n // Allow trafic-agent port from Docker bridge subnets so ddev-router can\n // reach the forward-auth endpoint. Docker typically uses 172.16.0.0/12.\n io.exec(\"ufw allow from 172.16.0.0/12 to any port 9876 comment 'trafic-agent from Docker'\");\n\n // Enable firewall\n io.exec(\"ufw --force enable\");\n\n success(\"Firewall enabled: SSH (22), HTTP (80), HTTPS (443), trafic-agent (9876 from Docker) allowed\");\n\n // Say it plainly rather than let `ufw status` imply a guarantee it cannot\n // make: Docker's rules run before UFW's, so published ports stay reachable\n info(\"UFW does not cover ports Docker publishes — DDEV's tool ports (Mailpit, xhgui)\");\n info(\"are protected by forward auth, not the firewall. For network-level closure use a\");\n info(\"firewall in front of the server; see the agent readme, 'Network exposure'\");\n}\n\n/** The unattended-upgrades config written by `setup`, and owned by Trafic. */\nexport const UNATTENDED_UPGRADES_CONFIG = \"/etc/apt/apt.conf.d/50unattended-upgrades\";\n\n/**\n * The Allowed-Origins entry covering Docker Engine.\n *\n * `origin=Docker` is the `Origin:` field of the Release file served by\n * download.docker.com (its `Label:` is `Docker CE`), and that repository's\n * dist is the Ubuntu codename, so `codename=${distro_codename}` pins the\n * entry to the release the server runs.\n */\nexport const DOCKER_ORIGIN_LINE = '\"origin=Docker,codename=${distro_codename}\"';\n\n/**\n * Build the contents of /etc/apt/apt.conf.d/50unattended-upgrades.\n *\n * Pure, so `setup` and migration 0014 write the exact same file.\n */\nexport function buildUnattendedUpgradesConfig(): string {\n return `// Trafic: Automatic security updates\nUnattended-Upgrade::Allowed-Origins {\n \"\\${distro_id}:\\${distro_codename}\";\n \"\\${distro_id}:\\${distro_codename}-security\";\n \"\\${distro_id}ESMApps:\\${distro_codename}-apps-security\";\n \"\\${distro_id}ESM:\\${distro_codename}-infra-security\";\n // Docker Engine ships from download.docker.com, outside Ubuntu's own\n // pockets, so without this entry its security releases were never applied.\n // DDEV is deliberately left out: a DDEV major landing unattended can break\n // running previews, so DDEV is updated by \\`trafic-agent upgrade\\` instead,\n // when an operator is there to see it.\n ${DOCKER_ORIGIN_LINE};\n};\n\n// Do not automatically reboot\nUnattended-Upgrade::Automatic-Reboot \"false\";\n\n// Remove unused dependencies\nUnattended-Upgrade::Remove-Unused-Dependencies \"true\";\n\n// Email notifications (optional)\n// Unattended-Upgrade::Mail \"root\";\n`;\n}\n\n/**\n * Add the Docker origin to an existing unattended-upgrades config.\n *\n * Returns whether the file was changed. Idempotent: a server with no such\n * file was set up without hardening and is left alone, and a config that\n * already names the Docker origin is not rewritten. The whole file is\n * regenerated rather than patched because Trafic generates and owns it — the\n * header comment says so.\n *\n * No service restart is needed: unattended-upgrades reads the config on each\n * daily run.\n */\nexport function addDockerOriginToUnattendedUpgrades(io: SetupIo = nodeIo): boolean {\n if (!io.fileExists(UNATTENDED_UPGRADES_CONFIG)) {\n return false;\n }\n\n if (io.readFile(UNATTENDED_UPGRADES_CONFIG).includes(DOCKER_ORIGIN_LINE)) {\n return false;\n }\n\n io.writeFile(UNATTENDED_UPGRADES_CONFIG, buildUnattendedUpgradesConfig());\n return true;\n}\n\n/**\n * Configure automatic security updates\n */\nexport function configureUnattendedUpgrades(io: SetupIo = nodeIo): void {\n step(\"Configure automatic security updates\");\n\n // Install unattended-upgrades\n io.exec(\n \"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y unattended-upgrades apt-listchanges\",\n { silent: true },\n );\n\n // Ubuntu's release and security pockets, plus Docker Engine\n io.writeFile(UNATTENDED_UPGRADES_CONFIG, buildUnattendedUpgradesConfig());\n\n // Enable automatic updates\n const autoConfig = `APT::Periodic::Update-Package-Lists \"1\";\nAPT::Periodic::Download-Upgradeable-Packages \"1\";\nAPT::Periodic::AutocleanInterval \"7\";\nAPT::Periodic::Unattended-Upgrade \"1\";\n`;\n\n io.writeFile(\"/etc/apt/apt.conf.d/20auto-upgrades\", autoConfig);\n\n // Enable the service\n io.exec(\"systemctl enable unattended-upgrades\");\n io.exec(\"systemctl start unattended-upgrades\");\n\n success(\"Automatic security updates enabled\");\n}\n\n/**\n * Configure Fail2ban\n */\nexport function configureFail2ban(io: SetupIo = nodeIo): void {\n step(\"Configure Fail2ban\");\n\n // Install fail2ban\n if (!io.commandExists(\"fail2ban-client\")) {\n io.exec(\"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y fail2ban\", { silent: true });\n }\n\n // Configure jail\n const jailConfig = `# Trafic: Fail2ban configuration\n[DEFAULT]\nbantime = 3600\nfindtime = 600\nmaxretry = 5\nbackend = auto\n\n[sshd]\nenabled = true\nport = ssh\nfilter = sshd\nlogpath = /var/log/auth.log\nmaxretry = 3\nbantime = 7200\n`;\n\n io.writeFile(\"/etc/fail2ban/jail.local\", jailConfig);\n io.exec(\"chmod 644 /etc/fail2ban/jail.local\");\n\n // Restart fail2ban\n io.exec(\"systemctl enable fail2ban\");\n io.exec(\"systemctl restart fail2ban\");\n\n success(\"Fail2ban enabled: SSH brute force protection active\");\n}\n\n/**\n * Configure system limits\n */\nexport function configureSystemLimits(io: SetupIo = nodeIo): void {\n step(\"Configure system limits\");\n\n const limitsConfig = `# Trafic: System limits for ddev user\nddev soft nofile 65536\nddev hard nofile 65536\nddev soft nproc 4096\nddev hard nproc 4096\n`;\n\n io.writeFile(\"/etc/security/limits.d/trafic.conf\", limitsConfig);\n io.exec(\"chmod 644 /etc/security/limits.d/trafic.conf\");\n\n success(\"System limits configured for ddev user\");\n}\n\n/**\n * Set proper file permissions\n */\nexport function configureFilePermissions(io: SetupIo = nodeIo): void {\n step(\"Configure file permissions\");\n\n // Create directories\n io.exec(\"mkdir -p /etc/trafic\");\n io.exec(\"mkdir -p /var/lib/trafic\");\n io.exec(\"mkdir -p /var/backups/trafic\");\n\n // Set permissions\n io.exec(\"chmod 755 /etc/trafic\");\n io.exec(\"chmod 700 /var/lib/trafic\");\n io.exec(\"chmod 700 /var/backups/trafic\");\n io.exec(\"chown root:ddev /etc/trafic\");\n io.exec(\"chown ddev:ddev /var/lib/trafic\");\n io.exec(\"chown ddev:ddev /var/backups/trafic\");\n\n // Config file (if exists)\n if (io.fileExists(\"/etc/trafic/config.toml\")) {\n io.exec(\"chmod 640 /etc/trafic/config.toml\");\n io.exec(\"chown root:ddev /etc/trafic/config.toml\");\n }\n\n success(\"File permissions configured\");\n}\n\n/**\n * Run all hardening steps\n */\nexport function hardenServer(\n options: { sshUsers?: string[]; noRootSsh?: boolean } = {},\n io: SetupIo = nodeIo,\n): void {\n hardenSsh(\n { allowedUsers: options.sshUsers, noRootSsh: options.noRootSsh },\n io,\n );\n configureFirewall(io);\n configureUnattendedUpgrades(io);\n configureFail2ban(io);\n configureSystemLimits(io);\n configureFilePermissions(io);\n}\n","import type { AuditCheck } from \"./types.js\";\nimport { nodeIo, type SetupIo } from \"./io.js\";\nimport { loadConfig } from \"../utils/config.js\";\nimport { ROUTER_COMPOSE_OVERRIDE } from \"./ddev.js\";\nimport type { AgentConfig } from \"../types.js\";\n\n/**\n * Check if a service is active\n */\nfunction isServiceActive(service: string, io: SetupIo): boolean {\n return io.execSilent(`systemctl is-active ${service}`) === \"active\";\n}\n\n/** Read a file, or \"\" when it does not exist. */\nfunction readOrEmpty(path: string, io: SetupIo): string {\n return io.fileExists(path) ? io.readFile(path) : \"\";\n}\n\n/**\n * Audit SSH configuration\n */\nexport function auditSsh(io: SetupIo = nodeIo): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Check root login\n const config =\n readOrEmpty(\"/etc/ssh/sshd_config\", io) +\n \"\\n\" +\n readOrEmpty(\"/etc/ssh/sshd_config.d/trafic.conf\", io);\n\n const rootLoginDisabled =\n config.includes(\"PermitRootLogin no\") ||\n config.includes(\"PermitRootLogin prohibit-password\");\n checks.push({\n name: \"SSH root login\",\n status: rootLoginDisabled ? \"pass\" : \"fail\",\n message: rootLoginDisabled\n ? \"Root login disabled\"\n : \"Root login may be enabled\",\n fix: \"Add 'PermitRootLogin no' to /etc/ssh/sshd_config.d/trafic.conf\",\n });\n\n const passwordAuthDisabled = config.includes(\"PasswordAuthentication no\");\n checks.push({\n name: \"SSH password auth\",\n status: passwordAuthDisabled ? \"pass\" : \"warn\",\n message: passwordAuthDisabled\n ? \"Password authentication disabled\"\n : \"Password authentication may be enabled\",\n fix: \"Add 'PasswordAuthentication no' to /etc/ssh/sshd_config.d/trafic.conf\",\n });\n\n return checks;\n}\n\n/**\n * Audit firewall configuration\n */\nexport function auditFirewall(io: SetupIo = nodeIo): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n const ufwStatus = io.execSilent(\"ufw status\");\n const isActive = ufwStatus.includes(\"Status: active\");\n\n checks.push({\n name: \"Firewall (UFW)\",\n status: isActive ? \"pass\" : \"fail\",\n message: isActive ? \"UFW is active\" : \"UFW is not active\",\n fix: \"Run: ufw enable\",\n });\n\n if (isActive) {\n const allows22 = ufwStatus.includes(\"22/tcp\");\n const allows80 = ufwStatus.includes(\"80/tcp\");\n const allows443 = ufwStatus.includes(\"443/tcp\");\n\n if (allows22 && allows80 && allows443) {\n checks.push({\n name: \"Firewall ports\",\n status: \"pass\",\n message: \"Required ports (22, 80, 443) are open\",\n });\n } else {\n checks.push({\n name: \"Firewall ports\",\n status: \"warn\",\n message: \"Some required ports may be blocked\",\n fix: \"Run: ufw allow 22/tcp && ufw allow 80/tcp && ufw allow 443/tcp\",\n });\n }\n }\n\n return checks;\n}\n\n/**\n * Audit security services\n */\nexport function auditServices(io: SetupIo = nodeIo): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Fail2ban\n const fail2banActive = isServiceActive(\"fail2ban\", io);\n checks.push({\n name: \"Fail2ban\",\n status: fail2banActive ? \"pass\" : \"warn\",\n message: fail2banActive ? \"Fail2ban is active\" : \"Fail2ban is not running\",\n fix: \"Run: apt install fail2ban && systemctl enable fail2ban --now\",\n });\n\n // Unattended upgrades\n const unattendedActive = isServiceActive(\"unattended-upgrades\", io);\n checks.push({\n name: \"Automatic updates\",\n status: unattendedActive ? \"pass\" : \"warn\",\n message: unattendedActive\n ? \"Unattended upgrades enabled\"\n : \"Automatic updates not configured\",\n fix: \"Run: apt install unattended-upgrades && dpkg-reconfigure unattended-upgrades\",\n });\n\n // Trafic agent\n const agentActive = isServiceActive(\"trafic-agent\", io);\n checks.push({\n name: \"Trafic agent\",\n status: agentActive ? \"pass\" : \"fail\",\n message: agentActive\n ? \"Trafic agent is running\"\n : \"Trafic agent is not running\",\n fix: \"Run: systemctl start trafic-agent\",\n });\n\n return checks;\n}\n\n/**\n * Audit Docker\n */\nexport function auditDocker(io: SetupIo = nodeIo): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Docker running\n const dockerActive = isServiceActive(\"docker\", io);\n checks.push({\n name: \"Docker\",\n status: dockerActive ? \"pass\" : \"fail\",\n message: dockerActive ? \"Docker is running\" : \"Docker is not running\",\n fix: \"Run: systemctl start docker\",\n });\n\n // Dangling images\n const danglingImages = io.execSilent(\"docker images -f dangling=true -q | wc -l\");\n const count = parseInt(danglingImages, 10) || 0;\n if (count > 0) {\n checks.push({\n name: \"Docker cleanup\",\n status: \"warn\",\n message: `${count} dangling Docker images`,\n fix: \"Run: docker system prune -f\",\n });\n }\n\n // Disk usage\n const diskUsage = io.execSilent(\"df -h / | awk 'NR==2{print $5}'\");\n const usagePercent = parseInt(diskUsage, 10) || 0;\n checks.push({\n name: \"Disk usage\",\n status: usagePercent > 90 ? \"fail\" : usagePercent > 80 ? \"warn\" : \"pass\",\n message: `Disk usage: ${diskUsage}`,\n fix: usagePercent > 80 ? \"Consider cleaning up old projects or Docker resources\" : undefined,\n });\n\n return checks;\n}\n\n/**\n * Audit file permissions\n */\nexport function auditPermissions(io: SetupIo = nodeIo): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Config file\n if (io.fileExists(\"/etc/trafic/config.toml\")) {\n const stats = io.execSilent(\"stat -c '%a' /etc/trafic/config.toml\");\n const isSecure = stats === \"640\" || stats === \"600\";\n checks.push({\n name: \"Config permissions\",\n status: isSecure ? \"pass\" : \"warn\",\n message: isSecure\n ? \"Config file has secure permissions\"\n : `Config file permissions: ${stats} (should be 640)`,\n fix: \"Run: chmod 640 /etc/trafic/config.toml\",\n });\n }\n\n // Projects directory\n if (io.fileExists(\"/home/ddev/www\")) {\n const owner = io.execSilent(\"stat -c '%U:%G' /home/ddev/www\");\n const isCorrect = owner === \"ddev:ddev\";\n checks.push({\n name: \"Projects directory\",\n status: isCorrect ? \"pass\" : \"warn\",\n message: isCorrect\n ? \"Projects directory owned by ddev\"\n : `Projects directory owner: ${owner}`,\n fix: \"Run: chown ddev:ddev /home/ddev/www\",\n });\n }\n\n return checks;\n}\n\n/**\n * Audit the wildcard certificate, where one is configured.\n *\n * Two things go wrong silently: the certificate is never issued (a wrong\n * token, a zone the token cannot edit), and the token file ends up readable\n * by everyone.\n */\nexport function auditWildcardTls(\n config: AgentConfig,\n io: SetupIo = nodeIo,\n): AuditCheck[] {\n if (!config.tls.dnsProvider) {\n return [];\n }\n\n const checks: AuditCheck[] = [];\n\n checks.push(wildcardCertificateCheck(config.tld, io));\n\n if (io.fileExists(ROUTER_COMPOSE_OVERRIDE)) {\n const mode = io.execSilent(`stat -c '%a' ${ROUTER_COMPOSE_OVERRIDE}`);\n const isSecure = mode === \"600\";\n\n checks.push({\n name: \"DNS credentials permissions\",\n status: isSecure ? \"pass\" : \"warn\",\n message: isSecure\n ? \"Router compose override is readable by its owner only\"\n : `Router compose override permissions: ${mode} (should be 600)`,\n fix: `Run: chmod 600 ${ROUTER_COMPOSE_OVERRIDE}`,\n });\n }\n\n return checks;\n}\n\n/** Look for the wildcard certificate in Traefik's DNS-01 ACME storage. */\nfunction wildcardCertificateCheck(tld: string, io: SetupIo): AuditCheck {\n const fix =\n \"Check the router log for ACME errors: docker logs ddev-router 2>&1 | grep -i acme\";\n\n const mountpoint = io.execSilent(\n \"docker volume inspect ddev-global-cache --format '{{.Mountpoint}}'\",\n );\n\n if (!mountpoint) {\n return {\n name: \"Wildcard certificate\",\n status: \"warn\",\n message: \"Traefik's storage volume (ddev-global-cache) was not found\",\n fix,\n };\n }\n\n const storage = `${mountpoint}/traefik/acme-dns.json`;\n\n if (!io.fileExists(storage)) {\n return {\n name: \"Wildcard certificate\",\n status: \"warn\",\n message: \"No DNS-01 certificate has been issued yet\",\n fix,\n };\n }\n\n const issued = hasCertificateFor(io.readFile(storage), tld);\n\n return {\n name: \"Wildcard certificate\",\n status: issued ? \"pass\" : \"warn\",\n message: issued\n ? `Wildcard certificate issued for *.${tld}`\n : `No certificate for ${tld} in Traefik's DNS-01 storage`,\n fix: issued ? undefined : fix,\n };\n}\n\n/**\n * Whether Traefik's ACME storage holds a certificate whose main domain is the\n * TLD — which is the wildcard one, since that is what the default store asks\n * for.\n *\n * The file is keyed by resolver name, each with a Certificates array.\n */\nexport function hasCertificateFor(storage: string, tld: string): boolean {\n let parsed: unknown;\n\n try {\n parsed = JSON.parse(storage);\n } catch {\n return false;\n }\n\n if (parsed === null || typeof parsed !== \"object\") {\n return false;\n }\n\n for (const resolver of Object.values(parsed as Record<string, unknown>)) {\n const certificates = (resolver as { Certificates?: unknown }).Certificates;\n\n if (!Array.isArray(certificates)) {\n continue;\n }\n\n for (const certificate of certificates) {\n const main = (certificate as { domain?: { main?: unknown } }).domain?.main;\n\n if (main === tld) {\n return true;\n }\n }\n }\n\n return false;\n}\n\n/**\n * Run all audit checks\n */\nexport function runAudit(io: SetupIo = nodeIo, config: AgentConfig = loadConfig()): AuditCheck[] {\n return [\n ...auditSsh(io),\n ...auditFirewall(io),\n ...auditServices(io),\n ...auditDocker(io),\n ...auditPermissions(io),\n ...auditWildcardTls(config, io),\n ];\n}\n\n/**\n * Print audit results\n */\nexport function printAuditResults(checks: AuditCheck[]): void {\n console.log(\"\\n\\x1b[1mSecurity Audit Results\\x1b[0m\\n\");\n\n for (const check of checks) {\n const icon =\n check.status === \"pass\"\n ? \"\\x1b[32m✓\\x1b[0m\"\n : check.status === \"warn\"\n ? \"\\x1b[33m⚠\\x1b[0m\"\n : \"\\x1b[31m✗\\x1b[0m\";\n\n console.log(`${icon} \\x1b[1m${check.name}\\x1b[0m: ${check.message}`);\n\n if (check.fix && check.status !== \"pass\") {\n console.log(` \\x1b[90m${check.fix}\\x1b[0m`);\n }\n }\n\n const passed = checks.filter((c) => c.status === \"pass\").length;\n const warnings = checks.filter((c) => c.status === \"warn\").length;\n const failed = checks.filter((c) => c.status === \"fail\").length;\n\n console.log(\n `\\n\\x1b[1mSummary:\\x1b[0m ${passed} passed, ${warnings} warnings, ${failed} failed`,\n );\n}\n","import { existsSync } from \"node:fs\";\nimport { exec } from \"../steps.js\";\nimport { addDdevAptRepo } from \"../ddev.js\";\nimport type { Migration } from \"../types.js\";\n\n/**\n * Migration 0001: Migrate DDEV from manual tarball to apt repository.\n *\n * Old installs downloaded a tarball from GitHub releases. This migration\n * removes the manually-installed binaries and reinstalls DDEV via the\n * official apt repository so future updates are handled by apt.\n *\n * Condition: skip if /etc/apt/sources.list.d/ddev.list already exists,\n * which means the apt repository is already configured.\n */\nexport const migration0001DdevAptRepo: Migration = {\n id: \"0001__ddev_apt_repo\",\n description: \"Migrate DDEV from manual tarball to apt repository\",\n\n run(): void {\n // Already using the apt repository — nothing to do\n if (existsSync(\"/etc/apt/sources.list.d/ddev.list\")) {\n return;\n }\n\n // 1. Remove manually-installed DDEV binaries\n exec(\"rm -f /usr/local/bin/ddev /usr/local/bin/ddev-hostname /usr/local/bin/mkcert\", {\n silent: true,\n });\n\n // 2. Add the official DDEV apt repository\n addDdevAptRepo();\n\n // 3. Install DDEV via apt\n exec(\"apt-get update -qq\", { silent: true });\n exec(\"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ddev\", {\n silent: true,\n });\n\n // 4. Re-install mkcert CA (mkcert is now bundled with DDEV via apt)\n exec(\"mkcert -install\", { silent: true });\n },\n};\n","import { existsSync } from \"node:fs\";\nimport { exec } from \"../steps.js\";\nimport type { Migration } from \"../types.js\";\n\n/**\n * Migration 0002: Install mkcert CA in the ddev user trust store.\n *\n * Previously, `mkcert -install` was called as root without overriding HOME,\n * so the CA was stored under ~root/.local/share/mkcert. DDEV runs as the\n * `ddev` user and looks for the CA in its own home directory — so it never\n * found it, reporting mkcert as not installed.\n *\n * Fix: run `mkcert -install` with HOME=/home/ddev so the CA lands in the\n * ddev user trust store, then chown the directory to ddev:ddev.\n *\n * Condition: skip if /home/ddev/.local/share/mkcert already exists,\n * which means the CA is already installed for the ddev user.\n */\nexport const migration0002MkcertDdevUser: Migration = {\n id: \"0002__mkcert_ddev_user\",\n description: \"Install mkcert CA in the ddev user trust store\",\n\n run(): void {\n // Already installed for the ddev user — nothing to do\n if (existsSync(\"/home/ddev/.local/share/mkcert\")) {\n return;\n }\n\n exec(\"HOME=/home/ddev mkcert -install\", { silent: true });\n exec(\"chown -R ddev:ddev /home/ddev/.local/share/mkcert\", { silent: true });\n },\n};\n","import { exec } from \"../steps.js\";\nimport type { Migration } from \"../types.js\";\n\n/**\n * Migration 0003: Enable router_bind_all_interfaces in DDEV global config.\n *\n * The initial setup did not set --router-bind-all-interfaces=true, which\n * caused ddev-router to bind only on 127.0.0.1. External traffic (including\n * Cloudflare proxying) could not reach the router, resulting in 521 errors.\n *\n * This migration enables the flag and restarts the DDEV router so the change\n * takes effect immediately.\n *\n * Idempotent: ddev config global is safe to re-run with the same value.\n */\nexport const migration0003DdevRouterBindAllInterfaces: Migration = {\n id: \"0003__ddev_router_bind_all_interfaces\",\n description: \"Bind ddev-router on all interfaces (fix 521 for external traffic)\",\n\n run(): void {\n exec(\"su - ddev -c 'ddev config global --router-bind-all-interfaces=true'\", { silent: true });\n // Restart all running projects so the router picks up the new bind setting\n exec(\"su - ddev -c 'ddev poweroff'\", { silent: true });\n exec(\"su - ddev -c 'ddev start --all'\", { silent: true });\n },\n};\n","import { existsSync } from \"node:fs\";\nimport { configureDdevSudoers } from \"../user.js\";\nimport type { Migration } from \"../types.js\";\n\n/**\n * Migration 0004: Allow ddev user to run ddev-hostname without a password.\n *\n * ddev-hostname manages /etc/hosts entries and always calls sudo internally,\n * even when DDEV_NONINTERACTIVE=true. Without this sudoers rule, any ddev\n * command that touches hostname resolution (start, restart, poweroff + start)\n * fails with \"sudo: a terminal is required to read the password\".\n *\n * Condition: skip if /etc/sudoers.d/trafic-ddev already exists.\n */\nexport const migration0004DdevHostnameSudoers: Migration = {\n id: \"0004__ddev_hostname_sudoers\",\n description: \"Allow ddev user to run ddev-hostname without a password\",\n\n run(): void {\n if (existsSync(\"/etc/sudoers.d/trafic-ddev\")) {\n return;\n }\n\n configureDdevSudoers();\n },\n};\n","import { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { getDockerGatewayIp } from \"../ddev.js\";\nimport type { Migration } from \"../types.js\";\n\nconst TRAFIC_YAML = \"/home/ddev/.ddev/traefik/trafic.yaml\";\n\n/**\n * Migration 0005: Replace host.docker.internal with the Docker bridge gateway IP\n * in the Traefik forward-auth config.\n *\n * host.docker.internal is a Docker Desktop / macOS feature and is not available\n * on Linux without explicit configuration. The Traefik router container cannot\n * resolve it, so forward-auth requests silently fail and all traffic bypasses\n * authentication.\n *\n * Fix: replace all occurrences of host.docker.internal in trafic.yaml with the\n * actual Docker bridge gateway IP (typically 172.17.0.1 or 172.18.0.1).\n *\n * Condition: skip if trafic.yaml doesn't exist or already uses an IP address.\n */\nexport const migration0005TraefikGatewayIp: Migration = {\n id: \"0005__traefik_gateway_ip\",\n description: \"Replace host.docker.internal with Docker bridge gateway IP in Traefik config\",\n\n run(): void {\n if (!existsSync(TRAFIC_YAML)) {\n return;\n }\n\n const content = readFileSync(TRAFIC_YAML, \"utf-8\");\n const gatewayIp = getDockerGatewayIp();\n\n // Replace host.docker.internal (original broken value) or any previously\n // written IP that no longer matches the current gateway (e.g. if the\n // migration ran before ddev_default existed and wrote 172.17.0.1 instead).\n const updated = content\n .replaceAll(\"host.docker.internal\", gatewayIp)\n .replace(/http:\\/\\/\\d+\\.\\d+\\.\\d+\\.\\d+:9876/g, `http://${gatewayIp}:9876`);\n\n if (updated === content) {\n return;\n }\n\n writeFileSync(TRAFIC_YAML, updated, \"utf-8\");\n },\n};\n","import { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { getDockerGatewayIp } from \"../ddev.js\";\nimport type { Migration } from \"../types.js\";\n\nconst TRAFIC_YAML = \"/home/ddev/.ddev/traefik/trafic.yaml\";\n\n/**\n * Migration 0006: Fix the Docker gateway IP in trafic.yaml.\n *\n * Migration 0005 inspected the default `bridge` network (172.17.0.1) instead\n * of the `ddev_default` network (typically 172.18.0.1) that ddev-router\n * actually runs on. This migration rewrites trafic.yaml with the correct IP.\n *\n * Idempotent: re-reads the file and only writes if the IP has changed.\n */\nexport const migration0006TraefikGatewayIpFix: Migration = {\n id: \"0006__traefik_gateway_ip_fix\",\n description: \"Fix Docker gateway IP in Traefik config (use ddev_default network)\",\n\n run(): void {\n if (!existsSync(TRAFIC_YAML)) {\n return;\n }\n\n const content = readFileSync(TRAFIC_YAML, \"utf-8\");\n const gatewayIp = getDockerGatewayIp();\n\n const updated = content.replace(\n /http:\\/\\/[\\w.]+:9876/g,\n `http://${gatewayIp}:9876`,\n );\n\n if (updated === content) {\n return;\n }\n\n writeFileSync(TRAFIC_YAML, updated, \"utf-8\");\n },\n};\n","import { exec, execSilent } from \"../steps.js\";\nimport type { Migration } from \"../types.js\";\n\n/**\n * Migration 0007: Allow trafic-agent port 9876 from Docker bridge subnets in UFW.\n *\n * UFW does not automatically allow traffic from Docker bridge networks.\n * Without this rule, ddev-router cannot reach the trafic-agent forward-auth\n * endpoint (172.18.0.1:9876), causing all auth checks to time out and traffic\n * to pass through unauthenticated.\n *\n * Condition: skip if the rule already exists.\n */\nexport const migration0007UfwDockerTraficPort: Migration = {\n id: \"0007__ufw_docker_trafic_port\",\n description: \"Allow trafic-agent port 9876 from Docker bridge subnets in UFW\",\n\n run(): void {\n const existing = execSilent(\"ufw status | grep '9876'\");\n if (existing.includes(\"9876\")) {\n return;\n }\n\n exec(\"ufw allow from 172.16.0.0/12 to any port 9876 comment 'trafic-agent from Docker'\", {\n silent: true,\n });\n },\n};\n","import { writeFileSync, rmSync, existsSync } from \"node:fs\";\nimport { exec } from \"../steps.js\";\nimport { getDockerGatewayIp } from \"../ddev.js\";\nimport type { Migration } from \"../types.js\";\n\n/**\n * Migration 0008: Switch Traefik from catch-all router to entry point middleware.\n *\n * The previous approach used a low-priority catch-all router (priority=1) with\n * trafic-auth middleware in dynamic.yaml. This failed because DDEV's project-specific\n * routers always win — they have higher priority and no trafic-auth middleware.\n *\n * The correct approach (from the previous trafic v1 with DDEV 1.24) is to attach\n * the middleware at the entry point level via a static_config.*.yaml file that DDEV\n * merges into .static_config.yaml on every start. This ensures every request on\n * http-80 and http-443 passes through auth regardless of which router handles it.\n *\n * Changes:\n * - Write static_config.trafic.yaml: attaches trafic-auth@file + trafic-errors@file\n * to the http-80 and http-443 entry points\n * - Write trafic.yaml to custom-global-config/ (survives DDEV 1.25+ config purge)\n * - Remove the old dynamic.yaml catch-all router\n */\nexport const migration0008TraefikEntrypointMiddleware: Migration = {\n id: \"0008__traefik_entrypoint_middleware\",\n description:\n \"Switch Traefik auth from catch-all router to entry point middleware\",\n\n run(): void {\n const gatewayIp = getDockerGatewayIp();\n\n // Dynamic config: middleware + service definitions\n const dynamicConfig = `# Trafic: Forward auth middleware definition\nhttp:\n middlewares:\n trafic-auth:\n forwardAuth:\n address: \"http://${gatewayIp}:9876/__auth__\"\n authResponseHeaders:\n - \"X-Trafic-Project\"\n\n trafic-errors:\n errors:\n status:\n - \"502\"\n - \"503\"\n service: trafic-service\n query: \"/\"\n\n services:\n trafic-service:\n loadBalancer:\n servers:\n - url: \"http://${gatewayIp}:9876\"\n`;\n\n exec(\"mkdir -p /home/ddev/.ddev/traefik/custom-global-config\", { silent: true });\n writeFileSync(\"/home/ddev/.ddev/traefik/trafic.yaml\", dynamicConfig);\n writeFileSync(\"/home/ddev/.ddev/traefik/custom-global-config/trafic.yaml\", dynamicConfig);\n exec(\"chown -R ddev:ddev /home/ddev/.ddev/traefik\", { silent: true });\n\n // Static config: attach middleware to entry points\n const staticConfig = `entryPoints:\n http-80:\n http:\n middlewares:\n - trafic-auth@file\n - trafic-errors@file\n http-443:\n http:\n middlewares:\n - trafic-auth@file\n - trafic-errors@file\n`;\n\n writeFileSync(\"/home/ddev/.ddev/traefik/static_config.trafic.yaml\", staticConfig);\n exec(\"chown ddev:ddev /home/ddev/.ddev/traefik/static_config.trafic.yaml\", { silent: true });\n\n // Remove the old catch-all router config — it's superseded by entry point middleware\n const oldDynamicYaml = \"/home/ddev/.ddev/traefik/dynamic.yaml\";\n if (existsSync(oldDynamicYaml)) {\n rmSync(oldDynamicYaml);\n }\n\n // Restart DDEV router to apply the new static config\n exec(\"su - ddev -c 'DDEV_NONINTERACTIVE=true ddev poweroff && ddev start --all'\", {\n silent: false,\n });\n },\n};\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { exec } from \"../steps.js\";\nimport { configureTraefik } from \"../ddev.js\";\nimport { createSystemdService } from \"../agent.js\";\nimport type { Migration } from \"../types.js\";\n\nconst UNIT_PATH = \"/etc/systemd/system/trafic-agent.service\";\nconst DYNAMIC_CONFIG = \"/home/ddev/.ddev/traefik/trafic.yaml\";\n\n/**\n * Migration 0009: Make scale-to-zero actually work.\n *\n * Two independent faults meant a stopped project never came back.\n *\n * The waiting page never appeared. `ddev stop` removes the project's router,\n * so no router matches and Traefik answers 404 — and entry point middlewares\n * do not run without a router match, so `trafic-errors` (which only lists 502\n * and 503) never saw it. Migration 0008 removed the catch-all router to fix an\n * auth bypass, which was right, but it also removed the only thing catching\n * these requests. A catch-all at priority 1 is safe now that auth lives on the\n * entry point: a project router always outranks it.\n *\n * Auto-start could not have worked either. The unit set\n * `ReadWritePaths=/var/lib/trafic /home/ddev/.ddev`, but `ddev start` writes\n * to /home/ddev/www/<project>/.ddev/.webimageBuild and to buildx state in\n * /home/ddev/.docker, both of which ProtectHome=read-only refused.\n *\n * Rewrites both the Traefik config and the systemd unit. Idempotent: skipped\n * once the catch-all is present and the unit grants the home.\n */\nexport const migration0009ScaleToZeroFixes: Migration = {\n id: \"0009__scale_to_zero_fixes\",\n description: \"Fix scale-to-zero: waiting page router and agent write access\",\n\n run(): void {\n const unitNeedsWrite =\n !existsSync(UNIT_PATH) ||\n !/^ReadWritePaths=.*\\/home\\/ddev(\\s|$)/m.test(\n readFileSync(UNIT_PATH, \"utf-8\"),\n );\n\n const configNeedsCatchall =\n !existsSync(DYNAMIC_CONFIG) ||\n !readFileSync(DYNAMIC_CONFIG, \"utf-8\").includes(\"trafic-catchall\");\n\n if (configNeedsCatchall) {\n // Rewrites both dynamic config copies and the static config, and\n // recomputes the gateway IP while it is there\n configureTraefik();\n\n // Traefik reads its config from the ddev-global-cache volume, and DDEV\n // only copies ~/.ddev/traefik into it when a project starts. Restarting\n // the router alone leaves the old config in place.\n exec(\"su - ddev -c 'DDEV_NONINTERACTIVE=true ddev poweroff && ddev start --all'\", {\n silent: true,\n });\n }\n\n if (unitNeedsWrite) {\n // Rewrites the unit with the corrected ReadWritePaths, then reloads and\n // restarts the agent\n createSystemdService();\n }\n },\n};\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { exec } from \"../steps.js\";\nimport { configureTraefik } from \"../ddev.js\";\nimport type { Migration } from \"../types.js\";\n\nconst DYNAMIC_CONFIG =\n \"/home/ddev/.ddev/traefik/custom-global-config/trafic.yaml\";\n\n/** The copy setup used to write for DDEV versions that watched the root. */\nconst STALE_CONFIG = \"/home/ddev/.ddev/traefik/trafic.yaml\";\n\n/**\n * Migration 0010: pin the catch-all routers to their entry points.\n *\n * The routers added by 0009 named no entry point, so Traefik instantiated\n * each on every one. DDEV's router health check counts router *definitions*\n * across its config files and compares that to what Traefik reports, so two\n * definitions showing up as fourteen instances never matched. The check waited\n * out its timeout and `ddev start` failed after 60 seconds — on every server\n * setup produces, because setup configures the tool entry points.\n *\n * Observed on a live server: 37 routers loaded against 22 expected, health\n * still \"starting\" after 91s. With the routers pinned: 23 loaded, healthy\n * immediately.\n *\n * Traefik reads its config from the ddev-global-cache volume and DDEV only\n * copies `~/.ddev/traefik` into it when a project starts, so this restarts\n * DDEV rather than the router alone.\n *\n * Idempotent: skipped once the config names entry points.\n */\nexport const migration0010CatchallEntrypoints: Migration = {\n id: \"0010__catchall_entrypoints\",\n description: \"Pin the Traefik catch-all routers to their entry points\",\n\n run(): void {\n const pinned =\n existsSync(DYNAMIC_CONFIG) &&\n readFileSync(DYNAMIC_CONFIG, \"utf-8\").includes(\"entryPoints:\");\n\n if (pinned && !existsSync(STALE_CONFIG)) {\n return;\n }\n\n if (!pinned) {\n // Rewrites both configs from the server's actual router ports\n configureTraefik();\n }\n\n // Nothing reads this on DDEV 1.25, and leaving it behind means two\n // sources of truth for anyone debugging the router\n if (existsSync(STALE_CONFIG)) {\n exec(`rm -f ${STALE_CONFIG}`, { silent: true });\n }\n\n if (!pinned) {\n exec(\n \"su - ddev -c 'DDEV_NONINTERACTIVE=true ddev poweroff && ddev start --all'\",\n { silent: true },\n );\n }\n },\n};\n","import { execSilent } from \"../steps.js\";\nimport { purgeTraefikAcmeStorage } from \"../ddev.js\";\nimport type { Migration } from \"../types.js\";\n\n/**\n * Migration 0011: drop Traefik's Let's Encrypt storage where it is disabled.\n *\n * `use_letsencrypt=false` stops DDEV asking for new certificates but leaves\n * the issued ones in acme.json inside the ddev-global-cache volume. Traefik\n * loads them again on restart, so a server that had Let's Encrypt on at some\n * point keeps serving those certificates from a port that is now only\n * reachable through a host ingress.\n *\n * That broke a live server: the ingress verifies the loopback hop against the\n * mkcert root, the resurrected Let's Encrypt certificate failed validation,\n * and every request answered 502 with \"certificate signed by unknown\n * authority\" until the storage was moved aside.\n *\n * Only runs where Let's Encrypt is off — with it on, those certificates are\n * the ones being served on purpose.\n *\n * Traefik holds the certificate in memory, so removing the file is not enough\n * on its own; the router is restarted. Restarting the router is safe here\n * because no config needs re-copying from the host — unlike a config change,\n * which needs a project start.\n */\nexport const migration0011PurgeAcmeWhenDisabled: Migration = {\n id: \"0011__purge_acme_when_disabled\",\n description: \"Remove Traefik's Let's Encrypt storage when it is disabled\",\n\n run(): void {\n const globalConfig = execSilent(\"su - ddev -c 'ddev config global'\");\n\n if (!/^use-letsencrypt=false$/m.test(globalConfig)) {\n // Enabled, or the config could not be read — leave the certificates be\n return;\n }\n\n if (!purgeTraefikAcmeStorage()) {\n return;\n }\n\n // The running Traefik still holds what it loaded at startup\n execSilent(\"docker restart ddev-router\");\n },\n};\n","import { existsSync } from \"node:fs\";\nimport { configureDockerFirewall, DOCKER_FIREWALL_SCRIPT } from \"../docker-firewall.js\";\nimport { readToolPorts } from \"../ddev.js\";\nimport type { Migration } from \"../types.js\";\n\n/**\n * Migration 0012: restrict the Docker-published DDEV tool ports.\n *\n * `ufw default deny incoming` never applied to them. Docker's DNAT sits in\n * `nat/PREROUTING` and its filtering in the `DOCKER` chain of `FORWARD`, both\n * evaluated before UFW's chains, so the ports ddev-router publishes for\n * Mailpit and xhgui have been reachable on every server regardless of what\n * `ufw status` showed.\n *\n * Forward auth answers those requests with a 401, so this was never an open\n * door — but it is a single control, and it was silently missing from those\n * exact entry points until 0.1.30, which is how xhgui came to answer the\n * internet unauthenticated.\n *\n * A provider firewall does not close it either. Measured on an OVH server\n * with the Edge Firewall enabled and denying these ports: a connection from\n * another host inside OVH still completed in 114ms, while a port with nothing\n * listening was dropped and timed out at 12s. Anyone able to rent a VM from\n * the same provider sits inside that blind spot.\n *\n * Idempotent: skipped once the script is installed. The script itself removes\n * any previous copy of each rule before inserting it, so reapplying is safe.\n */\nexport const migration0012DockerUserToolPorts: Migration = {\n id: \"0012__docker_user_tool_ports\",\n description: \"Restrict Docker-published DDEV tool ports via DOCKER-USER\",\n\n run(): void {\n if (existsSync(DOCKER_FIREWALL_SCRIPT)) {\n return;\n }\n\n configureDockerFirewall(readToolPorts());\n },\n};\n","import { removeSshKexAlgorithms } from \"../hardening.js\";\nimport type { Migration } from \"../types.js\";\n\n/**\n * Migration 0013: leave SSH key exchange on the OpenSSH defaults.\n *\n * `setup` wrote a `KexAlgorithms curve25519-sha256@libssh.org,...` line into\n * /etc/ssh/sshd_config.d/trafic.conf. Without a `+`, `-` or `^` prefix that\n * directive replaces OpenSSH's default list rather than adding to it, and the\n * two algorithms named are classical only. The post-quantum hybrids OpenSSH\n * enables by itself — mlkem768x25519-sha256 and sntrup761x25519-sha512 — were\n * therefore excluded, so an OpenSSH 10.1+ client prints a warning on every\n * connection that the session is not using a post-quantum key exchange and\n * may be vulnerable to \"store now, decrypt later\" attacks. Confirmed via\n * `sshd -T` on Ubuntu 26.04 with OpenSSH 10.2.\n *\n * `hardenSsh` no longer writes the line, which covers fresh installs. Servers\n * set up by an earlier release still carry it, so this migration removes it\n * from the existing drop-in. Ciphers and MACs are left pinned — neither is\n * involved in the warning.\n *\n * Idempotent: the helper does nothing when the drop-in is missing or already\n * free of the line, and restores the file when `sshd -t` rejects the result.\n */\nexport const migration0013SshKexDefaults: Migration = {\n id: \"0013__ssh_kex_defaults\",\n description: \"Remove the KexAlgorithms pin from the SSH drop-in\",\n\n run(): void {\n removeSshKexAlgorithms();\n },\n};\n","import { addDockerOriginToUnattendedUpgrades } from \"../hardening.js\";\nimport type { Migration } from \"../types.js\";\n\n/**\n * Migration 0014: apply Docker Engine security updates unattended.\n *\n * `setup` wrote an `Unattended-Upgrade::Allowed-Origins` list covering only\n * Ubuntu's own release, security and ESM pockets. Docker Engine is installed\n * from download.docker.com, which is none of them, so `docker-ce` was never\n * upgraded unless someone ran `apt-get upgrade` by hand — a Docker security\n * release could sit unapplied on a server indefinitely. The Release file\n * served by that repository carries `Origin: Docker` and `Label: Docker CE`,\n * and its dist is the Ubuntu codename, so\n * `\"origin=Docker,codename=${distro_codename}\"` is the entry that matches it.\n *\n * DDEV, installed from pkg.ddev.com, is deliberately not added: a DDEV major\n * landing unattended can break running previews. DDEV is updated by\n * `trafic-agent upgrade` instead, when an operator is there to see it.\n *\n * `configureUnattendedUpgrades` now writes the Docker entry, which covers\n * fresh installs. Servers set up by an earlier release still carry the old\n * list, so this migration regenerates the file — Trafic generates and owns\n * it, as its header comment states.\n *\n * Idempotent: the helper does nothing when the file is missing (a server set\n * up without hardening) or already names the Docker origin. No service\n * restart is needed — unattended-upgrades reads the config on each daily run.\n */\nexport const migration0014UnattendedUpgradesDockerOrigin: Migration = {\n id: \"0014__unattended_upgrades_docker_origin\",\n description: \"Allow unattended upgrades of Docker Engine\",\n\n run(): void {\n addDockerOriginToUnattendedUpgrades();\n },\n};\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { exec, info } from \"../steps.js\";\nimport { configureTraefik, findRunningProject } from \"../ddev.js\";\nimport type { Migration } from \"../types.js\";\n\nconst DYNAMIC_CONFIG = \"/home/ddev/.ddev/traefik/custom-global-config/trafic.yaml\";\n\n/**\n * Migration 0015: raise the Trafic catch-all routers above DDEV's own.\n *\n * DDEV 1.25.4's router image adds two catch-all routers of its own in\n * `default_config.yaml` — `ddev-router-fallback-http` and\n * `ddev-router-fallback-https`, from the `traefik_global_config_template.yaml`\n * template in ddev/ddev. They carry the rule `PathPrefix(`/`)` at priority 1,\n * on every router entry point, and point at a socat responder that answers\n * 404. Trafic's own catch-all routers used priority 1 too.\n *\n * With equal priority and identical rules Traefik picked DDEV's, so a stopped\n * project answered DDEV's 404 page instead of the Trafic waiting page, and\n * scale-to-zero never restarted it — the waiting page is what triggers the\n * restart. Observed in the access log of a live server:\n * `\"GET /rien-a-mettre.html HTTP/2.0\" 404 ...\n * \"http-443-ddev-router-fallback-https@file\"`.\n *\n * `trafic-agent upgrade` installs DDEV 1.25.4, so every upgraded server is\n * affected. Priority 2 puts the Trafic routers back in front, and is still far\n * below any project router, so per-project routing and auth are unchanged.\n *\n * Traefik reads its config from the ddev-global-cache volume, and DDEV copies\n * `~/.ddev/traefik/custom-global-config/*.yaml` into it on any `ddev start` —\n * `PushGlobalTraefikConfig` runs even when the router is already up. Starting\n * one project that is already running is therefore enough to load the new\n * priority; `ddev poweroff` would stop every preview on the server for no\n * gain, so it is not used. When nothing runs, there is nothing to push to and\n * the next project start applies it.\n *\n * Unrelated and not addressed here: DDEV 1.25.4 also logs \"Router count\n * mismatch\" warnings, because its two fallback definitions attach to every\n * entry point while its health check counts definitions. That is DDEV's own\n * check against DDEV's own routers.\n *\n * Idempotent: skipped when the config is missing, and when no `priority: 1`\n * is left in it.\n */\nexport const migration0015CatchallPriority: Migration = {\n id: \"0015__catchall_priority\",\n description: \"Raise the Traefik catch-all routers above DDEV's fallback routers\",\n\n run(): void {\n if (!existsSync(DYNAMIC_CONFIG)) {\n return;\n }\n\n if (!readFileSync(DYNAMIC_CONFIG, \"utf-8\").includes(\"priority: 1\")) {\n return;\n }\n\n // Rewrites both configs from the server's actual router ports\n configureTraefik();\n\n const project = findRunningProject();\n\n if (!project) {\n info(\"No project is running — the new priority applies on the next project start.\");\n return;\n }\n\n exec(`su - ddev -c 'DDEV_NONINTERACTIVE=true ddev start ${project}'`, { silent: true });\n },\n};\n","import { info } from \"../steps.js\";\nimport {\n configureTraefik,\n findRunningProject,\n DNS_RESOLVER,\n ROUTER_COMPOSE_OVERRIDE,\n STATIC_CONFIG,\n TLS_STORE_CONFIG,\n} from \"../ddev.js\";\nimport { nodeIo, type SetupIo } from \"../io.js\";\nimport { loadConfig } from \"../../utils/config.js\";\nimport type { Migration } from \"../types.js\";\nimport type { TlsConfig } from \"../../types.js\";\n\n/**\n * Migration 0016: apply the wildcard DNS-01 certificate on servers that ask\n * for one in their config.\n *\n * `tls.dns_provider` is read by `configureTraefik`, which setup runs on a\n * fresh server. An existing server has the config but none of the files, so\n * this writes them: the resolver in `static_config.trafic.yaml`, the default\n * TLS store in `custom-global-config/0-trafic-tls.yaml`, and the provider\n * credentials in `router-compose.trafic.yaml`.\n *\n * Why the router is removed and a project started, rather than restarted\n * (the 0010 rationale, plus one more): Traefik reads its configuration from\n * the ddev-global-cache volume, and DDEV only copies `~/.ddev/traefik` into\n * that volume and merges `static_config.*.yaml` into `.static_config.yaml`\n * when a project starts. And DDEV 1.25 reads `router-compose.*.yaml` only\n * when it *recreates* the router: a running, healthy router just gets the\n * Traefik config pushed (StartDdevRouter, `needsRecreation`). Seen on a live\n * server — the static config was merged, the credentials never reached the\n * container. Removing the router first forces the recreation; `ddev start`\n * brings it back within seconds with the new environment.\n *\n * The container is found by its compose label, not by name: when compose\n * recreates a container it renames the old one `<id>_ddev-router`, and a\n * failed recreation can leave it that way. `docker restart ddev-router` then\n * silently does nothing — also seen on that server.\n *\n * Two details this depends on:\n * - Traefik's file provider keeps the FIRST `tls.stores.default` it reads in\n * directory order and logs \"TLS store default already configured,\n * skipping\" for the rest. DDEV writes an empty one in\n * `default_config.yaml`, so the agent's file is named `0-trafic-tls.yaml`\n * to sort ahead of it.\n * - DDEV keeps `certResolver: acme-tlsChallenge` on every project router, and\n * that is fine: Traefik skips a per-host ACME request once the default\n * store already holds a certificate matching the host with wildcard\n * semantics. So `use_letsencrypt` stays true — the ACME account email comes\n * from it — and the per-host quota use stops on its own.\n *\n * `ddev poweroff` is deliberately not used: the server runs live previews and\n * a project start is enough to regenerate the config and recreate the router.\n *\n * Idempotent: skipped when no DNS provider is configured, and when all three\n * files are already in place.\n */\nexport const migration0016WildcardDnsChallenge: Migration = {\n id: \"0016__wildcard_dns_challenge\",\n description: \"Apply the wildcard DNS-01 certificate where one is configured\",\n\n run(): void {\n runWildcardMigration();\n },\n};\n\n/**\n * The migration body, with its effects injected so tests can drive it.\n *\n * `tls` defaults to the server's own config: the migration runs after setup\n * wrote /etc/trafic/config.toml, so that file is the only source.\n */\nexport function runWildcardMigration(\n io: SetupIo = nodeIo,\n tls: TlsConfig = loadConfig().tls,\n): void {\n if (!tls.dnsProvider) {\n // Per-host certificates stay as they are\n return;\n }\n\n const resolverConfigured =\n io.fileExists(STATIC_CONFIG) && io.readFile(STATIC_CONFIG).includes(`${DNS_RESOLVER}:`);\n\n if (\n resolverConfigured &&\n io.fileExists(TLS_STORE_CONFIG) &&\n io.fileExists(ROUTER_COMPOSE_OVERRIDE)\n ) {\n return;\n }\n\n configureTraefik({ tls }, io);\n\n const project = findRunningProject(io);\n\n if (!project) {\n info(\"No running project — the next deploy applies the wildcard certificate\");\n return;\n }\n\n // A running router keeps its environment: DDEV only re-reads the compose\n // override when it recreates the container\n const router = findRouterContainer(io);\n\n if (router) {\n io.execSilent(`docker rm -f ${router}`);\n }\n\n // Regenerates .static_config.yaml and the router compose from the files\n // above, and recreates ddev-router with the provider credentials\n io.exec(`su - ddev -c 'DDEV_NONINTERACTIVE=true ddev start ${project}'`, {\n silent: true,\n });\n}\n\n/**\n * The ddev-router container id, whatever its current name.\n *\n * Compose labels survive the `<id>_ddev-router` rename a recreation leaves\n * behind; the container name does not.\n */\nexport function findRouterContainer(io: SetupIo = nodeIo): string | undefined {\n const id = io\n .execSilent(\"docker ps -aq --filter label=com.docker.compose.service=ddev-router\")\n .trim();\n\n return id.split(\"\\n\")[0] || undefined;\n}\n","import { addDockerAddressPools } from \"../docker.js\";\nimport { nodeIo, type SetupIo } from \"../io.js\";\nimport type { Migration } from \"../types.js\";\n\n/**\n * Migration 0018: give Docker enough address space for one network per\n * project.\n *\n * `setup` wrote a daemon.json without `default-address-pools`, so Docker\n * used its built-in pools: about 31 networks. Each DDEV project creates its\n * own `ddev-<name>_default` network, and a server with ~30 projects failed\n * to start the next one with \"all predefined address pools have been fully\n * subnetted\". The last 16 of those built-in networks are in 192.168.x,\n * outside the 172.16.0.0/12 range the firewall allows to reach the agent.\n *\n * `configureDocker` now writes /24 pools in 172.16.0.0/12, which covers\n * fresh installs. This migration adds them to existing servers.\n *\n * Docker reads the pools only at start, so the daemon is restarted.\n * `live-restore`, set by setup, keeps the running containers up. Existing\n * networks keep their subnets; only new networks get a /24, and Docker skips\n * subnets an existing network already uses.\n *\n * Idempotent: does nothing when the config already sets pools.\n */\nexport const migration0018DockerAddressPools: Migration = {\n id: \"0018__docker_address_pools\",\n description: \"Give Docker address pools for one network per project\",\n\n run(): void {\n runDockerAddressPoolsMigration();\n },\n};\n\n/**\n * The migration body, with its effects injected so tests can drive it.\n */\nexport function runDockerAddressPoolsMigration(io: SetupIo = nodeIo): void {\n if (addDockerAddressPools(io)) {\n io.exec(\"systemctl restart docker\");\n }\n}\n","import { existsSync, readFileSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport type { Migration, MigrationState, MigrationListEntry } from \"../types.js\";\nimport { migration0001DdevAptRepo } from \"./0001__ddev_apt_repo.js\";\nimport { migration0002MkcertDdevUser } from \"./0002__mkcert_ddev_user.js\";\nimport { migration0003DdevRouterBindAllInterfaces } from \"./0003__ddev_router_bind_all_interfaces.js\";\nimport { migration0004DdevHostnameSudoers } from \"./0004__ddev_hostname_sudoers.js\";\nimport { migration0005TraefikGatewayIp } from \"./0005__traefik_gateway_ip.js\";\nimport { migration0006TraefikGatewayIpFix } from \"./0006__traefik_gateway_ip_fix.js\";\nimport { migration0007UfwDockerTraficPort } from \"./0007__ufw_docker_trafic_port.js\";\nimport { migration0008TraefikEntrypointMiddleware } from \"./0008__traefik_entrypoint_middleware.js\";\nimport { migration0009ScaleToZeroFixes } from \"./0009__scale_to_zero_fixes.js\";\nimport { migration0010CatchallEntrypoints } from \"./0010__catchall_entrypoints.js\";\nimport { migration0011PurgeAcmeWhenDisabled } from \"./0011__purge_acme_when_disabled.js\";\nimport { migration0012DockerUserToolPorts } from \"./0012__docker_user_tool_ports.js\";\nimport { migration0013SshKexDefaults } from \"./0013__ssh_kex_defaults.js\";\nimport { migration0014UnattendedUpgradesDockerOrigin } from \"./0014__unattended_upgrades_docker_origin.js\";\nimport { migration0015CatchallPriority } from \"./0015__catchall_priority.js\";\nimport { migration0016WildcardDnsChallenge } from \"./0016__wildcard_dns_challenge.js\";\nimport { migration0018DockerAddressPools } from \"./0018__docker_address_pools.js\";\n\n/** Path to the persisted migration state file */\nexport const MIGRATIONS_STATE_FILE = \"/etc/trafic/.migrations.json\";\n\n/** Ordered registry of all known migrations */\nexport const ALL_MIGRATIONS: Migration[] = [\n migration0001DdevAptRepo,\n migration0002MkcertDdevUser,\n migration0003DdevRouterBindAllInterfaces,\n migration0004DdevHostnameSudoers,\n migration0005TraefikGatewayIp,\n migration0006TraefikGatewayIpFix,\n migration0007UfwDockerTraficPort,\n migration0008TraefikEntrypointMiddleware,\n migration0009ScaleToZeroFixes,\n migration0010CatchallEntrypoints,\n migration0011PurgeAcmeWhenDisabled,\n migration0012DockerUserToolPorts,\n migration0013SshKexDefaults,\n migration0014UnattendedUpgradesDockerOrigin,\n migration0015CatchallPriority,\n migration0016WildcardDnsChallenge,\n migration0018DockerAddressPools,\n];\n\n// ---------------------------------------------------------------------------\n// State helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Load the current migration state from disk.\n * Returns an empty state if the file does not exist yet.\n */\nexport function loadMigrationState(): MigrationState {\n if (!existsSync(MIGRATIONS_STATE_FILE)) {\n return { applied: [] };\n }\n\n try {\n const raw = readFileSync(MIGRATIONS_STATE_FILE, \"utf-8\");\n const parsed = JSON.parse(raw) as unknown;\n if (\n parsed !== null &&\n typeof parsed === \"object\" &&\n \"applied\" in parsed &&\n Array.isArray((parsed as { applied: unknown }).applied)\n ) {\n return parsed as MigrationState;\n }\n } catch {\n // Corrupted file — start fresh\n }\n\n return { applied: [] };\n}\n\n/**\n * Persist the migration state to disk.\n * Creates /etc/trafic/ if it does not exist.\n */\nexport function saveMigrationState(state: MigrationState): void {\n mkdirSync(\"/etc/trafic\", { recursive: true });\n writeFileSync(MIGRATIONS_STATE_FILE, JSON.stringify(state, null, 2) + \"\\n\", \"utf-8\");\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Return all migrations with their current status.\n */\nexport function listMigrations(): MigrationListEntry[] {\n const state = loadMigrationState();\n return ALL_MIGRATIONS.map((migration) => ({\n migration,\n status: state.applied.includes(migration.id) ? \"applied\" : \"pending\",\n }));\n}\n\n/**\n * Run all pending migrations and persist state after each one.\n *\n * When `dryRun` is true the runner prints what it would do without\n * executing anything or touching the state file.\n */\nexport function runPendingMigrations(dryRun = false): void {\n const state = loadMigrationState();\n const pending = ALL_MIGRATIONS.filter((m) => !state.applied.includes(m.id));\n\n if (pending.length === 0) {\n console.log(\" No pending migrations.\");\n return;\n }\n\n for (const migration of pending) {\n console.log(`\\n → \\x1b[1m${migration.id}\\x1b[0m ${migration.description}`);\n\n if (dryRun) {\n console.log(` \\x1b[90m(dry-run: skipped)\\x1b[0m`);\n continue;\n }\n\n migration.run();\n\n // Persist state immediately so a partial failure leaves a consistent state\n state.applied.push(migration.id);\n saveMigrationState(state);\n\n console.log(` \\x1b[32m✓ Applied\\x1b[0m`);\n }\n}\n\n/**\n * Mark all known migrations as applied without running them.\n *\n * Called at the end of a fresh `setup()` run: a brand-new server has\n * already been configured correctly, so migrations that fix older\n * installations are not needed.\n */\nexport function markAllMigrationsApplied(): void {\n const state = loadMigrationState();\n\n for (const migration of ALL_MIGRATIONS) {\n if (!state.applied.includes(migration.id)) {\n state.applied.push(migration.id);\n }\n }\n\n saveMigrationState(state);\n}\n","import type { SetupOptions } from \"./types.js\";\nimport { step, info, error, setDryRun, resetSteps, isRoot } from \"./steps.js\";\nimport { createDdevUser } from \"./user.js\";\nimport { installDocker, configureDocker, setupDockerPrune } from \"./docker.js\";\nimport {\n installSystemDeps,\n installDdev,\n configureDdev,\n configureTraefik,\n readToolPorts,\n} from \"./ddev.js\";\nimport { configureDockerFirewall } from \"./docker-firewall.js\";\nimport { installNode, installAgent, createAgentConfig, createSystemdService } from \"./agent.js\";\nimport { hardenServer } from \"./hardening.js\";\nimport { runAudit, printAuditResults } from \"./audit.js\";\nimport { markAllMigrationsApplied } from \"./migrations/index.js\";\n\nexport * from \"./types.js\";\nexport * from \"./audit.js\";\n\n/**\n * Run the full server setup\n */\nexport async function setup(options: SetupOptions): Promise<void> {\n resetSteps();\n setDryRun(options.dryRun ?? false);\n\n console.log(\"\\n\\x1b[1m🚦 Trafic Server Setup\\x1b[0m\\n\");\n\n if (options.dryRun) {\n console.log(\"\\x1b[33m⚠ Dry-run mode: no changes will be made\\x1b[0m\\n\");\n }\n\n // Check root\n if (!isRoot() && !options.dryRun) {\n error(\"This command must be run as root\");\n console.log(\" Run: sudo trafic-agent setup ...\");\n process.exit(1);\n }\n\n info(`TLD: ${options.tld}`);\n if (options.email) {\n info(`Let's Encrypt email: ${options.email}`);\n }\n if (options.dnsProvider) {\n info(`Wildcard certificate: DNS-01 via ${options.dnsProvider}`);\n }\n if (options.noHardening) {\n info(\"Hardening: disabled\");\n }\n if (options.noRootSsh) {\n info(\"Root SSH login: disabled\");\n }\n\n try {\n // Step 1: Install system dependencies\n installSystemDeps();\n\n // Step 2: Create ddev user\n createDdevUser();\n\n // Step 3: Install Docker\n if (!options.noDocker) {\n installDocker();\n configureDocker();\n setupDockerPrune();\n } else {\n step(\"Install Docker\");\n info(\"Skipped (--no-docker)\");\n }\n\n // Step 4: Install DDEV\n if (!options.noDdev) {\n installDdev();\n configureDdev(options.tld, options.email);\n configureTraefik({\n tld: options.tld,\n tls: {\n dnsProvider: options.dnsProvider,\n caServer: options.caServer,\n dnsEnv: options.dnsEnv ?? {},\n },\n });\n // After DDEV, so the tool ports are known. UFW cannot reach these:\n // Docker's rules run before its chains.\n configureDockerFirewall(readToolPorts());\n } else {\n step(\"Install DDEV\");\n info(\"Skipped (--no-ddev)\");\n }\n\n // Step 5: Install Node.js\n installNode();\n\n // Step 6: Install and configure agent\n installAgent();\n createAgentConfig({\n tld: options.tld,\n trustedProxyHops: options.trustedProxyHops,\n dnsProvider: options.dnsProvider,\n dnsEnv: options.dnsEnv,\n });\n createSystemdService();\n\n // Step 7: Server hardening\n if (!options.noHardening) {\n hardenServer({\n sshUsers: options.sshUsers ?? [\"ddev\"],\n noRootSsh: options.noRootSsh,\n });\n } else {\n step(\"Server hardening\");\n info(\"Skipped (--no-hardening)\");\n }\n\n // Mark all migrations as applied — fresh servers are already up-to-date\n markAllMigrationsApplied();\n\n // Done\n console.log(\"\\n\\x1b[32m✓ Setup complete!\\x1b[0m\\n\");\n console.log(\"Next steps:\");\n console.log(` 1. Edit /etc/trafic/config.toml to configure authentication`);\n console.log(` 2. Add SSH public keys to /home/ddev/.ssh/authorized_keys`);\n console.log(` 3. Deploy your first project:`);\n console.log(` npx @studiometa/trafic-cli deploy --host=<this-server> --name=my-app`);\n console.log(`\\nYour projects will be available at https://<name>.${options.tld}`);\n } catch (err) {\n error(`Setup failed: ${err instanceof Error ? err.message : err}`);\n process.exit(1);\n }\n}\n\n/**\n * Run security audit\n */\nexport async function audit(): Promise<void> {\n const checks = runAudit();\n printAuditResults(checks);\n\n const failed = checks.filter((c) => c.status === \"fail\").length;\n if (failed > 0) {\n process.exit(1);\n }\n}\n"],"mappings":"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"}