@studiometa/trafic-agent 0.1.49 → 0.1.50
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/README.md +156 -13
- package/dist/cli.js +134 -14
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/{server-DZAo8Clg.js → server-B_iFvS6v.js} +366 -28
- package/dist/server-B_iFvS6v.js.map +1 -0
- package/dist/server.d.ts +21 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -2
- package/dist/setup/agent.d.ts +11 -1
- package/dist/setup/agent.d.ts.map +1 -1
- package/dist/setup/migrations/0020__backup_dir.d.ts +23 -0
- package/dist/setup/migrations/0020__backup_dir.d.ts.map +1 -0
- package/dist/setup/migrations/index.d.ts.map +1 -1
- package/dist/{setup-DWgb6dAD.js → setup-BbXL1ZFY.js} +826 -209
- package/dist/setup-BbXL1ZFY.js.map +1 -0
- package/dist/tasks/access-logs.d.ts +8 -0
- package/dist/tasks/access-logs.d.ts.map +1 -0
- package/dist/tasks/backup.d.ts +48 -0
- package/dist/tasks/backup.d.ts.map +1 -0
- package/dist/tasks/destroy-project.d.ts +71 -0
- package/dist/tasks/destroy-project.d.ts.map +1 -0
- package/dist/tasks/destroy-scheduled.d.ts +41 -0
- package/dist/tasks/destroy-scheduled.d.ts.map +1 -0
- package/dist/tasks/running-limit.d.ts +54 -0
- package/dist/tasks/running-limit.d.ts.map +1 -0
- package/dist/tasks/stop-idle.d.ts +12 -2
- package/dist/tasks/stop-idle.d.ts.map +1 -1
- package/dist/types.d.ts +41 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/config.d.ts +6 -0
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/db.d.ts +59 -1
- package/dist/utils/db.d.ts.map +1 -1
- package/dist/utils/ddev.d.ts +10 -3
- package/dist/utils/ddev.d.ts.map +1 -1
- package/dist/utils/paths.d.ts +3 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/package.json +1 -1
- package/dist/server-DZAo8Clg.js.map +0 -1
- package/dist/setup-DWgb6dAD.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-B_iFvS6v.js","names":[],"sources":["../src/utils/paths.ts","../src/utils/config.ts","../src/utils/auth.ts","../src/utils/ddev.ts","../src/setup/steps.ts","../src/setup/io.ts","../src/utils/db.ts","../src/utils/project-config.ts","../src/tasks/running-limit.ts","../src/setup/ddev.ts","../src/utils/traefik.ts","../src/utils/tls.ts","../src/server.ts"],"sourcesContent":["import { isAbsolute, relative, sep } from \"node:path\";\n\n/** Whether `child` is strictly inside `parent`. Both are resolved paths. */\nexport function isInside(parent: string, child: string): boolean {\n const path = relative(parent, child);\n return Boolean(path) && !isAbsolute(path) && path.split(sep)[0] !== \"..\";\n}\n","import { readFileSync, existsSync } from \"node:fs\";\nimport { isAbsolute, resolve } from \"node:path\";\nimport { parse } from \"smol-toml\";\nimport type { AgentConfig, AuthConfig, AuthRule, BackupConfig, TlsConfig } from \"../types.js\";\nimport { isInside } from \"./paths.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 maxRunningProjects: 0,\n auth: {\n defaultPolicy: \"basic\",\n allowedIps: [],\n tokens: [],\n basicAuth: [],\n rules: [],\n trustedProxyHops: 1,\n },\n tls: {\n dnsEnv: {},\n },\n backup: {\n dir: \"/var/backups/trafic\",\n retainDays: 14,\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/** Duration suffixes accepted in a timeout, in milliseconds. */\nconst TIMEOUT_UNITS: Record<string, number> = { s: 1000, m: 60 * 1000, h: 60 * 60 * 1000 };\n\n/**\n * Parse a command-line timeout such as \"10m\", \"90s\" or \"1h\"; a bare number\n * is minutes. The grammar is the one of `trafic --timeout`, which the CLI\n * passes on as is. Returns undefined for anything else, or for zero.\n */\nexport function parseTimeout(value: string): number | undefined {\n const match = /^(\\d+)([smh])?$/.exec(value.trim());\n\n if (!match || Number(match[1]) <= 0) {\n return undefined;\n }\n\n return Number(match[1]) * TIMEOUT_UNITS[match[2] ?? \"m\"]!;\n}\n\n/**\n * Load configuration from TOML file\n */\nexport function loadConfig(configPath?: string): AgentConfig {\n // Find config file\n let path = configPath;\n if (!path) {\n path = DEFAULT_CONFIG_PATHS.find((p) => existsSync(p));\n }\n\n if (!path || !existsSync(path)) {\n console.warn(\"No config file found, using defaults\");\n return DEFAULT_CONFIG;\n }\n\n const content = readFileSync(path, \"utf-8\");\n const parsed = parse(content) as Record<string, unknown>;\n\n // Merge with defaults\n const auth = mergeAuthConfig(parsed.auth as Record<string, unknown>);\n const tls = mergeTlsConfig(parsed.tls as Record<string, unknown>);\n const backup = mergeBackupConfig(parsed.backup 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 maxRunningProjects:\n (parsed.max_running_projects as number) ??\n DEFAULT_CONFIG.maxRunningProjects,\n auth,\n tls,\n backup,\n };\n}\n\n/**\n * Read the [backup] section. validateConfig reports a bad value.\n */\nfunction mergeBackupConfig(raw?: Record<string, unknown>): BackupConfig {\n if (!raw) return DEFAULT_CONFIG.backup;\n\n return {\n dir: (raw.dir as string | undefined) ?? DEFAULT_CONFIG.backup.dir,\n retainDays:\n (raw.retain_days as number | undefined) ?? DEFAULT_CONFIG.backup.retainDays,\n };\n}\n\n/**\n * Read the [tls] section.\n *\n * `dns_env` holds provider credentials, so its values are taken as-is and\n * only checked for being strings — validateConfig reports the rest.\n */\nfunction mergeTlsConfig(raw?: Record<string, unknown>): TlsConfig {\n if (!raw) return DEFAULT_CONFIG.tls;\n\n const env = (raw.dns_env as Record<string, unknown> | undefined) ?? {};\n const dnsEnv: Record<string, string> = {};\n\n for (const [key, value] of Object.entries(env)) {\n // Kept as read: a number or a boolean here is a config mistake, and\n // validateConfig says so rather than coercing it into a credential\n dnsEnv[key] = value as string;\n }\n\n return {\n dnsProvider: (raw.dns_provider as string | undefined) || undefined,\n caServer: (raw.ca_server as string | undefined) || undefined,\n dnsEnv,\n };\n}\n\n/**\n * Parse basic_auth entries from TOML.\n * Accepts either plain strings (\"user:pass\") or objects ({username, password}).\n */\nfunction parseBasicAuthEntries(raw: unknown): string[] | null {\n if (!Array.isArray(raw)) return null;\n return raw.map((entry) => {\n if (typeof entry === \"string\") return entry;\n if (typeof entry === \"object\" && entry !== null) {\n const { username, password } = entry as Record<string, unknown>;\n return `${username}:${password}`;\n }\n return String(entry);\n });\n}\n\nfunction mergeAuthConfig(raw?: Record<string, unknown>): AuthConfig {\n if (!raw) return DEFAULT_CONFIG.auth;\n\n const rules = (raw.rules as Array<Record<string, unknown>>) ?? [];\n\n return {\n defaultPolicy:\n (raw.default_policy as AuthConfig[\"defaultPolicy\"]) ??\n DEFAULT_CONFIG.auth.defaultPolicy,\n allowedIps:\n (raw.allowed_ips as string[]) ?? DEFAULT_CONFIG.auth.allowedIps,\n tokens: (raw.tokens as string[]) ?? DEFAULT_CONFIG.auth.tokens,\n basicAuth: parseBasicAuthEntries(raw.basic_auth) ?? DEFAULT_CONFIG.auth.basicAuth,\n rules: rules.map(\n (r): AuthRule => ({\n match: r.match as string,\n policy: r.policy as AuthRule[\"policy\"],\n tokens: r.tokens as string[] | undefined,\n allowedIps: r.allowed_ips as string[] | undefined,\n }),\n ),\n trustedProxyHops: parseTrustedProxyHops(raw.trusted_proxy_hops),\n };\n}\n\n/**\n * Parse trusted_proxy_hops, ignoring values that cannot be a hop count.\n *\n * A bad value falls back to the default rather than 0, because 0 would read\n * the socket peer — always the proxy — and quietly stop the IP allowlist and\n * per-hostname IP rules from ever matching.\n */\nfunction parseTrustedProxyHops(raw: unknown): number {\n const hops = typeof raw === \"number\" ? raw : Number(raw);\n\n if (!Number.isInteger(hops) || hops < 1) {\n return DEFAULT_CONFIG.auth.trustedProxyHops;\n }\n\n return hops;\n}\n\n/**\n * Validate configuration\n */\nexport function validateConfig(config: AgentConfig): string[] {\n const errors: string[] = [];\n\n if (!config.tld) {\n errors.push(\"tld is required\");\n }\n\n if (config.port < 1 || config.port > 65535) {\n errors.push(\"port must be between 1 and 65535\");\n }\n\n if (\n !Number.isInteger(config.maxRunningProjects) ||\n config.maxRunningProjects < 0\n ) {\n errors.push(\"max_running_projects must be a whole number, 0 or more\");\n }\n\n if (\n config.auth.defaultPolicy !== \"allow\" &&\n config.auth.defaultPolicy !== \"deny\" &&\n config.auth.defaultPolicy !== \"basic\" &&\n config.auth.defaultPolicy !== \"token\"\n ) {\n errors.push('auth.default_policy must be \"allow\", \"deny\", \"basic\", or \"token\"');\n }\n\n for (const [key, value] of Object.entries(config.tls.dnsEnv)) {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {\n errors.push(`tls.dns_env key \"${key}\" is not a valid environment variable name`);\n }\n\n if (typeof value !== \"string\") {\n errors.push(`tls.dns_env.${key} must be a string`);\n }\n }\n\n if (typeof config.backup.dir !== \"string\" || !isAbsolute(config.backup.dir)) {\n errors.push(\"backup.dir must be an absolute path\");\n } else {\n // trafic-agent destroy removes the project directories: a backup in\n // one of them would be removed right after it is written\n const backupDir = resolve(config.backup.dir);\n const projectsDir = resolve(config.projectsDir);\n\n if (backupDir === projectsDir || isInside(projectsDir, backupDir) || isInside(backupDir, projectsDir)) {\n errors.push(\"backup.dir must not be projects_dir, be inside it or contain it\");\n }\n }\n\n // 0 would remove each backup as soon as the sweep sees it\n if (!Number.isInteger(config.backup.retainDays) || config.backup.retainDays < 1) {\n errors.push(\"backup.retain_days must be a whole number of 1 or more\");\n }\n\n return errors;\n}\n","import type { AuthConfig, AuthRequest, AuthResult, AuthRule } from \"../types.js\";\n\n/**\n * Check if an IP matches a pattern (supports CIDR notation)\n */\nexport function matchIp(ip: string, pattern: string): boolean {\n // Exact match\n if (ip === pattern) return true;\n\n // CIDR notation\n if (pattern.includes(\"/\")) {\n return matchCidr(ip, pattern);\n }\n\n // Wildcard (e.g., 192.168.*)\n if (pattern.includes(\"*\")) {\n const regex = new RegExp(\n \"^\" + pattern.replace(/\\./g, \"\\\\.\").replace(/\\*/g, \".*\") + \"$\",\n );\n return regex.test(ip);\n }\n\n return false;\n}\n\n/**\n * Match IP against CIDR range\n */\nfunction matchCidr(ip: string, cidr: string): boolean {\n const [range, bits] = cidr.split(\"/\");\n const mask = parseInt(bits, 10);\n\n const ipNum = ipToNumber(ip);\n const rangeNum = ipToNumber(range);\n\n if (ipNum === null || rangeNum === null) return false;\n\n const maskNum = ~((1 << (32 - mask)) - 1);\n return (ipNum & maskNum) === (rangeNum & maskNum);\n}\n\n/**\n * Convert IP string to number\n */\nfunction ipToNumber(ip: string): number | null {\n const parts = ip.split(\".\");\n if (parts.length !== 4) return null;\n\n let num = 0;\n for (const part of parts) {\n const n = parseInt(part, 10);\n if (isNaN(n) || n < 0 || n > 255) return null;\n num = (num << 8) | n;\n }\n return num >>> 0; // Convert to unsigned\n}\n\n/**\n * Match hostname against a glob pattern\n */\nexport function matchHostname(hostname: string, pattern: string): boolean {\n // Exact match\n if (hostname === pattern) return true;\n\n // Glob pattern\n const regex = new RegExp(\n \"^\" +\n pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\*/g, \".*\")\n .replace(/\\?/g, \".\") +\n \"$\",\n );\n return regex.test(hostname);\n}\n\n/**\n * Parse Basic auth header\n */\nexport function parseBasicAuth(\n header: string,\n): { username: string; password: string } | null {\n if (!header.startsWith(\"Basic \")) return null;\n\n try {\n const encoded = header.slice(6);\n const decoded = Buffer.from(encoded, \"base64\").toString(\"utf-8\");\n const colonIndex = decoded.indexOf(\":\");\n if (colonIndex === -1) return null;\n\n return {\n username: decoded.slice(0, colonIndex),\n password: decoded.slice(colonIndex + 1),\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Parse Bearer token from header\n */\nexport function parseBearerToken(header: string): string | null {\n if (!header.startsWith(\"Bearer \")) return null;\n return header.slice(7);\n}\n\n/**\n * Resolve the client address from the socket peer and X-Forwarded-For.\n *\n * Every proxy appends the address it saw, so the entries grow left to right\n * and the rightmost ones were added by the proxies closest to us. Counting\n * `trustedHops` from the right therefore lands on the client, while anything\n * further left is whatever the client sent and must be ignored.\n *\n * Reading the leftmost entry instead — as this used to — let a client set\n * `X-Forwarded-For: <allowed-ip>` and match the IP allowlist, which grants\n * access unconditionally.\n *\n * Falls back to the socket peer whenever the header cannot be trusted to\n * hold that many entries.\n */\nexport function resolveClientIp(\n socketIp: string,\n forwardedFor: string | undefined,\n trustedHops = 1,\n): string {\n if (!forwardedFor || trustedHops < 1) {\n return socketIp;\n }\n\n const entries = forwardedFor\n .split(\",\")\n .map((entry) => entry.trim())\n .filter(Boolean);\n\n // Fewer entries than trusted proxies means the chain is shorter than\n // configured — the socket peer is the only address we can rely on.\n if (entries.length < trustedHops) {\n return socketIp;\n }\n\n return entries[entries.length - trustedHops] ?? socketIp;\n}\n\n/**\n * Check authentication for a request\n */\nexport function checkAuth(request: AuthRequest, config: AuthConfig): AuthResult {\n const { hostname, ip, authorization } = request;\n\n const realIp = resolveClientIp(ip, request.forwardedFor, config.trustedProxyHops);\n\n // Check IP whitelist first (always allows)\n for (const pattern of config.allowedIps) {\n if (matchIp(realIp, pattern)) {\n return { allowed: true, reason: \"ip\" };\n }\n }\n\n // Check per-hostname rules\n for (const rule of config.rules) {\n if (matchHostname(hostname, rule.match)) {\n return checkRule(rule, realIp, authorization, config);\n }\n }\n\n // Apply default policy\n return applyDefaultPolicy(config, realIp, authorization);\n}\n\n/**\n * Check a specific rule\n */\nfunction checkRule(\n rule: AuthRule,\n ip: string,\n authorization: string | undefined,\n config: AuthConfig,\n): AuthResult {\n // Rule-specific IP whitelist\n if (rule.allowedIps) {\n for (const pattern of rule.allowedIps) {\n if (matchIp(ip, pattern)) {\n return { allowed: true, reason: \"rule\" };\n }\n }\n }\n\n switch (rule.policy) {\n case \"allow\":\n return { allowed: true, reason: \"rule\" };\n\n case \"deny\":\n return { allowed: false, reason: \"rule\" };\n\n case \"token\": {\n if (!authorization) return { allowed: false, reason: \"rule\" };\n const token = parseBearerToken(authorization);\n const tokens = rule.tokens ?? config.tokens;\n if (token && tokens.includes(token)) {\n return { allowed: true, reason: \"token\" };\n }\n return { allowed: false, reason: \"rule\" };\n }\n\n case \"basic\": {\n if (!authorization) return { allowed: false, reason: \"rule\" };\n const creds = parseBasicAuth(authorization);\n if (creds) {\n const credString = `${creds.username}:${creds.password}`;\n if (config.basicAuth.includes(credString)) {\n return { allowed: true, reason: \"basic\" };\n }\n }\n return { allowed: false, reason: \"rule\" };\n }\n\n default:\n return { allowed: false, reason: \"rule\" };\n }\n}\n\n/**\n * Apply default policy\n */\nfunction applyDefaultPolicy(\n config: AuthConfig,\n ip: string,\n authorization: string | undefined,\n): AuthResult {\n switch (config.defaultPolicy) {\n case \"allow\":\n return { allowed: true, reason: \"default\" };\n\n case \"deny\":\n return { allowed: false, reason: \"default\" };\n\n case \"basic\": {\n if (!authorization) return { allowed: false, reason: \"default\" };\n const creds = parseBasicAuth(authorization);\n if (creds) {\n const credString = `${creds.username}:${creds.password}`;\n if (config.basicAuth.includes(credString)) {\n return { allowed: true, reason: \"basic\" };\n }\n }\n // Also check tokens as fallback\n const token = parseBearerToken(authorization);\n if (token && config.tokens.includes(token)) {\n return { allowed: true, reason: \"token\" };\n }\n return { allowed: false, reason: \"default\" };\n }\n\n case \"token\": {\n if (!authorization) return { allowed: false, reason: \"default\" };\n const token = parseBearerToken(authorization);\n if (token && config.tokens.includes(token)) {\n return { allowed: true, reason: \"token\" };\n }\n return { allowed: false, reason: \"default\" };\n }\n\n default:\n return { allowed: false, reason: \"default\" };\n }\n}\n","import { readFileSync, existsSync, watch } from \"node:fs\";\nimport { execFile } from \"node:child_process\";\nimport type { DdevProject } from \"../types.js\";\n\n/** Strip a single pair of surrounding quotes. */\nfunction unquote(value: string): string {\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n return value.slice(1, -1);\n }\n\n return value;\n}\n\n/** A project name at the start of a line, e.g. `my-project:` */\nconst PROJECT_LINE = /^(\\S[^:]*):\\s*$/;\n\n/** An indented `approot: /path` belonging to the project above it */\nconst APPROOT_LINE = /^\\s+approot:\\s*(.+?)\\s*$/;\n\n/**\n * Load projects from DDEV's project_list.yaml\n * Returns a map of project name -> app root path\n *\n * The file nests the path under the project name:\n *\n * scalian:\n * approot: /home/ddev/www/scalian\n *\n * Indentation therefore has to be read. A flat key/value pass produces one\n * entry per line instead: the project mapped to an empty string, plus a\n * phantom project called `approot`. Both did damage — the empty path meant\n * `loadProjectConfig` looked for `.ddev/config.trafic.yaml` relative to the\n * working directory and never found it, so per-project settings were silently\n * ignored, and the phantom entered the hostname index, where the TLS ask\n * endpoint would vouch for `approot.<tld>` and have a certificate issued for\n * a project that does not exist.\n */\nexport function loadProjectList(\n projectListPath: string,\n): Map<string, string> {\n if (!existsSync(projectListPath)) {\n return new Map();\n }\n\n const projects = new Map<string, string>();\n let current: string | undefined;\n\n for (const line of readFileSync(projectListPath, \"utf-8\").split(\"\\n\")) {\n if (!line.trim() || line.trimStart().startsWith(\"#\")) {\n continue;\n }\n\n const project = PROJECT_LINE.exec(line);\n\n if (project) {\n current = unquote(project[1]!.trim());\n // Recorded now so a project keeps its hostname even if DDEV writes no\n // approot for it\n projects.set(current, \"\");\n continue;\n }\n\n const approot = APPROOT_LINE.exec(line);\n\n if (approot && current) {\n projects.set(current, unquote(approot[1]!));\n }\n }\n\n return projects;\n}\n\n/**\n * Build a hostname -> project name index\n */\nexport function buildHostnameIndex(\n projects: Map<string, string>,\n tld: string,\n): Map<string, string> {\n const index = new Map<string, string>();\n\n for (const [name] of projects) {\n // Primary hostname: project-name.tld\n index.set(`${name}.${tld}`, name);\n }\n\n return index;\n}\n\n/**\n * Get project name from hostname\n */\nexport function getProjectFromHostname(\n hostname: string,\n index: Map<string, string>,\n): string | undefined {\n return index.get(hostname);\n}\n\n/**\n * Watch project_list.yaml for changes\n */\nexport function watchProjectList(\n projectListPath: string,\n onChange: () => void,\n): void {\n if (!existsSync(projectListPath)) {\n console.warn(`Project list not found: ${projectListPath}`);\n return;\n }\n\n watch(projectListPath, (eventType) => {\n if (eventType === \"change\") {\n onChange();\n }\n });\n}\n\n/**\n * Run a ddev command without blocking the event loop.\n *\n * The agent is single-threaded and serves forward auth for every project, so\n * an `execSync` here stops the whole server for the duration of the command.\n * `ddev start` takes over a minute, which froze auth for every unrelated\n * project and produced request timeouts on a live server. Nothing about\n * \"background\" work with setTimeout helps: the timer callback still runs on\n * the same thread.\n */\nexport interface DdevResult {\n ok: boolean;\n stdout: string;\n}\n\n/**\n * Runs a ddev command. Injected so tests drive the outcome directly instead\n * of replacing node:child_process for the whole module.\n *\n * `cwd` is for the commands that act on the project in that directory rather\n * than on a project named in `args`.\n */\nexport type DdevRunner = (\n args: string[],\n timeoutMs: number,\n cwd?: string,\n) => Promise<DdevResult>;\n\n/** The real runner, used unless a caller passes its own. */\nexport const nodeDdevRunner: DdevRunner = (args, timeoutMs, cwd) =>\n new Promise((resolve) => {\n execFile(\n \"ddev\",\n args,\n { encoding: \"utf-8\", timeout: timeoutMs, cwd },\n (error, stdout) => {\n if (error) {\n console.error(`ddev ${args.join(\" \")} failed:`, error.message);\n resolve({ ok: false, stdout: stdout ?? \"\" });\n return;\n }\n\n resolve({ ok: true, stdout: stdout ?? \"\" });\n },\n );\n });\n\n/**\n * Get detailed project info using ddev describe\n */\nexport async function getProjectInfo(\n name: string,\n run: DdevRunner = nodeDdevRunner,\n): Promise<DdevProject | undefined> {\n const { ok, stdout } = await run([\"describe\", name, \"-j\"], 10000);\n\n if (!ok) {\n return undefined;\n }\n\n try {\n const data = JSON.parse(stdout);\n\n if (!data.raw) return undefined;\n\n const raw = data.raw;\n return {\n name: raw.name,\n status: raw.status,\n appRoot: raw.approot,\n httpURLs: raw.httpURLs ?? [],\n httpsURLs: raw.httpsURLs ?? [],\n type: raw.type,\n phpVersion: raw.php_version,\n // DDEV only describes a database when the project has one\n dbType: raw.dbinfo?.database_type,\n };\n } catch {\n return undefined;\n }\n}\n\n/**\n * Start a DDEV project.\n *\n * Resolves when the project is up. Callers in the request path must not await\n * it — respond first, then let this settle and record the outcome.\n */\nexport async function startProject(\n name: string,\n run: DdevRunner = nodeDdevRunner,\n): Promise<boolean> {\n const { ok } = await run([\"start\", name], 120000);\n return ok;\n}\n\n/**\n * Stop a DDEV project\n */\nexport async function stopProject(\n name: string,\n run: DdevRunner = nodeDdevRunner,\n): Promise<boolean> {\n const { ok } = await run([\"stop\", name], 60000);\n return ok;\n}\n\n/**\n * List all DDEV projects with their live status.\n *\n * `ddev list -j` asks Docker, so a project started or stopped outside the\n * agent (by `trafic deploy`, say) shows its real state here. It wraps the\n * entries in a `raw` array.\n */\nexport async function listProjects(\n run: DdevRunner = nodeDdevRunner,\n): Promise<DdevProject[]> {\n const { ok, stdout } = await run([\"list\", \"-j\"], 30000);\n\n if (!ok) {\n return [];\n }\n\n try {\n const data = JSON.parse(stdout);\n\n if (!Array.isArray(data.raw)) return [];\n\n return data.raw.map((p: Record<string, unknown>) => ({\n name: p.name as string,\n status: p.status as DdevProject[\"status\"],\n appRoot: p.approot as string,\n httpURLs: (p.httpURLs as string[]) ?? [],\n httpsURLs: (p.httpsURLs as string[]) ?? [],\n type: p.type as string,\n }));\n } catch {\n return [];\n }\n}\n","import { execSync } from \"node:child_process\";\nimport { writeFileSync as fsWriteFile } from \"node:fs\";\n\nlet stepNumber = 0;\nlet dryRun = false;\n\n/**\n * Set dry-run mode\n */\nexport function setDryRun(value: boolean): void {\n dryRun = value;\n}\n\n/**\n * Reset step counter\n */\nexport function resetSteps(): void {\n stepNumber = 0;\n}\n\n/**\n * Log a step\n */\nexport function step(message: string): void {\n stepNumber++;\n console.log(`\\n\\x1b[36m[${stepNumber}]\\x1b[0m ${message}`);\n}\n\n/**\n * Log success\n */\nexport function success(message: string): void {\n console.log(` \\x1b[32m✓\\x1b[0m ${message}`);\n}\n\n/**\n * Log warning\n */\nexport function warn(message: string): void {\n console.log(` \\x1b[33m⚠\\x1b[0m ${message}`);\n}\n\n/**\n * Log error\n */\nexport function error(message: string): void {\n console.log(` \\x1b[31m✗\\x1b[0m ${message}`);\n}\n\n/**\n * Log info\n */\nexport function info(message: string): void {\n console.log(` ${message}`);\n}\n\n/**\n * Execute a command (or show in dry-run mode)\n */\nexport function exec(command: string, options?: { silent?: boolean }): string {\n if (dryRun) {\n if (!options?.silent) {\n console.log(` \\x1b[90m$ ${command}\\x1b[0m`);\n }\n return \"\";\n }\n\n try {\n return execSync(command, {\n encoding: \"utf-8\",\n stdio: options?.silent ? \"pipe\" : \"inherit\",\n });\n } catch (err) {\n const e = err as { status?: number; stderr?: string };\n throw new Error(`Command failed: ${command}\\n${e.stderr ?? \"\"}`);\n }\n}\n\n/**\n * Execute a command and return output (silent)\n */\nexport function execSilent(command: string): string {\n if (dryRun) {\n return \"\";\n }\n\n try {\n return execSync(command, {\n encoding: \"utf-8\",\n stdio: \"pipe\",\n }).trim();\n } catch {\n return \"\";\n }\n}\n\n/**\n * Check if a command exists\n */\nexport function commandExists(command: string): boolean {\n try {\n execSync(`which ${command}`, { stdio: \"pipe\" });\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Check if running as root\n */\nexport function isRoot(): boolean {\n return process.getuid?.() === 0;\n}\n\n/**\n * Write a file (or show in dry-run mode)\n */\nexport function writeFile(path: string, content: string): void {\n if (dryRun) {\n console.log(` \\x1b[90mWrite ${path}:\\x1b[0m`);\n const lines = content.split(\"\\n\").slice(0, 5);\n for (const line of lines) {\n console.log(` \\x1b[90m ${line}\\x1b[0m`);\n }\n if (content.split(\"\\n\").length > 5) {\n console.log(` \\x1b[90m ...\\x1b[0m`);\n }\n return;\n }\n\n fsWriteFile(path, content);\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { commandExists, exec, execSilent, writeFile } from \"./steps.js\";\n\n/**\n * The effectful operations the setup steps need.\n *\n * Injected into every setup function so tests can drive them with plain\n * fakes — no module mocking — and so dry-run has one place to intercept\n * writes instead of each module reaching for `node:fs` itself.\n */\nexport interface SetupIo {\n /** Run a shell command. Throws when it exits non-zero. */\n exec(command: string, options?: { silent?: boolean }): string;\n /**\n * Run a probe. Returns trimmed stdout, or \"\" when the command fails.\n *\n * Use this wherever a non-zero exit is an answer rather than an error —\n * `id -u ddev` on a server without that user, for instance.\n */\n execSilent(command: string): string;\n /** Whether a command is on the PATH. */\n commandExists(command: string): boolean;\n /** Write a file. Honours dry-run. */\n writeFile(path: string, content: string): void;\n /** Whether a path exists. */\n fileExists(path: string): boolean;\n /** Read a UTF-8 file. */\n readFile(path: string): string;\n /** Process environment, for values like SUDO_USER. */\n env: Record<string, string | undefined>;\n}\n\n/**\n * The real implementation, used when no io is injected.\n *\n * `writeFile` comes from steps.js rather than `node:fs` so writes are\n * skipped in dry-run mode. Reads stay direct: they change nothing.\n */\nexport const nodeIo: SetupIo = {\n exec,\n execSilent,\n commandExists,\n writeFile,\n fileExists: existsSync,\n readFile: (path) => readFileSync(path, \"utf-8\"),\n env: process.env,\n};\n","import { DatabaseSync } from \"node:sqlite\";\nimport type { ProjectRecord, AccessLog, ScheduledDestroy } from \"../types.js\";\n\n/**\n * Reads the current time in epoch milliseconds.\n *\n * Injected so a test can place a project's last access at a chosen moment\n * without freezing the clock for everything else in the process.\n */\nexport type Clock = () => number;\n\n/** The real clock, used unless a caller passes its own. */\nexport const systemClock: Clock = () => Date.now();\n\nlet db: DatabaseSync | null = null;\n\n/**\n * How long a write waits for another connection to release the lock.\n *\n * The service and a `trafic-agent destroy` run over SSH open the same file.\n * WAL lets readers and one writer work together, but a second writer fails\n * at once with \"database is locked\" unless it is told to wait.\n */\nconst BUSY_TIMEOUT_MS = 5000;\n\n/**\n * Initialize the database connection and schema\n */\nexport function initDb(dbPath: string): DatabaseSync {\n db = new DatabaseSync(dbPath, { timeout: BUSY_TIMEOUT_MS });\n db.exec(\"PRAGMA journal_mode=WAL\");\n\n // Create tables\n db.exec(`\n CREATE TABLE IF NOT EXISTS projects (\n name TEXT PRIMARY KEY,\n last_access INTEGER NOT NULL,\n status TEXT NOT NULL DEFAULT 'stopped',\n status_pid INTEGER\n );\n\n CREATE TABLE IF NOT EXISTS access_logs (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project TEXT NOT NULL,\n timestamp INTEGER NOT NULL,\n ip TEXT NOT NULL,\n user_agent TEXT NOT NULL,\n path TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_access_logs_project ON access_logs(project);\n CREATE INDEX IF NOT EXISTS idx_access_logs_timestamp ON access_logs(timestamp);\n\n CREATE TABLE IF NOT EXISTS scheduled_destroys (\n name TEXT PRIMARY KEY,\n destroy_at INTEGER NOT NULL,\n backup INTEGER NOT NULL DEFAULT 0,\n timeout_ms INTEGER,\n removing INTEGER NOT NULL DEFAULT 0\n );\n `);\n\n // Databases made before the column existed\n addColumn(db, \"projects\", \"status_pid INTEGER\");\n\n return db;\n}\n\n/**\n * Add a column to a table that does not have it yet. The service and a\n * `trafic-agent destroy` run can both get here first, so a column that\n * exists already is not an error.\n */\nfunction addColumn(database: DatabaseSync, table: string, column: string): void {\n try {\n database.exec(`ALTER TABLE ${table} ADD COLUMN ${column}`);\n } catch (error) {\n if (!/duplicate column name/i.test((error as Error).message)) {\n throw error;\n }\n }\n}\n\n/**\n * Get the database instance\n */\nexport function getDb(): DatabaseSync {\n if (!db) {\n throw new Error(\"Database not initialized. Call initDb first.\");\n }\n return db;\n}\n\n/**\n * Close the database connection\n */\nexport function closeDb(): void {\n if (db) {\n db.close();\n db = null;\n }\n}\n\n/**\n * Update project last access time\n */\nexport function updateProjectAccess(name: string, now: Clock = systemClock): void {\n const database = getDb();\n const stmt = database.prepare(`\n INSERT INTO projects (name, last_access, status)\n VALUES (?, ?, 'running')\n ON CONFLICT(name) DO UPDATE SET last_access = excluded.last_access\n `);\n stmt.run(name, now());\n}\n\n/**\n * Get project record\n */\nexport function getProject(name: string): ProjectRecord | undefined {\n const database = getDb();\n const stmt = database.prepare(\n \"SELECT name, last_access as lastAccess, status FROM projects WHERE name = ?\",\n );\n return stmt.get(name) as unknown as ProjectRecord | undefined;\n}\n\n/**\n * The statuses a process sets while it works on a project: \"starting\" (the\n * waiting page, in the service) and \"destroying\" (`trafic-agent destroy`).\n */\nconst BUSY_STATUSES: readonly ProjectRecord[\"status\"][] = [\"starting\", \"destroying\"];\n\n/**\n * Set project status\n *\n * A busy status (\"starting\", \"destroying\") records the id of this process,\n * so releaseStaleStatuses can tell when the process that set it is gone.\n */\nexport function setProjectStatus(\n name: string,\n status: ProjectRecord[\"status\"],\n now: Clock = systemClock,\n): void {\n const database = getDb();\n const stmt = database.prepare(`\n INSERT INTO projects (name, last_access, status, status_pid)\n VALUES (?, ?, ?, ?)\n ON CONFLICT(name) DO UPDATE SET\n status = excluded.status,\n status_pid = excluded.status_pid\n `);\n stmt.run(name, now(), status, BUSY_STATUSES.includes(status) ? process.pid : null);\n}\n\n/**\n * Whether a process is alive. EPERM means it is, but belongs to another user.\n */\nexport function isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\n/**\n * Set back to \"running\" the busy statuses (\"starting\", \"destroying\") whose\n * process is gone, and return the names of those projects.\n *\n * The process that sets a busy status sets it back when it ends. A process\n * that is killed (a cancelled CI job, a dropped SSH connection, a restart of\n * the service) cannot, and the status would stay: the sweeps never stop a\n * busy project, max_running_projects counts it as running, and the waiting\n * page never starts it. \"running\" lets the idle sweep check the project\n * against DDEV and stop it, also when it is stopped already.\n *\n * `isAlive` says whether a process still works on its project: the service\n * passes its own check at start, where its own id is not alive yet.\n */\nexport function releaseStaleStatuses(\n isAlive: (pid: number) => boolean = isProcessAlive,\n): string[] {\n const database = getDb();\n const rows = database\n .prepare(\n \"SELECT name, status, status_pid AS pid FROM projects WHERE status IN ('starting', 'destroying')\",\n )\n .all() as unknown as { name: string; status: string; pid: number | null }[];\n // Only when the same process still holds the status: another one may\n // have set it since the SELECT\n const release = database.prepare(`\n UPDATE projects SET status = 'running', status_pid = NULL\n WHERE name = ? AND status = ? AND status_pid IS ?\n `);\n const released: string[] = [];\n\n for (const { name, status, pid } of rows) {\n if (pid !== null && isAlive(pid)) {\n continue;\n }\n\n if (Number(release.run(name, status, pid).changes) > 0) {\n released.push(name);\n }\n }\n\n return released;\n}\n\n/**\n * Get idle projects (last access older than threshold)\n */\nexport function getIdleProjects(\n thresholdMs: number,\n now: Clock = systemClock,\n): ProjectRecord[] {\n const database = getDb();\n const cutoff = now() - thresholdMs;\n const stmt = database.prepare(`\n SELECT name, last_access as lastAccess, status\n FROM projects\n WHERE last_access < ? AND status = 'running'\n `);\n return stmt.all(cutoff) as unknown as ProjectRecord[];\n}\n\n/**\n * Delete every record of a project: its status row, its access logs and its\n * scheduled removal.\n */\nexport function deleteProject(name: string): void {\n const database = getDb();\n database.prepare(\"DELETE FROM projects WHERE name = ?\").run(name);\n database.prepare(\"DELETE FROM access_logs WHERE project = ?\").run(name);\n database.prepare(\"DELETE FROM scheduled_destroys WHERE name = ?\").run(name);\n}\n\n/**\n * Schedule the removal of a project, or replace a removal already\n * scheduled. `timeoutMs` is the time limit of the backup.\n */\nexport function scheduleDestroy(\n name: string,\n destroyAt: number,\n backup: boolean,\n timeoutMs?: number,\n): void {\n const database = getDb();\n const stmt = database.prepare(`\n INSERT INTO scheduled_destroys (name, destroy_at, backup, timeout_ms)\n VALUES (?, ?, ?, ?)\n ON CONFLICT(name) DO UPDATE SET\n destroy_at = excluded.destroy_at,\n backup = excluded.backup,\n timeout_ms = excluded.timeout_ms\n `);\n stmt.run(name, destroyAt, backup ? 1 : 0, timeoutMs ?? null);\n}\n\n/** A row of scheduled_destroys. */\ninterface ScheduledDestroyRow {\n name: string;\n destroyAt: number;\n backup: number;\n timeoutMs: number | null;\n removing: number;\n}\n\nfunction toScheduledDestroy({\n name,\n destroyAt,\n backup,\n timeoutMs,\n removing,\n}: ScheduledDestroyRow): ScheduledDestroy {\n return {\n name,\n destroyAt,\n backup: backup === 1,\n ...(timeoutMs === null ? {} : { timeoutMs }),\n removing: removing === 1,\n };\n}\n\n/** The columns of a ScheduledDestroy. */\nconst SCHEDULED_DESTROY_COLUMNS =\n \"name, destroy_at as destroyAt, backup, timeout_ms as timeoutMs, removing\";\n\n/**\n * Cancel the scheduled removal of a project:\n * - \"cancelled\" when it was scheduled and is now cancelled\n * - \"none\" when there was none\n * - \"removing\" when the sweep already removes the project (see\n * startScheduledDestroy): it is too late, and the removal stays\n */\nexport function cancelScheduledDestroy(name: string): \"cancelled\" | \"none\" | \"removing\" {\n const database = getDb();\n const result = database\n .prepare(\"DELETE FROM scheduled_destroys WHERE name = ? AND removing = 0\")\n .run(name);\n\n if (Number(result.changes) > 0) {\n return \"cancelled\";\n }\n\n return getScheduledDestroy(name) ? \"removing\" : \"none\";\n}\n\n/**\n * Mark a scheduled removal as started, when it is still due at `dueBy`.\n * From then on, a cancel is refused, and a retry after a failure does not\n * back the project up again. Returns false when the removal was cancelled\n * or moved to a later date meanwhile. One statement, so a cancel from\n * another process comes either before (false) or after (refused).\n */\nexport function startScheduledDestroy(name: string, dueBy: number): boolean {\n const database = getDb();\n const result = database\n .prepare(\"UPDATE scheduled_destroys SET removing = 1 WHERE name = ? AND destroy_at <= ?\")\n .run(name, dueBy);\n return Number(result.changes) > 0;\n}\n\n/**\n * Get the scheduled removals, soonest first. With `dueBy`, only the removals\n * due at or before that time.\n */\nexport function getScheduledDestroys(dueBy = Number.MAX_SAFE_INTEGER): ScheduledDestroy[] {\n const database = getDb();\n const stmt = database.prepare(`\n SELECT ${SCHEDULED_DESTROY_COLUMNS}\n FROM scheduled_destroys\n WHERE destroy_at <= ?\n ORDER BY destroy_at, name\n `);\n const rows = stmt.all(dueBy) as unknown as ScheduledDestroyRow[];\n return rows.map(toScheduledDestroy);\n}\n\n/**\n * Get the scheduled removal of a project when it is due at or before\n * `dueBy`, or undefined when there is none.\n */\nexport function getScheduledDestroy(\n name: string,\n dueBy = Number.MAX_SAFE_INTEGER,\n): ScheduledDestroy | undefined {\n const database = getDb();\n const stmt = database.prepare(`\n SELECT ${SCHEDULED_DESTROY_COLUMNS}\n FROM scheduled_destroys\n WHERE name = ? AND destroy_at <= ?\n `);\n const row = stmt.get(name, dueBy) as ScheduledDestroyRow | undefined;\n return row && toScheduledDestroy(row);\n}\n\n/**\n * Log an access\n */\nexport function logAccess(log: Omit<AccessLog, \"id\">): void {\n const database = getDb();\n const stmt = database.prepare(`\n INSERT INTO access_logs (project, timestamp, ip, user_agent, path)\n VALUES (?, ?, ?, ?, ?)\n `);\n stmt.run(log.project, log.timestamp, log.ip, log.userAgent, log.path);\n}\n\n/**\n * Get recent access logs for a project\n */\nexport function getAccessLogs(project: string, limit = 100): AccessLog[] {\n const database = getDb();\n const stmt = database.prepare(`\n SELECT id, project, timestamp, ip, user_agent as userAgent, path\n FROM access_logs\n WHERE project = ?\n ORDER BY timestamp DESC\n LIMIT ?\n `);\n return stmt.all(project, limit) as unknown as AccessLog[];\n}\n\n/**\n * Clean old access logs (older than given days)\n */\nexport function cleanOldLogs(days: number, now: Clock = systemClock): number {\n const database = getDb();\n const cutoff = now() - days * 24 * 60 * 60 * 1000;\n const stmt = database.prepare(\"DELETE FROM access_logs WHERE timestamp < ?\");\n const result = stmt.run(cutoff);\n return Number(result.changes);\n}\n","import { readFileSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/**\n * Per-project Trafic configuration\n * Read from .ddev/config.trafic.yaml\n */\nexport interface ProjectConfig {\n /** Auth policy override: allow | deny | basic | token */\n auth_policy?: \"allow\" | \"deny\" | \"basic\" | \"token\";\n /** Idle timeout override: \"never\" or duration like \"30m\", \"4h\" */\n idle_timeout?: string;\n}\n\n/**\n * Simple YAML parser for config.trafic.yaml\n * Only supports simple key: value pairs (no nesting, no arrays)\n */\nfunction parseSimpleYaml(content: string): Record<string, string> {\n const result: Record<string, string> = {};\n const lines = content.split(\"\\n\");\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Skip empty lines and comments\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n\n const colonIndex = trimmed.indexOf(\":\");\n if (colonIndex === -1) continue;\n\n const key = trimmed.slice(0, colonIndex).trim();\n let value = trimmed.slice(colonIndex + 1).trim();\n\n // Remove quotes if present\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n value = value.slice(1, -1);\n }\n\n result[key] = value;\n }\n\n return result;\n}\n\n/**\n * Load project-specific Trafic config from .ddev/config.trafic.yaml\n */\nexport function loadProjectConfig(projectDir: string): ProjectConfig {\n const configPath = join(projectDir, \".ddev\", \"config.trafic.yaml\");\n\n if (!existsSync(configPath)) {\n return {};\n }\n\n try {\n const content = readFileSync(configPath, \"utf-8\");\n const parsed = parseSimpleYaml(content);\n\n const config: ProjectConfig = {};\n\n // Validate auth_policy\n if (parsed.auth_policy) {\n const policy = parsed.auth_policy.toLowerCase();\n if ([\"allow\", \"deny\", \"basic\", \"token\"].includes(policy)) {\n config.auth_policy = policy as ProjectConfig[\"auth_policy\"];\n }\n }\n\n // Validate idle_timeout\n if (parsed.idle_timeout) {\n config.idle_timeout = parsed.idle_timeout;\n }\n\n return config;\n } catch (error) {\n console.warn(`Failed to load project config from ${configPath}:`, error);\n return {};\n }\n}\n\n/**\n * Check if a project should never be stopped (idle_timeout = \"never\")\n */\nexport function shouldNeverStop(config: ProjectConfig): boolean {\n return config.idle_timeout?.toLowerCase() === \"never\";\n}\n\n/**\n * Get idle timeout in milliseconds, or undefined to use global default\n */\nexport function getIdleTimeoutMs(config: ProjectConfig): number | undefined {\n if (!config.idle_timeout) return undefined;\n if (shouldNeverStop(config)) return undefined;\n\n // Parse duration string (e.g., \"30m\", \"4h\", \"2h30m\")\n const regex = /(?:(\\d+)h)?(?:(\\d+)m)?(?:(\\d+)s)?/;\n const match = config.idle_timeout.match(regex);\n if (!match) return undefined;\n\n const hours = parseInt(match[1] ?? \"0\", 10);\n const minutes = parseInt(match[2] ?? \"0\", 10);\n const seconds = parseInt(match[3] ?? \"0\", 10);\n\n const ms = (hours * 3600 + minutes * 60 + seconds) * 1000;\n return ms > 0 ? ms : undefined;\n}\n","import {\n getProject,\n setProjectStatus,\n updateProjectAccess,\n} from \"../utils/db.js\";\nimport {\n listProjects,\n nodeDdevRunner,\n stopProject,\n type DdevRunner,\n} from \"../utils/ddev.js\";\nimport type { DdevProject, ProjectRecord } from \"../types.js\";\n\n/**\n * What the limit needs from the outside world. Injected so a test can set\n * the DDEV state and record the stops without a DDEV install.\n */\nexport interface RunningLimitDeps {\n listProjects: () => Promise<DdevProject[]>;\n stopProject: (name: string) => Promise<boolean>;\n getProject: typeof getProject;\n setProjectStatus: (name: string, status: \"running\" | \"stopped\") => void;\n updateProjectAccess: (name: string) => void;\n}\n\n/**\n * The real dependencies: the agent database, and DDEV through `run`. The\n * default runner is the real one.\n */\nexport function ddevRunningLimitDeps(\n run: DdevRunner = nodeDdevRunner,\n): RunningLimitDeps {\n return {\n listProjects: () => listProjects(run),\n stopProject: (name) => stopProject(name, run),\n getProject,\n setProjectStatus: (name, status) => setProjectStatus(name, status),\n updateProjectAccess: (name) => updateProjectAccess(name),\n };\n}\n\n/** Whether the agent works on a project, so it must not be stopped. */\nfunction isBusy(record: ProjectRecord): boolean {\n return record.status === \"starting\" || record.status === \"destroying\";\n}\n\nexport interface RunningLimitOptions {\n /** max_running_projects: 0 means no limit */\n max: number;\n /** Whether a project's own config says never to stop it */\n neverStops: (name: string) => boolean;\n}\n\n/** DDEV states where the project's containers are up and use memory. */\nfunction isUp(project: DdevProject): boolean {\n return (\n project.status === \"running\" ||\n project.status === \"starting\" ||\n project.status === \"unhealthy\"\n );\n}\n\n/**\n * Stop the least recently used projects until `max` or fewer run.\n *\n * A project runs when `ddev list` shows it up, or when the agent is starting\n * it (status \"starting\" in the database). `ddev list` is the source because\n * `trafic deploy` runs `ddev start` itself, so the database does not know\n * about every running project. The projects the agent is starting count too,\n * because DDEV shows them stopped until their containers exist: the waiting\n * page marks its project \"starting\" before it calls this, so that project\n * counts once whether DDEV shows it up or not, and calls made for starts at\n * the same time each see all of them.\n *\n * Because of deploys, the database is first brought in line with DDEV. A\n * project that runs while the database says it is stopped (or has no record\n * of it) was started outside the agent, most likely by a deploy. Its last\n * access is then old or missing, which would make it the first one stopped,\n * right after the deploy. It is recorded as accessed now instead. A project\n * the database says runs while DDEV says it does not was stopped outside the\n * agent, and is recorded as stopped, so its next start is seen too.\n *\n * Projects with `idle_timeout: never`, projects the agent is starting and\n * projects `trafic-agent destroy` is removing (status \"destroying\": its\n * backup starts a stopped project) are never stopped. When no other project\n * can be stopped, a warning is logged and the server stays over the limit.\n *\n * Returns the names of the projects it stopped.\n */\nexport async function enforceRunningLimit(\n options: RunningLimitOptions,\n deps: RunningLimitDeps = ddevRunningLimitDeps(),\n): Promise<string[]> {\n const { max, neverStops } = options;\n\n if (max <= 0) return [];\n\n const projects = await deps.listProjects();\n\n for (const project of projects) {\n const status = deps.getProject(project.name)?.status;\n\n if (isUp(project) && (status === undefined || status === \"stopped\")) {\n deps.updateProjectAccess(project.name);\n deps.setProjectStatus(project.name, \"running\");\n } else if (!isUp(project) && status === \"running\") {\n deps.setProjectStatus(project.name, \"stopped\");\n }\n }\n\n const running: ProjectRecord[] = [];\n\n for (const project of projects) {\n const record = deps.getProject(project.name);\n\n if (record && (isUp(project) || record.status === \"starting\")) {\n running.push(record);\n }\n }\n\n const excess = running.length - max;\n\n if (excess <= 0) return [];\n\n const candidates = running\n .filter((record) => !isBusy(record) && !neverStops(record.name))\n .sort((a, b) => a.lastAccess - b.lastAccess)\n .slice(0, excess);\n\n const stopped: string[] = [];\n\n for (const record of candidates) {\n console.log(\n `Stopping ${record.name}: ${running.length} projects running, max_running_projects is ${max}`,\n );\n\n if (await deps.stopProject(record.name)) {\n deps.setProjectStatus(record.name, \"stopped\");\n stopped.push(record.name);\n } else {\n console.error(`Failed to stop: ${record.name}`);\n }\n }\n\n if (stopped.length < excess) {\n console.warn(\n `${running.length - stopped.length} projects running, max_running_projects is ${max}: no other project can be stopped`,\n );\n }\n\n return stopped;\n}\n","import { nodeIo, type SetupIo } from \"./io.js\";\nimport { step, success, info } from \"./steps.js\";\nimport { loadConfig } from \"../utils/config.js\";\nimport type { TlsConfig } from \"../types.js\";\n\n/**\n * Install system dependencies required by Trafic\n */\nexport function installSystemDeps(io: SetupIo = nodeIo): void {\n step(\"Install system dependencies\");\n\n io.exec(\"apt-get update -qq\", { silent: true });\n // gnupg and ca-certificates are needed to add the DDEV and NodeSource\n // apt repositories — a minimal Ubuntu image ships with neither\n io.exec(\n \"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y jq curl rsync gnupg ca-certificates\",\n { silent: true },\n );\n\n success(\"System dependencies installed: jq, curl, rsync, gnupg, ca-certificates\");\n}\n\n/** Where the DDEV signing key is installed. */\nexport const DDEV_KEYRING = \"/etc/apt/keyrings/ddev.asc\";\n\n/** The deb822 source file naming the DDEV repository. */\nexport const DDEV_SOURCES = \"/etc/apt/sources.list.d/ddev.sources\";\n\n/**\n * Add the official DDEV apt repository and signing key.\n *\n * The repository is the Cloudsmith one DDEV 1.25.4 moved to\n * (ddev/ddev#8698). The former Gemfury host, `pkg.ddev.com`, still serves\n * packages and carries no retirement date, but DDEV now tells Linux users to\n * switch, so this follows the documented install.\n *\n * Two differences from the copy/paste block in DDEV's docs, both deliberate:\n * the key is written by curl rather than piped into `tee`, so a failed\n * download exits non-zero here instead of surfacing as a confusing GPG error\n * on the next `apt-get update` (the same reasoning as the NodeSource key);\n * and the docs' `sudo` is dropped because setup already runs as root.\n *\n * The key is served armoured and stays that way — `.asc` rather than a\n * dearmoured `.gpg` — which is what `Signed-By` in the source file names.\n *\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(`curl -fsSL https://packages.ddev.com/public/gpg.key -o ${DDEV_KEYRING}`, {\n silent: true,\n });\n io.exec(`chmod a+r ${DDEV_KEYRING}`, { silent: true });\n io.exec(\n `printf \"Types: deb\\\\nURIs: https://packages.ddev.com/public/deb/ubuntu\\\\nSuites: stable\\\\nComponents: main\\\\nSigned-By: ${DDEV_KEYRING}\\\\n\" | tee ${DDEV_SOURCES} > /dev/null`,\n { silent: true },\n );\n}\n\n/**\n * Install DDEV\n */\nexport function installDdev(io: SetupIo = nodeIo): void {\n step(\"Install DDEV\");\n\n if (io.commandExists(\"ddev\")) {\n const version = io.exec(\"ddev --version 2>/dev/null | head -1\", { silent: true });\n info(`DDEV already installed: ${version?.trim() ?? \"unknown version\"}`);\n return;\n }\n\n // Install DDEV via the official apt repository\n // See https://docs.ddev.com/en/stable/users/install/ddev-installation/#debianubuntu\n info(\"Adding DDEV apt repository...\");\n addDdevAptRepo(io);\n\n info(\"Installing DDEV...\");\n io.exec(\"apt-get update -qq\", { silent: true });\n io.exec(\"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ddev\", { silent: true });\n\n // Initialize mkcert certificate authority for the ddev user.\n // mkcert installs the CA into the home directory of the current user.\n // Since setup runs as root, we must point HOME to the ddev user's home\n // so the CA is trusted by DDEV when it runs as that user.\n io.exec(\"HOME=/home/ddev mkcert -install\", { silent: true });\n io.exec(\"chown -R ddev:ddev /home/ddev/.local/share/mkcert\", { silent: true });\n\n const version = io.exec(\"ddev --version 2>/dev/null | head -1\", { silent: true });\n success(`DDEV installed: ${version?.trim() ?? \"unknown version\"}`);\n}\n\n/** The Docker volume Traefik keeps its state in. */\nconst GLOBAL_CACHE_VOLUME = \"ddev-global-cache\";\n\n/**\n * Delete Traefik's ACME storage.\n *\n * Turning `use_letsencrypt` off stops DDEV asking for new certificates, but\n * it does not discard the ones already issued: Traefik keeps them in\n * acme.json inside the ddev-global-cache volume and serves them again after a\n * restart. On a server where a host ingress terminates TLS and verifies the\n * hop against the mkcert root, that resurrected certificate fails validation\n * and every request answers 502 — seen on a live server.\n *\n * The volume's mountpoint is resolved rather than reached through a\n * container, so this works whether or not ddev-router is running and needs no\n * extra image. Setup and migrations already run as root, which is what\n * reading a Docker volume from the host needs.\n *\n * Returns true when something was removed.\n */\nexport function purgeTraefikAcmeStorage(io: SetupIo = nodeIo): boolean {\n const mountpoint = io\n .execSilent(`docker volume inspect ${GLOBAL_CACHE_VOLUME} --format '{{.Mountpoint}}'`)\n .trim();\n\n if (!mountpoint) {\n // No volume yet — a fresh server has nothing to purge\n return false;\n }\n\n const acmeDir = `${mountpoint}/traefik`;\n\n // acme.json.* covers the copies a hand-fix or an upgrade may have left\n const found = io.execSilent(`ls ${acmeDir}/acme.json ${acmeDir}/acme.json.* 2>/dev/null`).trim();\n\n if (!found) {\n return false;\n }\n\n io.exec(`rm -f ${acmeDir}/acme.json ${acmeDir}/acme.json.*`, { silent: true });\n return true;\n}\n\n/**\n * Configure DDEV global settings for production\n *\n * Note on DNS and /etc/hosts:\n * When using a custom TLD (not ddev.site), DDEV may try to edit /etc/hosts\n * if it can't resolve project hostnames via DNS. To avoid this:\n * 1. Configure DNS so *.{tld} resolves to the server's IP (recommended)\n * 2. Or install dnsmasq for local wildcard DNS resolution\n *\n * With proper DNS, DDEV won't need sudo for hostname management.\n */\nexport function configureDdev(\n tld: string,\n email?: string,\n io: SetupIo = nodeIo,\n): void {\n step(\"Configure DDEV\");\n\n // Run as ddev user\n const ddevCmd = (cmd: string) => `su - ddev -c '${cmd}'`;\n\n // Configure all global settings in a single call\n io.exec(\n ddevCmd(\n `ddev config global --project-tld=${tld} --router-http-port=80 --router-https-port=443 --router-bind-all-interfaces=true --use-letsencrypt=${email ? \"true\" : \"false\"} ${email ? `--letsencrypt-email=${email}` : \"\"} --instrumentation-opt-in=false`,\n ),\n { silent: true },\n );\n\n success(`DDEV configured with TLD: ${tld}`);\n info(`Ensure DNS is configured: *.${tld} → server IP`);\n\n if (email) {\n success(`Let's Encrypt enabled with email: ${email}`);\n } else if (purgeTraefikAcmeStorage(io)) {\n // Otherwise a certificate issued before it was turned off comes back on\n // the next router restart, and a proxy verifying against mkcert rejects it\n success(\"Removed Traefik's Let's Encrypt storage\");\n }\n\n success(\"DDEV global settings configured\");\n}\n\n/**\n * Install dnsmasq for local DNS resolution (optional)\n * This allows the server itself to resolve *.{tld} hostnames\n * without needing to edit /etc/hosts for each project\n */\nexport function installDnsmasq(tld: string, io: SetupIo = nodeIo): void {\n step(\"Install dnsmasq for local DNS\");\n\n io.exec(\"apt-get update && apt-get install -y dnsmasq\", { silent: true });\n\n // Get server's public IP\n const serverIp = io.exec(\n \"curl -4 -s ifconfig.me || hostname -I | awk '{print $1}'\",\n { silent: true },\n )?.trim() || \"127.0.0.1\";\n\n // Configure dnsmasq to resolve *.{tld} to server IP\n const dnsmasqConfig = `# Trafic: Local DNS for DDEV projects\n# Resolve all *.${tld} to this server\naddress=/${tld}/${serverIp}\n\n# Don't read /etc/resolv.conf\nno-resolv\n\n# Use upstream DNS for everything else\nserver=1.1.1.1\nserver=8.8.8.8\n\n# Listen on localhost only\nlisten-address=127.0.0.1\nbind-interfaces\n`;\n\n io.writeFile(\"/etc/dnsmasq.d/trafic.conf\", dnsmasqConfig);\n\n // Configure system to use local dnsmasq\n io.exec(\"systemctl restart dnsmasq\");\n io.exec(\"systemctl enable dnsmasq\");\n\n // Update resolv.conf to use local DNS first\n info(\"Configure /etc/resolv.conf to use 127.0.0.1 as primary DNS\");\n\n success(`dnsmasq configured: *.${tld} → ${serverIp}`);\n}\n\n/**\n * Get the Docker gateway IP reachable from inside DDEV containers on Linux.\n * DDEV runs containers on the `ddev_default` network — we inspect that first.\n * Falls back to the default `bridge` network, then to 172.17.0.1.\n * `host.docker.internal` is not available on Linux without extra configuration.\n */\nexport function getDockerGatewayIp(io: SetupIo = nodeIo): string {\n // Prefer the ddev_default network gateway — that's the network ddev-router is on\n const ddevIp = io.execSilent(\n \"docker network inspect ddev_default --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}'\",\n );\n if (ddevIp) return ddevIp;\n\n // Fall back to the default bridge network\n const bridgeIp = io.execSilent(\n \"docker network inspect bridge --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}'\",\n );\n return bridgeIp || \"172.17.0.1\";\n}\n\n/** DDEV tool ports, used when the global config does not say otherwise */\nconst DEFAULT_TOOL_PORTS = {\n \"mailpit-http-port\": \"8025\",\n \"mailpit-https-port\": \"8026\",\n \"xhgui-http-port\": \"8143\",\n \"xhgui-https-port\": \"8142\",\n} as const;\n\n/**\n * Read the ports DDEV publishes its tools on.\n *\n * ddev-router publishes these on 0.0.0.0, and Docker bypasses UFW for\n * published ports, so a firewall rule cannot protect them — the forward auth\n * middleware is the only thing that can. Read them rather than hardcode: they\n * are configurable, and an entry point Traefik does not know about is ignored.\n */\nexport function readToolPorts(io: SetupIo = nodeIo): string[] {\n const output = io.execSilent(\"su - ddev -c 'ddev config global'\");\n const ports: string[] = [];\n\n for (const [key, fallback] of Object.entries(DEFAULT_TOOL_PORTS)) {\n const match = new RegExp(`^${key}=(\\\\d+)$`, \"m\").exec(output);\n ports.push(match?.[1] ?? fallback);\n }\n\n return [...new Set(ports)];\n}\n\n/** The ports ddev-router serves projects on. */\nexport interface RouterPorts {\n http: string;\n https: string;\n}\n\n/** Used when the global config does not say otherwise */\nconst DEFAULT_ROUTER_PORTS: RouterPorts = { http: \"80\", https: \"443\" };\n\n/**\n * Read the ports ddev-router serves projects on.\n *\n * Traefik names an entry point after its port, so these decide what the\n * catch-all routers and the auth middleware must attach to. Hardcoding 80 and\n * 443 is wrong on any server where the router was moved — which is every\n * server running a host ingress in front of it.\n */\nexport function readRouterPorts(io: SetupIo = nodeIo): RouterPorts {\n const output = io.execSilent(\"su - ddev -c 'ddev config global'\");\n\n const read = (key: string, fallback: string): string =>\n new RegExp(`^${key}=(\\\\d+)$`, \"m\").exec(output)?.[1] ?? fallback;\n\n return {\n http: read(\"router-http-port\", DEFAULT_ROUTER_PORTS.http),\n https: read(\"router-https-port\", DEFAULT_ROUTER_PORTS.https),\n };\n}\n\n/** Where the agent writes the Traefik files it owns. */\nexport const TRAEFIK_DIR = \"/home/ddev/.ddev/traefik\";\n\n/** Static config the agent owns, merged by DDEV into .static_config.yaml. */\nexport const STATIC_CONFIG = `${TRAEFIK_DIR}/static_config.trafic.yaml`;\n\n/** Wildcard TLS store, read before DDEV's own default_config.yaml. */\nexport const TLS_STORE_CONFIG = `${TRAEFIK_DIR}/custom-global-config/0-trafic-tls.yaml`;\n\n/** Router compose override carrying the DNS provider credentials. */\nexport const ROUTER_COMPOSE_OVERRIDE = \"/home/ddev/.ddev/router-compose.trafic.yaml\";\n\n/** Name of the DNS-01 resolver the agent adds to Traefik. */\nexport const DNS_RESOLVER = \"acme-dns\";\n\n/** What buildStaticConfig needs to emit the DNS-01 resolver. */\nexport interface TlsResolverOptions {\n /** lego provider name, e.g. \"cloudflare\" */\n provider: string;\n /** Account email — Let's Encrypt refuses a registration without one */\n email: string;\n /** ACME CA directory URL; the provider default is used when omitted */\n caServer?: string;\n}\n\n/**\n * Build the static config attaching forward auth to every entry point.\n *\n * The error page middleware stays on the web entry points only: a 502 from a\n * stopped project should show the waiting page, while a tool port has nothing\n * to wait for.\n *\n * With `tls`, a DNS-01 certificate resolver is added. It is only ever used by\n * the default TLS store (see buildTlsStoreConfig): DDEV keeps writing\n * `certResolver: acme-tlsChallenge` on every project router, and Traefik\n * skips those requests once the default store holds a certificate matching\n * the host with wildcard semantics.\n *\n * The propagation check is disabled on purpose. lego sets two TXT values at\n * the same name, one for `<tld>` and one for `*.<tld>`, then asks public\n * resolvers for each. A resolver that answered between the two writes caches\n * the partial answer for the record TTL — 120s on Cloudflare, longer than\n * lego's check window — and the order fails with \"NS 9.9.9.9:53 did not\n * return the expected TXT record\" although both records were correct. That\n * was seen on a live server; the staging run had passed by timing luck.\n * Let's Encrypt resolves authoritatively itself, so the check adds a failure\n * mode without adding safety. `PropagationWait(delay, skipCheck)` sleeps the\n * delay and then skips the check, so the 30s covers propagation to the\n * provider's edges before the CA looks.\n */\nexport function buildStaticConfig(\n toolPorts: string[],\n routerPorts: RouterPorts = DEFAULT_ROUTER_PORTS,\n tls?: TlsResolverOptions,\n): string {\n const webEntryPoint = (name: string) => ` ${name}:\n http:\n middlewares:\n - trafic-auth@file\n - trafic-errors@file\n`;\n\n const toolEntryPoint = (port: string) => ` http-${port}:\n http:\n middlewares:\n - trafic-auth@file\n`;\n\n // A tool port that happens to equal a router port must not be emitted\n // twice: a duplicate key makes the whole static config invalid\n const web = [routerPorts.http, routerPorts.https];\n const tools = toolPorts.filter((port) => !web.includes(port));\n\n const entryPoints = `entryPoints:\n${web.map((port) => webEntryPoint(`http-${port}`)).join(\"\")}${tools.map(toolEntryPoint).join(\"\")}`;\n\n if (!tls) {\n return entryPoints;\n }\n\n return `${entryPoints}\ncertificatesResolvers:\n ${DNS_RESOLVER}:\n acme:\n email: \"${escapeYamlDouble(tls.email)}\"\n storage: /mnt/ddev-global-cache/traefik/${DNS_RESOLVER}.json\n${tls.caServer ? ` caServer: \"${escapeYamlDouble(tls.caServer)}\"\\n` : \"\"} dnsChallenge:\n provider: \"${escapeYamlDouble(tls.provider)}\"\n propagation:\n delayBeforeChecks: 30s\n disableChecks: true\n`;\n}\n\n/**\n * Build the default TLS store holding the wildcard certificate.\n *\n * Traefik's file provider keeps the first `tls.stores.default` it reads in\n * directory order and logs \"TLS store default already configured, skipping\"\n * for every later file. DDEV writes an empty one in `default_config.yaml`,\n * so this has to be read first — hence the `0-` prefix on the file name.\n */\nexport function buildTlsStoreConfig(tld: string): string {\n return `# Trafic: wildcard certificate served to every host without its own\n#\n# Read before DDEV's default_config.yaml — Traefik keeps the first default\n# store it sees, so the file name has to sort first.\ntls:\n stores:\n default:\n defaultGeneratedCert:\n resolver: ${DNS_RESOLVER}\n domain:\n main: \"${escapeYamlDouble(tld)}\"\n sans:\n - \"*.${escapeYamlDouble(tld)}\"\n`;\n}\n\n/**\n * Build the router compose override carrying the provider credentials.\n *\n * lego reads them from the router container's environment, so they cannot be\n * passed through a config file. DDEV merges `~/.ddev/router-compose.*.yaml`\n * into the router compose and recreates the container when it changes, on the\n * next `ddev start`.\n */\nexport function buildRouterComposeOverride(env: Record<string, string>): string {\n const entries = Object.entries(env);\n\n return `# Trafic: DNS-01 provider credentials for the Traefik router\n# Contains secrets — this file is mode 600.\nservices:\n ddev-router:\n environment:\n${entries.map(([key, value]) => ` - \"${key}=${escapeYamlDouble(value)}\"`).join(\"\\n\")}\n`;\n}\n\n/** Escape a value for a double-quoted YAML scalar. */\nfunction escapeYamlDouble(value: string): string {\n return value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n}\n\n/**\n * Build the dynamic config: the middlewares, the agent service, and the\n * catch-all routers that hand a stopped project to the waiting page.\n *\n * One catch-all per web entry point, each naming the entry point it serves.\n * A router that names none is instantiated by Traefik on *every* entry point,\n * and DDEV's router health check compares the number of router definitions in\n * its config files against the number Traefik reports. Two unpinned\n * definitions became fourteen instances on a server with six tool entry\n * points, so the counts never matched, the health check waited out its\n * timeout, and every `ddev start` failed after 60 seconds.\n */\nexport function buildDynamicConfig(\n gatewayIp: string,\n routerPorts: RouterPorts = DEFAULT_ROUTER_PORTS,\n): string {\n return `# Trafic: Forward auth middleware definition\nhttp:\n middlewares:\n trafic-auth:\n forwardAuth:\n address: \"http://${gatewayIp}:9876/__auth__\"\n authResponseHeaders:\n - \"X-Trafic-Project\"\n\n trafic-errors:\n errors:\n status:\n - \"502\"\n - \"503\"\n service: trafic-service\n query: \"/\"\n\n routers:\n # The lowest priority above DDEV's own fallback routers, which sit at\n # priority 1 since DDEV 1.25.4, and still far below any project router —\n # so a DDEV project router always wins. These only match when none does —\n # which is what a stopped project produces, because DDEV removes that\n # project's router and Traefik would otherwise answer 404. Without them\n # the waiting page never appears and scale-to-zero never restarts\n # anything. Auth is unaffected: it is attached at the entry point, not\n # here, so this cannot reintroduce the bypass that #27 fixed.\n #\n # Priority 1 is not enough any more: DDEV 1.25.4 ships\n # ddev-router-fallback-http and ddev-router-fallback-https with the same\n # PathPrefix(\\`/\\`) rule at priority 1, pointing at a 404 responder. With\n # equal priority and identical rules Traefik picked DDEV's, so a stopped\n # project answered DDEV's 404 page instead of the waiting page.\n #\n # Each names exactly one entry point. Leaving entryPoints out attaches a\n # router to all of them, which breaks DDEV's router health check — it\n # counts definitions and compares them to what Traefik loaded.\n trafic-catchall:\n rule: \"PathPrefix(\\`/\\`)\"\n entryPoints:\n - http-${routerPorts.http}\n priority: 2\n service: trafic-service\n\n # Carries tls, so it only matches the https entry point. The plain one\n # above cannot serve it, and one alone would leave HTTP answering 404.\n trafic-catchall-tls:\n rule: \"PathPrefix(\\`/\\`)\"\n entryPoints:\n - http-${routerPorts.https}\n priority: 2\n service: trafic-service\n tls: {}\n\n services:\n trafic-service:\n loadBalancer:\n servers:\n - url: \"http://${gatewayIp}:9876\"\n`;\n}\n\n/** What DDEV knows about Let's Encrypt, read from its global config. */\nexport interface LetsEncryptSettings {\n enabled: boolean;\n email?: string;\n}\n\n/**\n * Read DDEV's Let's Encrypt settings.\n *\n * DDEV owns them — `ddev config global --use-letsencrypt --letsencrypt-email`\n * — so they are the single source of truth for the ACME account, wildcard\n * mode included.\n */\nexport function readLetsEncryptSettings(io: SetupIo = nodeIo): LetsEncryptSettings {\n const output = io.execSilent(\"su - ddev -c 'ddev config global'\");\n\n return {\n enabled: /^use-letsencrypt=true$/m.test(output),\n email: /^letsencrypt-email=(.+)$/m.exec(output)?.[1]?.trim() || undefined,\n };\n}\n\n/** Read the project TLD from DDEV's global config. */\nexport function readProjectTld(io: SetupIo = nodeIo): string {\n const output = io.execSilent(\"su - ddev -c 'ddev config global'\");\n\n return /^project-tld=(.+)$/m.exec(output)?.[1]?.trim() ?? \"\";\n}\n\n/** Options for configureTraefik. */\nexport interface ConfigureTraefikOptions {\n /**\n * TLS settings. Read from /etc/trafic/config.toml when omitted, which is\n * what migrations do — setup passes them, since it writes that file later.\n */\n tls?: TlsConfig;\n /** Project TLD. Read from DDEV's global config when omitted. */\n tld?: string;\n}\n\n/**\n * Configure Traefik for forward auth, and for the wildcard certificate when\n * a DNS provider is configured.\n *\n * Everything the agent owns in Traefik's configuration is written here, so\n * setup and the migrations share one code path and cannot drift apart.\n */\nexport function configureTraefik(\n options: ConfigureTraefikOptions = {},\n io: SetupIo = nodeIo,\n): void {\n step(\"Configure Traefik for forward auth\");\n\n // Create custom Traefik config directories\n io.exec(`mkdir -p ${TRAEFIK_DIR}/custom-global-config`, { silent: true });\n io.exec(\"chown -R ddev:ddev /home/ddev/.ddev\", { silent: true });\n\n // Use the Docker bridge gateway IP instead of host.docker.internal —\n // the latter is not available on Linux without extra Docker configuration.\n const gatewayIp = getDockerGatewayIp(io);\n\n // Entry point names come from the ports, so both configs need them\n const routerPorts = readRouterPorts(io);\n\n // Dynamic configuration: the middlewares, the agent service and the\n // catch-all routers. Written to custom-global-config/, which DDEV copies\n // into the ddev-global-cache volume on project start. It used to be written\n // to the traefik root as well, for DDEV versions that watched it — nothing\n // reads that copy on 1.25, and having two made debugging harder.\n const dynamicConfig = buildDynamicConfig(gatewayIp, routerPorts);\n\n io.writeFile(`${TRAEFIK_DIR}/custom-global-config/trafic.yaml`, dynamicConfig);\n io.exec(`chown ddev:ddev ${TRAEFIK_DIR}/custom-global-config/trafic.yaml`, { silent: true });\n\n const tls = options.tls ?? loadConfig().tls;\n const resolver = tls.dnsProvider\n ? buildTlsResolverOptions(tls, io)\n : undefined;\n\n // Read and check the TLD before anything is written: a static config that\n // names the resolver without the TLS store behind it is a broken pairing,\n // and there is no reason to leave one on disk when the TLD is missing.\n const tld = resolver ? readWildcardTld(options.tld, io) : \"\";\n\n // Static configuration: attaches trafic-auth to every entry point DDEV\n // publishes, and trafic-errors to the web ones, so every request goes\n // through auth regardless of which project router handles it.\n // DDEV merges all static_config.*.yaml files into .static_config.yaml on start.\n const staticConfig = buildStaticConfig(readToolPorts(io), routerPorts, resolver);\n\n io.writeFile(STATIC_CONFIG, staticConfig);\n io.exec(`chown ddev:ddev ${STATIC_CONFIG}`, { silent: true });\n\n info(`Entry points: http-${routerPorts.http}, http-${routerPorts.https} (web) plus tool ports`);\n\n if (resolver) {\n writeWildcardFiles(tls, tld, io);\n } else {\n removeWildcardFiles(io);\n }\n\n success(\"Traefik configured with Trafic middleware\");\n}\n\n/**\n * Names of the running DDEV projects.\n *\n * `ddev list -j` is the only source that knows: `project_list.yaml`, which the\n * agent watches for discovery, records every project DDEV has ever seen but\n * carries no live status — a project stopped an hour ago still sits there.\n * `ddev list -j` asks Docker, and wraps the entries in a `raw` array.\n */\nexport function findRunningProjects(io: SetupIo = nodeIo): string[] {\n const output = io.execSilent(\"su - ddev -c 'ddev list -j'\");\n\n if (!output) {\n return [];\n }\n\n try {\n const parsed = JSON.parse(output) as { raw?: { name?: string; status?: string }[] };\n return (parsed.raw ?? [])\n .filter((project) => project.status === \"running\" && project.name)\n .map((project) => project.name as string);\n } catch {\n // A DDEV that answers something other than JSON is not worth guessing at\n return [];\n }\n}\n\n/**\n * Name of one running DDEV project, or null when none runs.\n *\n * Used to push a changed global Traefik config into the running router: DDEV\n * copies `custom-global-config/*.yaml` into it on any `ddev start`, so\n * starting a project that is already running is enough, and nothing that is\n * serving traffic has to be stopped.\n */\nexport function findRunningProject(io: SetupIo = nodeIo): string | null {\n return findRunningProjects(io)[0] ?? null;\n}\n\n/**\n * Turn the agent's TLS config into what the static config needs.\n *\n * The ACME account comes from DDEV: a resolver without an email is refused by\n * Let's Encrypt, and `use_letsencrypt=false` means DDEV is also stripping the\n * certificate resolver from the project routers.\n */\nfunction buildTlsResolverOptions(tls: TlsConfig, io: SetupIo): TlsResolverOptions {\n const letsEncrypt = readLetsEncryptSettings(io);\n\n if (!letsEncrypt.enabled || !letsEncrypt.email) {\n throw new Error(\n \"tls.dns_provider needs Let's Encrypt enabled in DDEV with an account email.\\n\" +\n \" Run setup again with --email=<address>, or unset tls.dns_provider.\",\n );\n }\n\n return {\n provider: tls.dnsProvider!,\n email: letsEncrypt.email,\n caServer: tls.caServer,\n };\n}\n\n/** Read the TLD the wildcard certificate covers, or fail before any write. */\nfunction readWildcardTld(configured: string | undefined, io: SetupIo): string {\n const tld = configured || readProjectTld(io);\n\n if (!tld) {\n throw new Error(\n \"Cannot configure the wildcard certificate: no project TLD is set in DDEV\",\n );\n }\n\n return tld;\n}\n\n/** Write the wildcard TLS store and the router credentials. */\nfunction writeWildcardFiles(tls: TlsConfig, tld: string, io: SetupIo): void {\n io.writeFile(TLS_STORE_CONFIG, buildTlsStoreConfig(tld));\n io.exec(`chown ddev:ddev ${TLS_STORE_CONFIG}`, { silent: true });\n\n // Holds the provider token: created empty and mode 600 first, so the\n // credentials never exist on disk under the umask default. `install`\n // truncates an existing file and resets its mode, and the later write\n // keeps it.\n io.exec(`install -m 600 -o ddev -g ddev /dev/null ${ROUTER_COMPOSE_OVERRIDE}`, {\n silent: true,\n });\n io.writeFile(ROUTER_COMPOSE_OVERRIDE, buildRouterComposeOverride(tls.dnsEnv));\n\n success(`Wildcard certificate: *.${tld} via the ${tls.dnsProvider} DNS-01 provider`);\n}\n\n/** Remove the wildcard files where no DNS provider is configured. */\nfunction removeWildcardFiles(io: SetupIo): void {\n if (io.fileExists(TLS_STORE_CONFIG)) {\n io.exec(`rm -f ${TLS_STORE_CONFIG}`, { silent: true });\n }\n\n if (io.fileExists(ROUTER_COMPOSE_OVERRIDE)) {\n io.exec(`rm -f ${ROUTER_COMPOSE_OVERRIDE}`, { silent: true });\n }\n}\n","import { dirname, join } from \"node:path\";\nimport { getDockerGatewayIp } from \"../setup/ddev.js\";\nimport { nodeIo, type SetupIo } from \"../setup/io.js\";\n\n/**\n * Files DDEV reads the dynamic Traefik configuration from, relative to the\n * .ddev directory.\n *\n * Only custom-global-config: DDEV copies that into the ddev-global-cache\n * volume, which is where Traefik actually reads from. Setup used to write a\n * second copy at the traefik root for older DDEV versions; migration 0010\n * removes it, and nothing reads it on 1.25.\n */\nconst DYNAMIC_CONFIGS = [\"traefik/custom-global-config/trafic.yaml\"];\n\n/** Matches the host in an agent URL, e.g. http://172.17.0.1:9876/__auth__ */\nconst AGENT_URL = /http:\\/\\/(\\d+\\.\\d+\\.\\d+\\.\\d+):(\\d+)/g;\n\n/**\n * The address the forward-auth middleware currently points at.\n */\nexport function readForwardAuthAddress(content: string): string | undefined {\n const match = /http:\\/\\/(\\d+\\.\\d+\\.\\d+\\.\\d+):\\d+/.exec(content);\n return match?.[1];\n}\n\n/**\n * Point every agent URL in a dynamic config at a different host, keeping\n * each port as it was.\n */\nexport function rewriteForwardAuthAddress(\n content: string,\n gatewayIp: string,\n): string {\n return content.replace(AGENT_URL, `http://${gatewayIp}:$2`);\n}\n\n/**\n * Correct the forward-auth address if the Docker gateway has moved.\n *\n * setup runs `configureTraefik` before any DDEV project exists, so the\n * `ddev_default` network is absent and the address falls back to the default\n * bridge. Once a project starts, that address is stale. Migration 0006 exists\n * to fix it on servers provisioned earlier, but `markAllMigrationsApplied()`\n * marks it applied on fresh ones, so nothing ever recomputed it.\n *\n * Traefik watches the dynamic config, so rewriting the file is enough — no\n * restart. Any failure is logged and ignored: a stale address is survivable,\n * an agent that will not start is not.\n */\nexport function syncForwardAuthAddress(\n projectListPath: string,\n io: SetupIo = nodeIo,\n): void {\n try {\n const ddevDir = dirname(projectListPath);\n const gatewayIp = getDockerGatewayIp(io);\n const changed: string[] = [];\n\n for (const relative of DYNAMIC_CONFIGS) {\n const path = join(ddevDir, relative);\n\n if (!io.fileExists(path)) {\n continue;\n }\n\n const content = io.readFile(path);\n const current = readForwardAuthAddress(content);\n\n if (!current || current === gatewayIp) {\n continue;\n }\n\n io.writeFile(path, rewriteForwardAuthAddress(content, gatewayIp));\n changed.push(path);\n }\n\n if (changed.length > 0) {\n console.log(\n `Traefik forward auth: corrected address to ${gatewayIp} in ${changed.length} file(s)`,\n );\n }\n } catch (error) {\n console.warn(\n `Could not check the Traefik forward auth address: ${error instanceof Error ? error.message : error}`,\n );\n }\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { DNS_RESOLVER, STATIC_CONFIG } from \"../setup/ddev.js\";\nimport type { AgentConfig } from \"../types.js\";\n\n/**\n * Warn when the config asks for a wildcard certificate that Traefik never got.\n *\n * The files are written by setup and by migration 0016, not by the running\n * agent: writing them needs root, and applying them needs a project start.\n * So this only reports, and says which command fixes it.\n */\nexport function warnIfWildcardNotApplied(\n config: AgentConfig,\n staticConfigPath: string = STATIC_CONFIG,\n): boolean {\n if (!config.tls.dnsProvider) {\n return false;\n }\n\n const configured =\n existsSync(staticConfigPath) &&\n readFileSync(staticConfigPath, \"utf-8\").includes(`${DNS_RESOLVER}:`);\n\n if (configured) {\n return false;\n }\n\n console.warn(\n `tls.dns_provider is set to \"${config.tls.dnsProvider}\" but Traefik has no ${DNS_RESOLVER} resolver.`,\n );\n console.warn(\n \" The wildcard certificate is not being requested. Run `sudo trafic-agent upgrade`, or re-run setup.\",\n );\n\n return true;\n}\n","import { createServer, type IncomingMessage, type ServerResponse } from \"node:http\";\nimport { readFileSync } from \"node:fs\";\nimport { resolve, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { AgentConfig, AuthConfig } from \"./types.js\";\nimport { checkAuth, resolveClientIp } from \"./utils/auth.js\";\nimport {\n loadProjectList,\n buildHostnameIndex,\n watchProjectList,\n startProject,\n getProjectInfo,\n nodeDdevRunner,\n type DdevRunner,\n} from \"./utils/ddev.js\";\nimport {\n initDb,\n updateProjectAccess,\n logAccess,\n setProjectStatus,\n getProject,\n isProcessAlive,\n releaseStaleStatuses,\n} from \"./utils/db.js\";\nimport {\n loadProjectConfig,\n shouldNeverStop,\n type ProjectConfig,\n} from \"./utils/project-config.js\";\nimport {\n ddevRunningLimitDeps,\n enforceRunningLimit,\n} from \"./tasks/running-limit.js\";\nimport { syncForwardAuthAddress } from \"./utils/traefik.js\";\nimport { warnIfWildcardNotApplied } from \"./utils/tls.js\";\n\ndeclare const __VERSION__: string;\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n// State\nlet config: AgentConfig;\nlet projectList: Map<string, string>;\nlet hostnameIndex: Map<string, string>;\n// Cache of per-project configs (project name -> config)\nconst projectConfigs = new Map<string, ReturnType<typeof loadProjectConfig>>();\n\n/**\n * What the request handlers need from the outside world.\n *\n * Gathered into one seam so a test can drive the handlers with a known\n * project list and record what they did, without a listening socket, a real\n * database or a DDEV install. `startServer` builds the real one.\n */\nexport interface ServerDeps {\n auth: AuthConfig;\n /** hostname -> project name */\n hostnameIndex: Map<string, string>;\n /** project name -> its own config, where it has one */\n projectConfigs: Map<string, ReturnType<typeof loadProjectConfig>>;\n loadTemplate: (name: string) => string;\n updateProjectAccess: (name: string) => void;\n logAccess: (log: Parameters<typeof logAccess>[0]) => void;\n getProject: (name: string) => ReturnType<typeof getProject>;\n setProjectStatus: (name: string, status: \"running\" | \"stopped\" | \"starting\") => void;\n /** Set back the \"starting\" and \"destroying\" statuses whose process is gone */\n releaseStaleStatuses: () => unknown;\n startProject: (name: string) => Promise<boolean>;\n /**\n * Stop projects until the running ones fit under max_running_projects. The\n * project marked \"starting\" counts as running, so it has its slot.\n */\n makeRoom: () => Promise<unknown>;\n getProjectInfo: (name: string) => ReturnType<typeof getProjectInfo>;\n}\n\n/**\n * Load HTML template\n */\nfunction loadTemplate(name: string): string {\n const templatePath = resolve(__dirname, \"..\", \"templates\", `${name}.html`);\n try {\n return readFileSync(templatePath, \"utf-8\");\n } catch {\n return `<html><body><h1>${name}</h1></body></html>`;\n }\n}\n\n/**\n * Get effective auth config for a project (merges global + per-project)\n */\nexport function getEffectiveAuthConfig(\n projectName: string | undefined,\n deps: Pick<ServerDeps, \"auth\" | \"projectConfigs\">,\n): AuthConfig {\n if (!projectName) return deps.auth;\n\n const projectConfig = deps.projectConfigs.get(projectName);\n if (!projectConfig?.auth_policy) return deps.auth;\n\n // Override default policy with project-specific policy\n return {\n ...deps.auth,\n defaultPolicy: projectConfig.auth_policy,\n };\n}\n\n/**\n * Handle forward auth requests from Traefik\n * Traefik sends the original request headers, we return 200 (allow) or 401 (deny)\n */\nexport function handleAuth(\n req: IncomingMessage,\n res: ServerResponse,\n deps: ServerDeps,\n): void {\n const hostname = req.headers[\"x-forwarded-host\"] as string ?? \"\";\n // Keep the two apart: the socket peer cannot be forged, X-Forwarded-For\n // partly can. checkAuth decides which entry to trust.\n const socketIp = req.socket.remoteAddress ?? \"\";\n const forwardedFor = req.headers[\"x-forwarded-for\"] as string | undefined;\n const authorization = req.headers[\"authorization\"];\n const path = req.headers[\"x-forwarded-uri\"] as string ?? \"/\";\n\n // Find project from hostname\n const projectName = deps.hostnameIndex.get(hostname);\n\n // Get effective auth config (global + per-project overrides)\n const authConfig = getEffectiveAuthConfig(projectName, deps);\n\n const clientIp = resolveClientIp(\n socketIp,\n forwardedFor,\n authConfig.trustedProxyHops,\n );\n\n const result = checkAuth(\n {\n hostname,\n ip: socketIp,\n authorization,\n forwardedFor,\n },\n authConfig,\n );\n\n if (result.allowed) {\n // Log access and update last access time\n if (projectName) {\n deps.updateProjectAccess(projectName);\n deps.logAccess({\n project: projectName,\n timestamp: Date.now(),\n ip: clientIp,\n userAgent: req.headers[\"user-agent\"] ?? \"\",\n path,\n });\n }\n\n res.writeHead(200);\n res.end();\n } else {\n // Return 401 with WWW-Authenticate header for basic auth\n res.writeHead(401, {\n \"WWW-Authenticate\": 'Basic realm=\"Trafic\"',\n });\n res.end(\"Unauthorized\");\n }\n}\n\n/**\n * Handle errors middleware requests (502 from Traefik)\n * When a project is stopped, Traefik returns 502. We show a waiting page and start the project.\n */\nexport async function handleErrors(\n req: IncomingMessage,\n res: ServerResponse,\n deps: ServerDeps,\n): Promise<void> {\n const hostname = req.headers[\"x-forwarded-host\"] as string ?? req.headers.host ?? \"\";\n const projectName = deps.hostnameIndex.get(hostname);\n\n if (!projectName) {\n // Unknown project\n const template = deps.loadTemplate(\"error\");\n res.writeHead(404, { \"Content-Type\": \"text/html\" });\n res.end(template.replace(\"{{message}}\", \"Project not found\"));\n return;\n }\n\n // A start or a removal that was killed leaves its status behind\n deps.releaseStaleStatuses();\n const record = deps.getProject(projectName);\n\n if (record?.status === \"destroying\") {\n // trafic-agent destroy is removing it: a start now would bring back the\n // containers that ddev delete removes, or stop the backup's own start\n const template = deps.loadTemplate(\"error\");\n res.writeHead(503, { \"Content-Type\": \"text/html\" });\n res.end(template.replace(\"{{message}}\", \"This project is being removed\"));\n return;\n }\n\n // Check if project is already starting\n if (record?.status === \"starting\") {\n // Show waiting page\n const template = deps.loadTemplate(\"wait\");\n res.writeHead(503, {\n \"Content-Type\": \"text/html\",\n \"Retry-After\": \"5\",\n });\n res.end(\n template\n .replace(/\\{\\{project\\}\\}/g, projectName)\n .replace(/\\{\\{hostname\\}\\}/g, hostname),\n );\n return;\n }\n\n // Mark as starting\n deps.setProjectStatus(projectName, \"starting\");\n\n // Show waiting page immediately\n const template = deps.loadTemplate(\"wait\");\n res.writeHead(503, {\n \"Content-Type\": \"text/html\",\n \"Retry-After\": \"5\",\n });\n res.end(\n template\n .replace(/\\{\\{project\\}\\}/g, projectName)\n .replace(/\\{\\{hostname\\}\\}/g, hostname),\n );\n\n // Start the project without waiting for it. The response is already sent,\n // and `startProject` no longer blocks the event loop, so the agent keeps\n // serving forward auth for every other project while this runs. The status\n // is recorded when it settles, which is what stops a second request from\n // starting the same project again.\n void startWithinLimit(projectName, deps);\n}\n\n/**\n * Stop projects until the running ones fit under `max`, before the waiting\n * page starts a project. A project's own config says whether it never stops.\n */\nexport function makeRoomUnderLimit(\n max: number,\n projectConfigs: Map<string, ProjectConfig>,\n run: DdevRunner = nodeDdevRunner,\n): Promise<string[]> {\n return enforceRunningLimit(\n {\n max,\n neverStops: (name) => shouldNeverStop(projectConfigs.get(name) ?? {}),\n },\n ddevRunningLimitDeps(run),\n );\n}\n\n/**\n * Make room under max_running_projects, then start the project.\n *\n * A failure to make room does not block the start: a page that never comes\n * up is worse than a server one project over its limit.\n */\nasync function startWithinLimit(projectName: string, deps: ServerDeps): Promise<void> {\n try {\n await deps.makeRoom();\n } catch (error: unknown) {\n console.error(`Could not make room for ${projectName}:`, error);\n }\n\n try {\n const success = await deps.startProject(projectName);\n deps.setProjectStatus(projectName, success ? \"running\" : \"stopped\");\n } catch (error: unknown) {\n // Leaving it \"starting\" forever would wedge the waiting page\n deps.setProjectStatus(projectName, \"stopped\");\n console.error(`Could not start ${projectName}:`, error);\n }\n}\n\n/**\n * Handle status polling requests\n */\nexport async function handleStatus(\n req: IncomingMessage,\n res: ServerResponse,\n deps: ServerDeps,\n): Promise<void> {\n const url = new URL(req.url ?? \"/\", `http://${req.headers.host}`);\n const projectName = url.searchParams.get(\"project\");\n\n if (!projectName) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Missing project parameter\" }));\n return;\n }\n\n const info = await deps.getProjectInfo(projectName);\n const record = deps.getProject(projectName);\n\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n name: projectName,\n status: info?.status ?? record?.status ?? \"unknown\",\n ready: info?.status === \"running\",\n }),\n );\n}\n\n/**\n * The path a request should be routed on, without the query string.\n *\n * `req.url` is a target, not a URL: it carries the query and, for a proxied\n * request, may be absolute. Parsing against a fixed base keeps a malformed or\n * absolute target from deciding the route, and a trailing slash is dropped so\n * `/__auth__/` and `/__auth__` are the same endpoint.\n */\nexport function routePath(target: string | undefined): string {\n let path: string;\n\n try {\n path = new URL(target ?? \"/\", \"http://localhost\").pathname;\n } catch {\n return \"/\";\n }\n\n return path.length > 1 && path.endsWith(\"/\") ? path.slice(0, -1) : path;\n}\n\n/**\n * Request handler\n */\nexport async function handleRequest(\n req: IncomingMessage,\n res: ServerResponse,\n deps: ServerDeps,\n): Promise<void> {\n // Route on the path alone. Matching the raw URL meant a query string threw\n // every internal route off: Traefik's catch-all and errors middleware pass\n // the original request through, so `/?s=term` on a stopped project missed\n // every branch and fell to the waiting page — which then started an\n // already-running project. `/__status__?project=` never reached its handler\n // for the same reason: it needs the query it was being routed away from.\n const path = routePath(req.url);\n\n try {\n // Route requests\n if (path === \"/__auth__\" || path.startsWith(\"/__auth__/\")) {\n handleAuth(req, res, deps);\n } else if (path === \"/__status__\" || path.startsWith(\"/__status__/\")) {\n await handleStatus(req, res, deps);\n } else if (path === \"/__health__\") {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"ok\", version: __VERSION__ }));\n } else {\n // Default: errors middleware\n await handleErrors(req, res, deps);\n }\n } catch (error) {\n console.error(\"Request error:\", error);\n res.writeHead(500, { \"Content-Type\": \"text/plain\" });\n res.end(\"Internal Server Error\");\n }\n}\n\n/**\n * Reload project list and their configs\n */\nfunction reloadProjects(): void {\n projectList = loadProjectList(config.projectListPath);\n hostnameIndex = buildHostnameIndex(projectList, config.tld);\n\n // Load per-project configs\n projectConfigs.clear();\n for (const [name, projectDir] of projectList) {\n const projectConfig = loadProjectConfig(projectDir);\n if (projectConfig.auth_policy || projectConfig.idle_timeout) {\n projectConfigs.set(name, projectConfig);\n console.log(` ${name}: auth=${projectConfig.auth_policy ?? \"default\"}, idle=${projectConfig.idle_timeout ?? \"default\"}`);\n }\n }\n\n console.log(`Loaded ${projectList.size} projects (${projectConfigs.size} with custom config)`);\n}\n\n/**\n * Set back the busy statuses that a stopped service or a killed removal left\n * behind, when the service starts. No start or removal of this new process\n * is in flight yet, so its own id counts as gone too: after a reboot, the\n * new service can have the id of the old one.\n */\nexport function releaseStatusesAtStart(\n isAlive: (pid: number) => boolean = isProcessAlive,\n): string[] {\n return releaseStaleStatuses((pid) => pid !== process.pid && isAlive(pid));\n}\n\n/**\n * Start the agent server\n */\nexport function startServer(agentConfig: AgentConfig): void {\n config = agentConfig;\n\n // Initialize database\n initDb(config.dbPath);\n\n for (const name of releaseStatusesAtStart()) {\n console.log(`Set ${name} back to running: the process that started or removed it is gone`);\n }\n\n // setup wrote the forward auth address before any DDEV network existed, so\n // correct it now that one may have appeared\n syncForwardAuthAddress(config.projectListPath);\n\n // A DNS provider in the config that never reached Traefik means the\n // wildcard certificate is not being requested at all\n warnIfWildcardNotApplied(config);\n\n // Load projects\n reloadProjects();\n\n // Watch for changes\n watchProjectList(config.projectListPath, () => {\n console.log(\"Project list changed, reloading...\");\n reloadProjects();\n });\n\n // The real dependencies. Rebuilt per request for the two maps, which\n // reloadProjects replaces wholesale when the project list changes.\n const deps = (): ServerDeps => ({\n auth: config.auth,\n hostnameIndex,\n projectConfigs,\n loadTemplate,\n updateProjectAccess,\n logAccess,\n getProject,\n setProjectStatus,\n releaseStaleStatuses,\n startProject,\n makeRoom: () => makeRoomUnderLimit(config.maxRunningProjects, projectConfigs),\n getProjectInfo,\n });\n\n // Create HTTP server\n const server = createServer((req, res) => {\n handleRequest(req, res, deps()).catch((error) => {\n console.error(\"Unhandled error:\", error);\n if (!res.headersSent) {\n res.writeHead(500);\n res.end(\"Internal Server Error\");\n }\n });\n });\n\n server.listen(config.port, () => {\n console.log(`Trafic agent listening on port ${config.port}`);\n console.log(`TLD: ${config.tld}`);\n console.log(`Projects: ${projectList.size}`);\n });\n}\n"],"mappings":"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"}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type IncomingMessage, type ServerResponse } from "node:http";
|
|
2
2
|
import type { AgentConfig, AuthConfig } from "./types.js";
|
|
3
|
-
import { getProjectInfo } from "./utils/ddev.js";
|
|
3
|
+
import { getProjectInfo, type DdevRunner } from "./utils/ddev.js";
|
|
4
4
|
import { logAccess, getProject } from "./utils/db.js";
|
|
5
|
-
import { loadProjectConfig } from "./utils/project-config.js";
|
|
5
|
+
import { loadProjectConfig, type ProjectConfig } from "./utils/project-config.js";
|
|
6
6
|
/**
|
|
7
7
|
* What the request handlers need from the outside world.
|
|
8
8
|
*
|
|
@@ -21,7 +21,14 @@ export interface ServerDeps {
|
|
|
21
21
|
logAccess: (log: Parameters<typeof logAccess>[0]) => void;
|
|
22
22
|
getProject: (name: string) => ReturnType<typeof getProject>;
|
|
23
23
|
setProjectStatus: (name: string, status: "running" | "stopped" | "starting") => void;
|
|
24
|
+
/** Set back the "starting" and "destroying" statuses whose process is gone */
|
|
25
|
+
releaseStaleStatuses: () => unknown;
|
|
24
26
|
startProject: (name: string) => Promise<boolean>;
|
|
27
|
+
/**
|
|
28
|
+
* Stop projects until the running ones fit under max_running_projects. The
|
|
29
|
+
* project marked "starting" counts as running, so it has its slot.
|
|
30
|
+
*/
|
|
31
|
+
makeRoom: () => Promise<unknown>;
|
|
25
32
|
getProjectInfo: (name: string) => ReturnType<typeof getProjectInfo>;
|
|
26
33
|
}
|
|
27
34
|
/**
|
|
@@ -38,6 +45,11 @@ export declare function handleAuth(req: IncomingMessage, res: ServerResponse, de
|
|
|
38
45
|
* When a project is stopped, Traefik returns 502. We show a waiting page and start the project.
|
|
39
46
|
*/
|
|
40
47
|
export declare function handleErrors(req: IncomingMessage, res: ServerResponse, deps: ServerDeps): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Stop projects until the running ones fit under `max`, before the waiting
|
|
50
|
+
* page starts a project. A project's own config says whether it never stops.
|
|
51
|
+
*/
|
|
52
|
+
export declare function makeRoomUnderLimit(max: number, projectConfigs: Map<string, ProjectConfig>, run?: DdevRunner): Promise<string[]>;
|
|
41
53
|
/**
|
|
42
54
|
* Handle status polling requests
|
|
43
55
|
*/
|
|
@@ -55,6 +67,13 @@ export declare function routePath(target: string | undefined): string;
|
|
|
55
67
|
* Request handler
|
|
56
68
|
*/
|
|
57
69
|
export declare function handleRequest(req: IncomingMessage, res: ServerResponse, deps: ServerDeps): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Set back the busy statuses that a stopped service or a killed removal left
|
|
72
|
+
* behind, when the service starts. No start or removal of this new process
|
|
73
|
+
* is in flight yet, so its own id counts as gone too: after a reboot, the
|
|
74
|
+
* new service can have the id of the old one.
|
|
75
|
+
*/
|
|
76
|
+
export declare function releaseStatusesAtStart(isAlive?: (pid: number) => boolean): string[];
|
|
58
77
|
/**
|
|
59
78
|
* Start the agent server
|
|
60
79
|
*/
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AAIpF,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE1D,OAAO,EAKL,cAAc,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/dist/server.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as handleStatus, i as handleRequest, n as handleAuth, o as
|
|
2
|
-
export { getEffectiveAuthConfig, handleAuth, handleErrors, handleRequest, handleStatus, routePath, startServer };
|
|
1
|
+
import { a as handleStatus, c as routePath, i as handleRequest, l as startServer, n as handleAuth, o as makeRoomUnderLimit, r as handleErrors, s as releaseStatusesAtStart, t as getEffectiveAuthConfig } from "./server-B_iFvS6v.js";
|
|
2
|
+
export { getEffectiveAuthConfig, handleAuth, handleErrors, handleRequest, handleStatus, makeRoomUnderLimit, releaseStatusesAtStart, routePath, startServer };
|
package/dist/setup/agent.d.ts
CHANGED
|
@@ -18,7 +18,10 @@ export declare function addNodeSourceAptRepo(io?: SetupIo): void;
|
|
|
18
18
|
*/
|
|
19
19
|
export declare function installNode(io?: SetupIo): void;
|
|
20
20
|
/**
|
|
21
|
-
* Install the Trafic agent
|
|
21
|
+
* Install the Trafic agent globally, at the version that runs this setup.
|
|
22
|
+
*
|
|
23
|
+
* `trafic setup --agent-version` installs that version, then runs this
|
|
24
|
+
* setup: an install with no version would replace it by the latest one.
|
|
22
25
|
*/
|
|
23
26
|
export declare function installAgent(io?: SetupIo): void;
|
|
24
27
|
/**
|
|
@@ -30,6 +33,13 @@ export declare function createAgentConfig(options: {
|
|
|
30
33
|
dnsProvider?: string;
|
|
31
34
|
dnsEnv?: Record<string, string>;
|
|
32
35
|
}, io?: SetupIo): void;
|
|
36
|
+
/** Default backup directory, `[backup] dir` in the agent config. */
|
|
37
|
+
export declare const BACKUP_DIR = "/var/backups/trafic";
|
|
38
|
+
/**
|
|
39
|
+
* Create the backup directory, writable by the ddev user only: it holds
|
|
40
|
+
* database dumps.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createBackupDir(io?: SetupIo): void;
|
|
33
43
|
/**
|
|
34
44
|
* Create systemd service for the agent
|
|
35
45
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/setup/agent.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,OAAO,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/setup/agent.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,OAAO,EAAE,MAAM,SAAS,CAAC;AAQ/C,2EAA2E;AAC3E,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAK5C;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,GAAE,OAAgB,GAAG,IAAI,CAuB/D;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,EAAE,GAAE,OAAgB,GAAG,IAAI,CAqBtD;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,EAAE,GAAE,OAAgB,GAAG,IAAI,CAMvD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE;IACP,GAAG,EAAE,MAAM,CAAC;IACZ,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC,EACD,EAAE,GAAE,OAAgB,GACnB,IAAI,CAgFN;AA4CD,oEAAoE;AACpE,eAAO,MAAM,UAAU,wBAAwB,CAAC;AAEhD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,EAAE,GAAE,OAAgB,GAAG,IAAI,CAI1D;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,GAAE,OAAgB,GAAG,IAAI,CAwD/D"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type SetupIo } from "../io.js";
|
|
2
|
+
import type { Migration } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Migration 0020: let the agent service write to the backup directory.
|
|
5
|
+
*
|
|
6
|
+
* The agent now removes old backups from /var/backups/trafic in a periodic
|
|
7
|
+
* sweep, but the unit runs with `ProtectSystem=strict` and only lists
|
|
8
|
+
* /var/lib/trafic and /home/ddev in `ReadWritePaths`, so every removal would
|
|
9
|
+
* fail with a read-only file system error.
|
|
10
|
+
*
|
|
11
|
+
* Creates the directory first: hardening created it, but a server set up with
|
|
12
|
+
* `--no-hardening` has none, and the ddev user cannot create it in
|
|
13
|
+
* /var/backups. Then rewrites the unit, which lists the directory with the
|
|
14
|
+
* `-` prefix: systemd refuses to start a unit whose `ReadWritePaths` names a
|
|
15
|
+
* missing path, unless the path has that prefix.
|
|
16
|
+
*
|
|
17
|
+
* Idempotent: the unit is rewritten only while it does not list the backup
|
|
18
|
+
* directory with the `-` prefix.
|
|
19
|
+
*/
|
|
20
|
+
export declare const migration0020BackupDir: Migration;
|
|
21
|
+
/** The migration body, with its effects injected so tests can drive it. */
|
|
22
|
+
export declare function runBackupDirMigration(io?: SetupIo): void;
|
|
23
|
+
//# sourceMappingURL=0020__backup_dir.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"0020__backup_dir.d.ts","sourceRoot":"","sources":["../../../src/setup/migrations/0020__backup_dir.ts"],"names":[],"mappings":"AACA,OAAO,EAAU,KAAK,OAAO,EAAE,MAAM,UAAU,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAI7C;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,sBAAsB,EAAE,SAOpC,CAAC;AAEF,2EAA2E;AAC3E,wBAAgB,qBAAqB,CAAC,EAAE,GAAE,OAAgB,GAAG,IAAI,CAWhE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/setup/migrations/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/setup/migrations/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAsBjF,iDAAiD;AACjD,eAAO,MAAM,qBAAqB,iCAAiC,CAAC;AAEpE,+CAA+C;AAC/C,eAAO,MAAM,cAAc,EAAE,SAAS,EAqBrC,CAAC;AAMF;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,cAAc,CAqBnD;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI,CAG9D;AAMD;;GAEG;AACH,wBAAgB,cAAc,IAAI,kBAAkB,EAAE,CAMrD;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,UAAQ,GAAG,IAAI,CAyBzD;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,IAAI,IAAI,CAU/C"}
|