@studiometa/trafic-agent 0.1.28 → 0.1.30

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.
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { _ as validateConfig, c as info, d as success, f as warn, h as loadConfig, i as runPendingMigrations, l as isRoot, n as setup, p as startIdleScheduler, r as listMigrations, s as exec, t as audit, u as step } from "./setup-BiaJHMQW.js";
2
+ import { _ as validateConfig, c as info, d as success, f as warn, h as loadConfig, i as runPendingMigrations, l as isRoot, n as setup, p as startIdleScheduler, r as listMigrations, s as exec, t as audit, u as step } from "./setup-DXihIjv9.js";
3
3
  import { o as closeDb, t as startServer } from "./server-zVWzPuj2.js";
4
4
  import { readFileSync } from "node:fs";
5
5
  import { execSync, spawnSync } from "node:child_process";
@@ -108,7 +108,7 @@ function runUpgrade(dryRun = false, reExecArgs) {
108
108
  }
109
109
  const alreadyReExeced = process.env["TRAFIC_UPGRADE_REEXEC"] === "1";
110
110
  step("Check for updates");
111
- const current = "0.1.28";
111
+ const current = "0.1.30";
112
112
  info(`Current version: ${current}`);
113
113
  const latest = fetchLatestVersion();
114
114
  if (!latest) warn("Could not reach npm registry — skipping version check");
@@ -191,7 +191,7 @@ function printHelp() {
191
191
  console.log(HELP.trim());
192
192
  }
193
193
  function printVersion() {
194
- console.log("0.1.28");
194
+ console.log("0.1.30");
195
195
  }
196
196
  async function runStart(values) {
197
197
  const config = loadConfig(values.config);
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { _ as validateConfig, a as printAuditResults, g as parseDuration, h as loadConfig, m as stopIdleProjects, n as setup, o as runAudit, p as startIdleScheduler, t as audit } from "./setup-BiaJHMQW.js";
1
+ import { _ as validateConfig, a as printAuditResults, g as parseDuration, h as loadConfig, m as stopIdleProjects, n as setup, o as runAudit, p as startIdleScheduler, t as audit } from "./setup-DXihIjv9.js";
2
2
  import { C as checkAuth, D as parseBearerToken, E as parseBasicAuth, S as watchProjectList, T as matchIp, _ as getProjectInfo, a as cleanOldLogs, b as startProject, c as getDb, d as initDb, f as logAccess, g as getProjectFromHostname, h as buildHostnameIndex, i as shouldNeverStop, l as getIdleProjects, m as updateProjectAccess, n as getIdleTimeoutMs, o as closeDb, p as setProjectStatus, r as loadProjectConfig, s as getAccessLogs, t as startServer, u as getProject, v as listProjects, w as matchHostname, x as stopProject, y as loadProjectList } from "./server-zVWzPuj2.js";
3
3
  export { audit, buildHostnameIndex, checkAuth, cleanOldLogs, closeDb, getAccessLogs, getDb, getIdleProjects, getIdleTimeoutMs, getProject, getProjectFromHostname, getProjectInfo, initDb, listProjects, loadConfig, loadProjectConfig, loadProjectList, logAccess, matchHostname, matchIp, parseBasicAuth, parseBearerToken, parseDuration, printAuditResults, runAudit, setProjectStatus, setup, shouldNeverStop, startIdleScheduler, startProject, startServer, stopIdleProjects, stopProject, updateProjectAccess, validateConfig, watchProjectList };
@@ -1 +1 @@
1
- {"version":3,"file":"ddev.d.ts","sourceRoot":"","sources":["../../src/setup/ddev.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,OAAO,EAAE,MAAM,SAAS,CAAC;AAG/C;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,GAAE,OAAgB,GAAG,IAAI,CAY5D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAAE,GAAE,OAAgB,GAAG,IAAI,CAWzD;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,EAAE,GAAE,OAAgB,GAAG,IAAI,CA2BtD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,MAAM,EACd,EAAE,GAAE,OAAgB,GACnB,IAAI,CAsBN;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,GAAE,OAAgB,GAAG,IAAI,CAsCtE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,GAAE,OAAgB,GAAG,MAAM,CAc/D;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,GAAE,OAAgB,GAAG,IAAI,CAkE3D"}
1
+ {"version":3,"file":"ddev.d.ts","sourceRoot":"","sources":["../../src/setup/ddev.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,OAAO,EAAE,MAAM,SAAS,CAAC;AAG/C;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,GAAE,OAAgB,GAAG,IAAI,CAY5D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAAE,GAAE,OAAgB,GAAG,IAAI,CAWzD;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,EAAE,GAAE,OAAgB,GAAG,IAAI,CA2BtD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,MAAM,EACd,EAAE,GAAE,OAAgB,GACnB,IAAI,CAsBN;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,GAAE,OAAgB,GAAG,IAAI,CAsCtE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,GAAE,OAAgB,GAAG,MAAM,CAY/D;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,GAAE,OAAgB,GAAG,IAAI,CAkE3D"}
@@ -6,10 +6,17 @@
6
6
  * writes instead of each module reaching for `node:fs` itself.
7
7
  */
8
8
  export interface SetupIo {
9
- /** Run a shell command. Returns its stdout when silent. */
9
+ /** Run a shell command. Throws when it exits non-zero. */
10
10
  exec(command: string, options?: {
11
11
  silent?: boolean;
12
12
  }): string;
13
+ /**
14
+ * Run a probe. Returns trimmed stdout, or "" when the command fails.
15
+ *
16
+ * Use this wherever a non-zero exit is an answer rather than an error —
17
+ * `id -u ddev` on a server without that user, for instance.
18
+ */
19
+ execSilent(command: string): string;
13
20
  /** Whether a command is on the PATH. */
14
21
  commandExists(command: string): boolean;
15
22
  /** Write a file. Honours dry-run. */
@@ -1 +1 @@
1
- {"version":3,"file":"io.d.ts","sourceRoot":"","sources":["../../src/setup/io.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,MAAM,WAAW,OAAO;IACtB,2DAA2D;IAC3D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,MAAM,CAAC;IAC9D,wCAAwC;IACxC,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;IACxC,qCAAqC;IACrC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,6BAA6B;IAC7B,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAClC,yBAAyB;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,sDAAsD;IACtD,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;CACzC;AAED;;;;;GAKG;AACH,eAAO,MAAM,MAAM,EAAE,OAOpB,CAAC"}
1
+ {"version":3,"file":"io.d.ts","sourceRoot":"","sources":["../../src/setup/io.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,MAAM,WAAW,OAAO;IACtB,0DAA0D;IAC1D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,MAAM,CAAC;IAC9D;;;;;OAKG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IACpC,wCAAwC;IACxC,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;IACxC,qCAAqC;IACrC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,6BAA6B;IAC7B,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAClC,yBAAyB;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,sDAAsD;IACtD,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;CACzC;AAED;;;;;GAKG;AACH,eAAO,MAAM,MAAM,EAAE,OAQpB,CAAC"}
@@ -274,6 +274,7 @@ function writeFile(path, content) {
274
274
  */
275
275
  var nodeIo = {
276
276
  exec,
277
+ execSilent: execSilent$1,
277
278
  commandExists,
278
279
  writeFile,
279
280
  fileExists: existsSync,
@@ -299,7 +300,7 @@ ddev ALL=(ALL) NOPASSWD: /usr/bin/ddev-hostname
299
300
  */
300
301
  function createDdevUser(io = nodeIo) {
301
302
  step("Create ddev user");
302
- if (io.exec("id -u ddev 2>/dev/null", { silent: true })) info("User 'ddev' already exists");
303
+ if (io.execSilent("id -u ddev")) info("User 'ddev' already exists");
303
304
  else {
304
305
  io.exec("useradd -m -s /bin/bash ddev");
305
306
  success("Created user 'ddev'");
@@ -444,9 +445,9 @@ function configureDdev(tld, email, io = nodeIo) {
444
445
  * `host.docker.internal` is not available on Linux without extra configuration.
445
446
  */
446
447
  function getDockerGatewayIp(io = nodeIo) {
447
- const ddevIp = io.exec("docker network inspect ddev_default --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}' 2>/dev/null", { silent: true })?.trim();
448
+ const ddevIp = io.execSilent("docker network inspect ddev_default --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}'");
448
449
  if (ddevIp) return ddevIp;
449
- return io.exec("docker network inspect bridge --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}' 2>/dev/null", { silent: true })?.trim() || "172.17.0.1";
450
+ return io.execSilent("docker network inspect bridge --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}'") || "172.17.0.1";
450
451
  }
451
452
  /**
452
453
  * Configure Traefik for forward auth
@@ -1423,4 +1424,4 @@ async function audit() {
1423
1424
  //#endregion
1424
1425
  export { validateConfig as _, printAuditResults as a, info as c, success as d, warn as f, parseDuration as g, loadConfig as h, runPendingMigrations as i, isRoot as l, stopIdleProjects as m, setup as n, runAudit as o, startIdleScheduler as p, listMigrations as r, exec as s, audit as t, step as u };
1425
1426
 
1426
- //# sourceMappingURL=setup-BiaJHMQW.js.map
1427
+ //# sourceMappingURL=setup-DXihIjv9.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup-DXihIjv9.js","names":[],"sources":["../src/utils/config.ts","../src/tasks/stop-idle.ts","../src/setup/steps.ts","../src/setup/io.ts","../src/setup/user.ts","../src/setup/docker.ts","../src/setup/ddev.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/index.ts","../src/setup/index.ts"],"sourcesContent":["import { readFileSync, existsSync } from \"node:fs\";\nimport { parse } from \"smol-toml\";\nimport type { AgentConfig, AuthConfig, AuthRule } 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};\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\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 };\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 return errors;\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 function stopIdleProjects(config: AgentConfig): 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 = 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 = 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 */\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 stopIdleProjects(config);\n\n // Then run on interval\n return setInterval(() => {\n stopIdleProjects(config);\n }, intervalMs);\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 { 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/**\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 };\n\n io.exec(\"mkdir -p /etc/docker\");\n\n const configPath = \"/etc/docker/daemon.json\";\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 // Reload Docker to apply config\n io.exec(\"systemctl reload docker || 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/**\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/**\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 }\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/**\n * Configure Traefik for forward auth\n */\nexport function configureTraefik(io: SetupIo = nodeIo): void {\n step(\"Configure Traefik for forward auth\");\n\n // Create custom Traefik config directories\n io.exec(\"mkdir -p /home/ddev/.ddev/traefik/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 // Dynamic configuration: defines the trafic-auth and trafic-errors middlewares\n // and the trafic-service backend pointing at the agent.\n // Written to:\n // - custom-global-config/ — picked up by DDEV 1.25+ (survives config dir purge on restart)\n // - traefik root — also watched by older DDEV versions\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 io.writeFile(\"/home/ddev/.ddev/traefik/trafic.yaml\", dynamicConfig);\n io.exec(\"chown ddev:ddev /home/ddev/.ddev/traefik/trafic.yaml\", { silent: true });\n io.writeFile(\"/home/ddev/.ddev/traefik/custom-global-config/trafic.yaml\", dynamicConfig);\n io.exec(\"chown ddev:ddev /home/ddev/.ddev/traefik/custom-global-config/trafic.yaml\", { silent: true });\n\n // Static configuration: attaches trafic-auth and trafic-errors to the\n // http-80 and http-443 entry points so every request goes through auth —\n // regardless of which project router handles it.\n // DDEV merges all static_config.*.yaml files into .static_config.yaml on start.\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 io.writeFile(\"/home/ddev/.ddev/traefik/static_config.trafic.yaml\", staticConfig);\n io.exec(\"chown ddev:ddev /home/ddev/.ddev/traefik/static_config.trafic.yaml\", { silent: true });\n\n success(\"Traefik configured with Trafic middleware\");\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: { tld: string; trustedProxyHops?: number },\n io: SetupIo = nodeIo,\n): void {\n const { tld, trustedProxyHops = DEFAULT_TRUSTED_PROXY_HOPS } = 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 if (io.fileExists(configPath)) {\n info(\"Configuration already exists at /etc/trafic/config.toml — skipping\");\n info(\"Edit this file to configure authentication\");\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`;\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 * 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\nReadWritePaths=/var/lib/trafic /home/ddev/.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/**\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# Use strong algorithms only\nKexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256\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(\"/etc/ssh/sshd_config.d/trafic.conf\", sshConfig);\n io.exec(\"chmod 644 /etc/ssh/sshd_config.d/trafic.conf\");\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 /etc/ssh/sshd_config.d/trafic.conf\");\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 * 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\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 // Configure for security updates only\n const upgradesConfig = `// 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};\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 io.writeFile(\n \"/etc/apt/apt.conf.d/50unattended-upgrades\",\n upgradesConfig,\n );\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 { execSync } from \"node:child_process\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport type { AuditCheck } from \"./types.js\";\n\n/**\n * Run a command silently and return output\n */\nfunction execSilent(command: string): string {\n try {\n return execSync(command, { encoding: \"utf-8\", stdio: \"pipe\" }).trim();\n } catch {\n return \"\";\n }\n}\n\n/**\n * Check if a service is active\n */\nfunction isServiceActive(service: string): boolean {\n const status = execSilent(`systemctl is-active ${service}`);\n return status === \"active\";\n}\n\n/**\n * Audit SSH configuration\n */\nfunction auditSsh(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Check root login\n const sshdConfig = existsSync(\"/etc/ssh/sshd_config\")\n ? readFileSync(\"/etc/ssh/sshd_config\", \"utf-8\")\n : \"\";\n const traficConfig = existsSync(\"/etc/ssh/sshd_config.d/trafic.conf\")\n ? readFileSync(\"/etc/ssh/sshd_config.d/trafic.conf\", \"utf-8\")\n : \"\";\n\n const config = sshdConfig + \"\\n\" + traficConfig;\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 */\nfunction auditFirewall(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n const ufwStatus = 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 */\nfunction auditServices(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Fail2ban\n const fail2banActive = isServiceActive(\"fail2ban\");\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\");\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\");\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 */\nfunction auditDocker(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Docker running\n const dockerActive = isServiceActive(\"docker\");\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 = execSilent(\n \"docker images -f dangling=true -q | wc -l\",\n );\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 = 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 */\nfunction auditPermissions(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Config file\n if (existsSync(\"/etc/trafic/config.toml\")) {\n const stats = 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 (existsSync(\"/home/ddev/www\")) {\n const owner = 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 * Run all audit checks\n */\nexport function runAudit(): AuditCheck[] {\n return [\n ...auditSsh(),\n ...auditFirewall(),\n ...auditServices(),\n ...auditDocker(),\n ...auditPermissions(),\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, 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\";\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];\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 { installSystemDeps, installDdev, configureDdev, configureTraefik } from \"./ddev.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.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 } 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 });\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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@studiometa/trafic-agent",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "Server agent for DDEV preview environments — auth, scale-to-zero, auto-start",
5
5
  "keywords": [
6
6
  "agent",
@@ -1 +0,0 @@
1
- {"version":3,"file":"setup-BiaJHMQW.js","names":[],"sources":["../src/utils/config.ts","../src/tasks/stop-idle.ts","../src/setup/steps.ts","../src/setup/io.ts","../src/setup/user.ts","../src/setup/docker.ts","../src/setup/ddev.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/index.ts","../src/setup/index.ts"],"sourcesContent":["import { readFileSync, existsSync } from \"node:fs\";\nimport { parse } from \"smol-toml\";\nimport type { AgentConfig, AuthConfig, AuthRule } 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};\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\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 };\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 return errors;\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 function stopIdleProjects(config: AgentConfig): 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 = 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 = 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 */\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 stopIdleProjects(config);\n\n // Then run on interval\n return setInterval(() => {\n stopIdleProjects(config);\n }, intervalMs);\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, 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. Returns its stdout when silent. */\n exec(command: string, options?: { silent?: boolean }): 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 commandExists,\n writeFile,\n fileExists: existsSync,\n readFile: (path) => readFileSync(path, \"utf-8\"),\n env: process.env,\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.exec(\"id -u ddev 2>/dev/null\", { silent: true });\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.exec(`cat ${authKeysPath}`, { silent: true });\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/**\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 };\n\n io.exec(\"mkdir -p /etc/docker\");\n\n const configPath = \"/etc/docker/daemon.json\";\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 // Reload Docker to apply config\n io.exec(\"systemctl reload docker || 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/**\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/**\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 }\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.exec(\n \"docker network inspect ddev_default --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}' 2>/dev/null\",\n { silent: true },\n )?.trim();\n if (ddevIp) return ddevIp;\n\n // Fall back to the default bridge network\n const bridgeIp = io.exec(\n \"docker network inspect bridge --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}' 2>/dev/null\",\n { silent: true },\n )?.trim();\n return bridgeIp || \"172.17.0.1\";\n}\n\n/**\n * Configure Traefik for forward auth\n */\nexport function configureTraefik(io: SetupIo = nodeIo): void {\n step(\"Configure Traefik for forward auth\");\n\n // Create custom Traefik config directories\n io.exec(\"mkdir -p /home/ddev/.ddev/traefik/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 // Dynamic configuration: defines the trafic-auth and trafic-errors middlewares\n // and the trafic-service backend pointing at the agent.\n // Written to:\n // - custom-global-config/ — picked up by DDEV 1.25+ (survives config dir purge on restart)\n // - traefik root — also watched by older DDEV versions\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 io.writeFile(\"/home/ddev/.ddev/traefik/trafic.yaml\", dynamicConfig);\n io.exec(\"chown ddev:ddev /home/ddev/.ddev/traefik/trafic.yaml\", { silent: true });\n io.writeFile(\"/home/ddev/.ddev/traefik/custom-global-config/trafic.yaml\", dynamicConfig);\n io.exec(\"chown ddev:ddev /home/ddev/.ddev/traefik/custom-global-config/trafic.yaml\", { silent: true });\n\n // Static configuration: attaches trafic-auth and trafic-errors to the\n // http-80 and http-443 entry points so every request goes through auth —\n // regardless of which project router handles it.\n // DDEV merges all static_config.*.yaml files into .static_config.yaml on start.\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 io.writeFile(\"/home/ddev/.ddev/traefik/static_config.trafic.yaml\", staticConfig);\n io.exec(\"chown ddev:ddev /home/ddev/.ddev/traefik/static_config.trafic.yaml\", { silent: true });\n\n success(\"Traefik configured with Trafic middleware\");\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: { tld: string; trustedProxyHops?: number },\n io: SetupIo = nodeIo,\n): void {\n const { tld, trustedProxyHops = DEFAULT_TRUSTED_PROXY_HOPS } = 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 if (io.fileExists(configPath)) {\n info(\"Configuration already exists at /etc/trafic/config.toml — skipping\");\n info(\"Edit this file to configure authentication\");\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`;\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 * 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\nReadWritePaths=/var/lib/trafic /home/ddev/.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/**\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# Use strong algorithms only\nKexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256\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(\"/etc/ssh/sshd_config.d/trafic.conf\", sshConfig);\n io.exec(\"chmod 644 /etc/ssh/sshd_config.d/trafic.conf\");\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 /etc/ssh/sshd_config.d/trafic.conf\");\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 * 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\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 // Configure for security updates only\n const upgradesConfig = `// 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};\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 io.writeFile(\n \"/etc/apt/apt.conf.d/50unattended-upgrades\",\n upgradesConfig,\n );\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 { execSync } from \"node:child_process\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport type { AuditCheck } from \"./types.js\";\n\n/**\n * Run a command silently and return output\n */\nfunction execSilent(command: string): string {\n try {\n return execSync(command, { encoding: \"utf-8\", stdio: \"pipe\" }).trim();\n } catch {\n return \"\";\n }\n}\n\n/**\n * Check if a service is active\n */\nfunction isServiceActive(service: string): boolean {\n const status = execSilent(`systemctl is-active ${service}`);\n return status === \"active\";\n}\n\n/**\n * Audit SSH configuration\n */\nfunction auditSsh(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Check root login\n const sshdConfig = existsSync(\"/etc/ssh/sshd_config\")\n ? readFileSync(\"/etc/ssh/sshd_config\", \"utf-8\")\n : \"\";\n const traficConfig = existsSync(\"/etc/ssh/sshd_config.d/trafic.conf\")\n ? readFileSync(\"/etc/ssh/sshd_config.d/trafic.conf\", \"utf-8\")\n : \"\";\n\n const config = sshdConfig + \"\\n\" + traficConfig;\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 */\nfunction auditFirewall(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n const ufwStatus = 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 */\nfunction auditServices(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Fail2ban\n const fail2banActive = isServiceActive(\"fail2ban\");\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\");\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\");\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 */\nfunction auditDocker(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Docker running\n const dockerActive = isServiceActive(\"docker\");\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 = execSilent(\n \"docker images -f dangling=true -q | wc -l\",\n );\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 = 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 */\nfunction auditPermissions(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Config file\n if (existsSync(\"/etc/trafic/config.toml\")) {\n const stats = 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 (existsSync(\"/home/ddev/www\")) {\n const owner = 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 * Run all audit checks\n */\nexport function runAudit(): AuditCheck[] {\n return [\n ...auditSsh(),\n ...auditFirewall(),\n ...auditServices(),\n ...auditDocker(),\n ...auditPermissions(),\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, 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\";\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];\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 { installSystemDeps, installDdev, configureDdev, configureTraefik } from \"./ddev.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.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 } 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 });\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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"}