@studiometa/trafic-agent 0.1.33 → 0.1.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { d as validateConfig, i as runPendingMigrations, l as loadConfig, n as setup, r as listMigrations, s as startIdleScheduler, t as audit } from "./setup-V1w6lkID.js";
3
- import { T as closeDb, b as warn, f as exec, h as isRoot, m as info, r as startServer, v as step, y as success } from "./server-C4j-C4Q1.js";
2
+ import { d as validateConfig, i as runPendingMigrations, l as loadConfig, n as setup, r as listMigrations, s as startIdleScheduler, t as audit } from "./setup-DI2XDsDP.js";
3
+ import { T as closeDb, b as warn, f as exec, h as isRoot, m as info, r as startServer, v as step, y as success } from "./server-3g76yism.js";
4
4
  import { readFileSync } from "node:fs";
5
5
  import { execSync, spawnSync } from "node:child_process";
6
6
  import { parseArgs } from "node:util";
@@ -108,7 +108,7 @@ function runUpgrade(dryRun = false, reExecArgs) {
108
108
  }
109
109
  const alreadyReExeced = process.env["TRAFIC_UPGRADE_REEXEC"] === "1";
110
110
  step("Check for updates");
111
- const current = "0.1.33";
111
+ const current = "0.1.35";
112
112
  info(`Current version: ${current}`);
113
113
  const latest = fetchLatestVersion();
114
114
  if (!latest) warn("Could not reach npm registry — skipping version check");
@@ -191,7 +191,7 @@ function printHelp() {
191
191
  console.log(HELP.trim());
192
192
  }
193
193
  function printVersion() {
194
- console.log("0.1.33");
194
+ console.log("0.1.35");
195
195
  }
196
196
  async function runStart(values) {
197
197
  const config = loadConfig(values.config);
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { a as printAuditResults, c as stopIdleProjects, d as validateConfig, l as loadConfig, n as setup, o as runAudit, s as startIdleScheduler, t as audit, u as parseDuration } from "./setup-V1w6lkID.js";
2
- import { A as initDb, B as stopProject, C as shouldNeverStop, D as getDb, E as getAccessLogs, F as getProjectFromHostname, G as parseBasicAuth, H as checkAuth, I as getProjectInfo, K as parseBearerToken, L as listProjects, M as setProjectStatus, N as updateProjectAccess, O as getIdleProjects, P as buildHostnameIndex, R as loadProjectList, S as loadProjectConfig, T as closeDb, U as matchHostname, V as watchProjectList, W as matchIp, j as logAccess, k as getProject, r as startServer, w as cleanOldLogs, x as getIdleTimeoutMs, z as startProject } from "./server-C4j-C4Q1.js";
1
+ import { a as printAuditResults, c as stopIdleProjects, d as validateConfig, l as loadConfig, n as setup, o as runAudit, s as startIdleScheduler, t as audit, u as parseDuration } from "./setup-DI2XDsDP.js";
2
+ import { A as initDb, B as stopProject, C as shouldNeverStop, D as getDb, E as getAccessLogs, F as getProjectFromHostname, G as parseBasicAuth, H as checkAuth, I as getProjectInfo, K as parseBearerToken, L as listProjects, M as setProjectStatus, N as updateProjectAccess, O as getIdleProjects, P as buildHostnameIndex, R as loadProjectList, S as loadProjectConfig, T as closeDb, U as matchHostname, V as watchProjectList, W as matchIp, j as logAccess, k as getProject, r as startServer, w as cleanOldLogs, x as getIdleTimeoutMs, z as startProject } from "./server-3g76yism.js";
3
3
  export { audit, buildHostnameIndex, checkAuth, cleanOldLogs, closeDb, getAccessLogs, getDb, getIdleProjects, getIdleTimeoutMs, getProject, getProjectFromHostname, getProjectInfo, initDb, listProjects, loadConfig, loadProjectConfig, loadProjectList, logAccess, matchHostname, matchIp, parseBasicAuth, parseBearerToken, parseDuration, printAuditResults, runAudit, setProjectStatus, setup, shouldNeverStop, startIdleScheduler, startProject, startServer, stopIdleProjects, stopProject, updateProjectAccess, validateConfig, watchProjectList };
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync, watch, writeFileSync } from "node:fs";
2
- import { execSync } from "node:child_process";
2
+ import { execFile, execSync } from "node:child_process";
3
3
  import { DatabaseSync } from "node:sqlite";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { createServer } from "node:http";
@@ -295,15 +295,48 @@ function watchProjectList(projectListPath, onChange) {
295
295
  });
296
296
  }
297
297
  /**
298
+ * Run a ddev command without blocking the event loop.
299
+ *
300
+ * The agent is single-threaded and serves forward auth for every project, so
301
+ * an `execSync` here stops the whole server for the duration of the command.
302
+ * `ddev start` takes over a minute, which froze auth for every unrelated
303
+ * project and produced request timeouts on a live server. Nothing about
304
+ * "background" work with setTimeout helps: the timer callback still runs on
305
+ * the same thread.
306
+ */
307
+ function ddev(args, timeoutMs) {
308
+ return new Promise((resolve) => {
309
+ execFile("ddev", args, {
310
+ encoding: "utf-8",
311
+ timeout: timeoutMs
312
+ }, (error, stdout) => {
313
+ if (error) {
314
+ console.error(`ddev ${args.join(" ")} failed:`, error.message);
315
+ resolve({
316
+ ok: false,
317
+ stdout: stdout ?? ""
318
+ });
319
+ return;
320
+ }
321
+ resolve({
322
+ ok: true,
323
+ stdout: stdout ?? ""
324
+ });
325
+ });
326
+ });
327
+ }
328
+ /**
298
329
  * Get detailed project info using ddev describe
299
330
  */
300
- function getProjectInfo(name) {
331
+ async function getProjectInfo(name) {
332
+ const { ok, stdout } = await ddev([
333
+ "describe",
334
+ name,
335
+ "-j"
336
+ ], 1e4);
337
+ if (!ok) return;
301
338
  try {
302
- const output = execSync(`ddev describe ${name} -j`, {
303
- encoding: "utf-8",
304
- timeout: 1e4
305
- });
306
- const data = JSON.parse(output);
339
+ const data = JSON.parse(stdout);
307
340
  if (!data.raw) return void 0;
308
341
  const raw = data.raw;
309
342
  return {
@@ -321,36 +354,21 @@ function getProjectInfo(name) {
321
354
  }
322
355
  }
323
356
  /**
324
- * Start a DDEV project
357
+ * Start a DDEV project.
358
+ *
359
+ * Resolves when the project is up. Callers in the request path must not await
360
+ * it — respond first, then let this settle and record the outcome.
325
361
  */
326
- function startProject(name) {
327
- try {
328
- execSync(`ddev start ${name}`, {
329
- encoding: "utf-8",
330
- timeout: 12e4,
331
- stdio: "pipe"
332
- });
333
- return true;
334
- } catch (error) {
335
- console.error(`Failed to start project ${name}:`, error);
336
- return false;
337
- }
362
+ async function startProject(name) {
363
+ const { ok } = await ddev(["start", name], 12e4);
364
+ return ok;
338
365
  }
339
366
  /**
340
367
  * Stop a DDEV project
341
368
  */
342
- function stopProject(name) {
343
- try {
344
- execSync(`ddev stop ${name}`, {
345
- encoding: "utf-8",
346
- timeout: 6e4,
347
- stdio: "pipe"
348
- });
349
- return true;
350
- } catch (error) {
351
- console.error(`Failed to stop project ${name}:`, error);
352
- return false;
353
- }
369
+ async function stopProject(name) {
370
+ const { ok } = await ddev(["stop", name], 6e4);
371
+ return ok;
354
372
  }
355
373
  /**
356
374
  * List all DDEV projects with their status
@@ -774,6 +792,27 @@ function readToolPorts(io = nodeIo) {
774
792
  }
775
793
  return [...new Set(ports)];
776
794
  }
795
+ /** Used when the global config does not say otherwise */
796
+ var DEFAULT_ROUTER_PORTS = {
797
+ http: "80",
798
+ https: "443"
799
+ };
800
+ /**
801
+ * Read the ports ddev-router serves projects on.
802
+ *
803
+ * Traefik names an entry point after its port, so these decide what the
804
+ * catch-all routers and the auth middleware must attach to. Hardcoding 80 and
805
+ * 443 is wrong on any server where the router was moved — which is every
806
+ * server running a host ingress in front of it.
807
+ */
808
+ function readRouterPorts(io = nodeIo) {
809
+ const output = io.execSilent("su - ddev -c 'ddev config global'");
810
+ const read = (key, fallback) => new RegExp(`^${key}=(\\d+)$`, "m").exec(output)?.[1] ?? fallback;
811
+ return {
812
+ http: read("router-http-port", DEFAULT_ROUTER_PORTS.http),
813
+ https: read("router-https-port", DEFAULT_ROUTER_PORTS.https)
814
+ };
815
+ }
777
816
  /**
778
817
  * Build the static config attaching forward auth to every entry point.
779
818
  *
@@ -781,7 +820,7 @@ function readToolPorts(io = nodeIo) {
781
820
  * stopped project should show the waiting page, while a tool port has nothing
782
821
  * to wait for.
783
822
  */
784
- function buildStaticConfig(toolPorts) {
823
+ function buildStaticConfig(toolPorts, routerPorts = DEFAULT_ROUTER_PORTS) {
785
824
  const webEntryPoint = (name) => ` ${name}:
786
825
  http:
787
826
  middlewares:
@@ -793,18 +832,25 @@ function buildStaticConfig(toolPorts) {
793
832
  middlewares:
794
833
  - trafic-auth@file
795
834
  `;
835
+ const web = [routerPorts.http, routerPorts.https];
836
+ const tools = toolPorts.filter((port) => !web.includes(port));
796
837
  return `entryPoints:
797
- ${webEntryPoint("http-80")}${webEntryPoint("http-443")}${toolPorts.map(toolEntryPoint).join("")}`;
838
+ ${web.map((port) => webEntryPoint(`http-${port}`)).join("")}${tools.map(toolEntryPoint).join("")}`;
798
839
  }
799
840
  /**
800
- * Configure Traefik for forward auth
801
- */
802
- function configureTraefik(io = nodeIo) {
803
- step("Configure Traefik for forward auth");
804
- io.exec("mkdir -p /home/ddev/.ddev/traefik/custom-global-config", { silent: true });
805
- io.exec("chown -R ddev:ddev /home/ddev/.ddev", { silent: true });
806
- const gatewayIp = getDockerGatewayIp(io);
807
- const dynamicConfig = `# Trafic: Forward auth middleware definition
841
+ * Build the dynamic config: the middlewares, the agent service, and the
842
+ * catch-all routers that hand a stopped project to the waiting page.
843
+ *
844
+ * One catch-all per web entry point, each naming the entry point it serves.
845
+ * A router that names none is instantiated by Traefik on *every* entry point,
846
+ * and DDEV's router health check compares the number of router definitions in
847
+ * its config files against the number Traefik reports. Two unpinned
848
+ * definitions became fourteen instances on a server with six tool entry
849
+ * points, so the counts never matched, the health check waited out its
850
+ * timeout, and every `ddev start` failed after 60 seconds.
851
+ */
852
+ function buildDynamicConfig(gatewayIp, routerPorts = DEFAULT_ROUTER_PORTS) {
853
+ return `# Trafic: Forward auth middleware definition
808
854
  http:
809
855
  middlewares:
810
856
  trafic-auth:
@@ -822,22 +868,29 @@ http:
822
868
  query: "/"
823
869
 
824
870
  routers:
825
- # Lowest priority, so a DDEV project router always wins. It only matches
826
- # when none does - which is what a stopped project produces, because DDEV
871
+ # Lowest priority, so a DDEV project router always wins. These only match
872
+ # when none does — which is what a stopped project produces, because DDEV
827
873
  # removes that project's router and Traefik would otherwise answer 404.
828
- # Without this the waiting page never appears and scale-to-zero never
874
+ # Without them the waiting page never appears and scale-to-zero never
829
875
  # restarts anything. Auth is unaffected: it is attached at the entry
830
876
  # point, not here, so this cannot reintroduce the bypass that #27 fixed.
831
877
  #
832
- # Two of them, because a router carrying tls only matches HTTPS entry
833
- # points: one alone would leave plain HTTP answering 404.
878
+ # Each names exactly one entry point. Leaving entryPoints out attaches a
879
+ # router to all of them, which breaks DDEV's router health check — it
880
+ # counts definitions and compares them to what Traefik loaded.
834
881
  trafic-catchall:
835
882
  rule: "PathPrefix(\`/\`)"
883
+ entryPoints:
884
+ - http-${routerPorts.http}
836
885
  priority: 1
837
886
  service: trafic-service
838
887
 
888
+ # Carries tls, so it only matches the https entry point. The plain one
889
+ # above cannot serve it, and one alone would leave HTTP answering 404.
839
890
  trafic-catchall-tls:
840
891
  rule: "PathPrefix(\`/\`)"
892
+ entryPoints:
893
+ - http-${routerPorts.https}
841
894
  priority: 1
842
895
  service: trafic-service
843
896
  tls: {}
@@ -848,23 +901,37 @@ http:
848
901
  servers:
849
902
  - url: "http://${gatewayIp}:9876"
850
903
  `;
851
- io.writeFile("/home/ddev/.ddev/traefik/trafic.yaml", dynamicConfig);
852
- io.exec("chown ddev:ddev /home/ddev/.ddev/traefik/trafic.yaml", { silent: true });
904
+ }
905
+ /**
906
+ * Configure Traefik for forward auth
907
+ */
908
+ function configureTraefik(io = nodeIo) {
909
+ step("Configure Traefik for forward auth");
910
+ io.exec("mkdir -p /home/ddev/.ddev/traefik/custom-global-config", { silent: true });
911
+ io.exec("chown -R ddev:ddev /home/ddev/.ddev", { silent: true });
912
+ const gatewayIp = getDockerGatewayIp(io);
913
+ const routerPorts = readRouterPorts(io);
914
+ const dynamicConfig = buildDynamicConfig(gatewayIp, routerPorts);
853
915
  io.writeFile("/home/ddev/.ddev/traefik/custom-global-config/trafic.yaml", dynamicConfig);
854
916
  io.exec("chown ddev:ddev /home/ddev/.ddev/traefik/custom-global-config/trafic.yaml", { silent: true });
855
- const staticConfig = buildStaticConfig(readToolPorts(io));
917
+ const staticConfig = buildStaticConfig(readToolPorts(io), routerPorts);
856
918
  io.writeFile("/home/ddev/.ddev/traefik/static_config.trafic.yaml", staticConfig);
857
919
  io.exec("chown ddev:ddev /home/ddev/.ddev/traefik/static_config.trafic.yaml", { silent: true });
920
+ info(`Entry points: http-${routerPorts.http}, http-${routerPorts.https} (web) plus tool ports`);
858
921
  success("Traefik configured with Trafic middleware");
859
922
  }
860
923
  //#endregion
861
924
  //#region src/utils/traefik.ts
862
925
  /**
863
926
  * Files DDEV reads the dynamic Traefik configuration from, relative to the
864
- * .ddev directory. Both are written by setup: DDEV 1.25+ picks up
865
- * custom-global-config, older versions watch the traefik root.
927
+ * .ddev directory.
928
+ *
929
+ * Only custom-global-config: DDEV copies that into the ddev-global-cache
930
+ * volume, which is where Traefik actually reads from. Setup used to write a
931
+ * second copy at the traefik root for older DDEV versions; migration 0010
932
+ * removes it, and nothing reads it on 1.25.
866
933
  */
867
- var DYNAMIC_CONFIGS = ["traefik/trafic.yaml", "traefik/custom-global-config/trafic.yaml"];
934
+ var DYNAMIC_CONFIGS = ["traefik/custom-global-config/trafic.yaml"];
868
935
  /** Matches the host in an agent URL, e.g. http://172.17.0.1:9876/__auth__ */
869
936
  var AGENT_URL = /http:\/\/(\d+\.\d+\.\d+\.\d+):(\d+)/g;
870
937
  /**
@@ -1007,10 +1074,12 @@ async function handleErrors(req, res) {
1007
1074
  "Retry-After": "5"
1008
1075
  });
1009
1076
  res.end(template.replace(/\{\{project\}\}/g, projectName).replace(/\{\{hostname\}\}/g, hostname));
1010
- setTimeout(() => {
1011
- const success = startProject(projectName);
1077
+ startProject(projectName).then((success) => {
1012
1078
  setProjectStatus(projectName, success ? "running" : "stopped");
1013
- }, 100);
1079
+ }).catch((error) => {
1080
+ setProjectStatus(projectName, "stopped");
1081
+ console.error(`Could not start ${projectName}:`, error);
1082
+ });
1014
1083
  }
1015
1084
  /**
1016
1085
  * Decide whether Caddy may obtain a certificate for a hostname.
@@ -1040,14 +1109,14 @@ function handleTlsAsk(req, res) {
1040
1109
  /**
1041
1110
  * Handle status polling requests
1042
1111
  */
1043
- function handleStatus(req, res) {
1112
+ async function handleStatus(req, res) {
1044
1113
  const projectName = new URL(req.url ?? "/", `http://${req.headers.host}`).searchParams.get("project");
1045
1114
  if (!projectName) {
1046
1115
  res.writeHead(400, { "Content-Type": "application/json" });
1047
1116
  res.end(JSON.stringify({ error: "Missing project parameter" }));
1048
1117
  return;
1049
1118
  }
1050
- const info = getProjectInfo(projectName);
1119
+ const info = await getProjectInfo(projectName);
1051
1120
  const record = getProject(projectName);
1052
1121
  res.writeHead(200, { "Content-Type": "application/json" });
1053
1122
  res.end(JSON.stringify({
@@ -1080,7 +1149,7 @@ async function handleRequest(req, res) {
1080
1149
  const path = routePath(req.url);
1081
1150
  try {
1082
1151
  if (path === "/__auth__" || path.startsWith("/__auth__/")) handleAuth(req, res);
1083
- else if (path === "/__status__" || path.startsWith("/__status__/")) handleStatus(req, res);
1152
+ else if (path === "/__status__" || path.startsWith("/__status__/")) await handleStatus(req, res);
1084
1153
  else if (path === "/__tls__") handleTlsAsk(req, res);
1085
1154
  else if (path === "/__health__") {
1086
1155
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -1140,4 +1209,4 @@ function startServer(agentConfig) {
1140
1209
  //#endregion
1141
1210
  export { initDb as A, stopProject as B, shouldNeverStop as C, getDb as D, getAccessLogs as E, getProjectFromHostname as F, parseBasicAuth as G, checkAuth as H, getProjectInfo as I, parseBearerToken as K, listProjects as L, setProjectStatus as M, updateProjectAccess as N, getIdleProjects as O, buildHostnameIndex as P, loadProjectList as R, loadProjectConfig as S, closeDb as T, matchHostname as U, watchProjectList as V, matchIp as W, setDryRun as _, configureDdev as a, warn as b, installDdev as c, error as d, exec as f, resetSteps as g, isRoot as h, addDdevAptRepo as i, logAccess as j, getProject as k, installSystemDeps as l, info as m, routePath as n, configureTraefik as o, execSilent as p, startServer as r, getDockerGatewayIp as s, allowTlsFor as t, nodeIo as u, step as v, cleanOldLogs as w, getIdleTimeoutMs as x, success as y, startProject as z };
1142
1211
 
1143
- //# sourceMappingURL=server-C4j-C4Q1.js.map
1212
+ //# sourceMappingURL=server-3g76yism.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-3g76yism.js","names":[],"sources":["../src/utils/auth.ts","../src/utils/ddev.ts","../src/utils/db.ts","../src/utils/project-config.ts","../src/setup/steps.ts","../src/setup/io.ts","../src/setup/ddev.ts","../src/utils/traefik.ts","../src/server.ts"],"sourcesContent":["import type { AuthConfig, AuthRequest, AuthResult, AuthRule } from \"../types.js\";\n\n/**\n * Check if an IP matches a pattern (supports CIDR notation)\n */\nexport function matchIp(ip: string, pattern: string): boolean {\n // Exact match\n if (ip === pattern) return true;\n\n // CIDR notation\n if (pattern.includes(\"/\")) {\n return matchCidr(ip, pattern);\n }\n\n // Wildcard (e.g., 192.168.*)\n if (pattern.includes(\"*\")) {\n const regex = new RegExp(\n \"^\" + pattern.replace(/\\./g, \"\\\\.\").replace(/\\*/g, \".*\") + \"$\",\n );\n return regex.test(ip);\n }\n\n return false;\n}\n\n/**\n * Match IP against CIDR range\n */\nfunction matchCidr(ip: string, cidr: string): boolean {\n const [range, bits] = cidr.split(\"/\");\n const mask = parseInt(bits, 10);\n\n const ipNum = ipToNumber(ip);\n const rangeNum = ipToNumber(range);\n\n if (ipNum === null || rangeNum === null) return false;\n\n const maskNum = ~((1 << (32 - mask)) - 1);\n return (ipNum & maskNum) === (rangeNum & maskNum);\n}\n\n/**\n * Convert IP string to number\n */\nfunction ipToNumber(ip: string): number | null {\n const parts = ip.split(\".\");\n if (parts.length !== 4) return null;\n\n let num = 0;\n for (const part of parts) {\n const n = parseInt(part, 10);\n if (isNaN(n) || n < 0 || n > 255) return null;\n num = (num << 8) | n;\n }\n return num >>> 0; // Convert to unsigned\n}\n\n/**\n * Match hostname against a glob pattern\n */\nexport function matchHostname(hostname: string, pattern: string): boolean {\n // Exact match\n if (hostname === pattern) return true;\n\n // Glob pattern\n const regex = new RegExp(\n \"^\" +\n pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\*/g, \".*\")\n .replace(/\\?/g, \".\") +\n \"$\",\n );\n return regex.test(hostname);\n}\n\n/**\n * Parse Basic auth header\n */\nexport function parseBasicAuth(\n header: string,\n): { username: string; password: string } | null {\n if (!header.startsWith(\"Basic \")) return null;\n\n try {\n const encoded = header.slice(6);\n const decoded = Buffer.from(encoded, \"base64\").toString(\"utf-8\");\n const colonIndex = decoded.indexOf(\":\");\n if (colonIndex === -1) return null;\n\n return {\n username: decoded.slice(0, colonIndex),\n password: decoded.slice(colonIndex + 1),\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Parse Bearer token from header\n */\nexport function parseBearerToken(header: string): string | null {\n if (!header.startsWith(\"Bearer \")) return null;\n return header.slice(7);\n}\n\n/**\n * Resolve the client address from the socket peer and X-Forwarded-For.\n *\n * Every proxy appends the address it saw, so the entries grow left to right\n * and the rightmost ones were added by the proxies closest to us. Counting\n * `trustedHops` from the right therefore lands on the client, while anything\n * further left is whatever the client sent and must be ignored.\n *\n * Reading the leftmost entry instead — as this used to — let a client set\n * `X-Forwarded-For: <allowed-ip>` and match the IP allowlist, which grants\n * access unconditionally.\n *\n * Falls back to the socket peer whenever the header cannot be trusted to\n * hold that many entries.\n */\nexport function resolveClientIp(\n socketIp: string,\n forwardedFor: string | undefined,\n trustedHops = 1,\n): string {\n if (!forwardedFor || trustedHops < 1) {\n return socketIp;\n }\n\n const entries = forwardedFor\n .split(\",\")\n .map((entry) => entry.trim())\n .filter(Boolean);\n\n // Fewer entries than trusted proxies means the chain is shorter than\n // configured — the socket peer is the only address we can rely on.\n if (entries.length < trustedHops) {\n return socketIp;\n }\n\n return entries[entries.length - trustedHops] ?? socketIp;\n}\n\n/**\n * Check authentication for a request\n */\nexport function checkAuth(request: AuthRequest, config: AuthConfig): AuthResult {\n const { hostname, ip, authorization } = request;\n\n const realIp = resolveClientIp(ip, request.forwardedFor, config.trustedProxyHops);\n\n // Check IP whitelist first (always allows)\n for (const pattern of config.allowedIps) {\n if (matchIp(realIp, pattern)) {\n return { allowed: true, reason: \"ip\" };\n }\n }\n\n // Check per-hostname rules\n for (const rule of config.rules) {\n if (matchHostname(hostname, rule.match)) {\n return checkRule(rule, realIp, authorization, config);\n }\n }\n\n // Apply default policy\n return applyDefaultPolicy(config, realIp, authorization);\n}\n\n/**\n * Check a specific rule\n */\nfunction checkRule(\n rule: AuthRule,\n ip: string,\n authorization: string | undefined,\n config: AuthConfig,\n): AuthResult {\n // Rule-specific IP whitelist\n if (rule.allowedIps) {\n for (const pattern of rule.allowedIps) {\n if (matchIp(ip, pattern)) {\n return { allowed: true, reason: \"rule\" };\n }\n }\n }\n\n switch (rule.policy) {\n case \"allow\":\n return { allowed: true, reason: \"rule\" };\n\n case \"deny\":\n return { allowed: false, reason: \"rule\" };\n\n case \"token\": {\n if (!authorization) return { allowed: false, reason: \"rule\" };\n const token = parseBearerToken(authorization);\n const tokens = rule.tokens ?? config.tokens;\n if (token && tokens.includes(token)) {\n return { allowed: true, reason: \"token\" };\n }\n return { allowed: false, reason: \"rule\" };\n }\n\n case \"basic\": {\n if (!authorization) return { allowed: false, reason: \"rule\" };\n const creds = parseBasicAuth(authorization);\n if (creds) {\n const credString = `${creds.username}:${creds.password}`;\n if (config.basicAuth.includes(credString)) {\n return { allowed: true, reason: \"basic\" };\n }\n }\n return { allowed: false, reason: \"rule\" };\n }\n\n default:\n return { allowed: false, reason: \"rule\" };\n }\n}\n\n/**\n * Apply default policy\n */\nfunction applyDefaultPolicy(\n config: AuthConfig,\n ip: string,\n authorization: string | undefined,\n): AuthResult {\n switch (config.defaultPolicy) {\n case \"allow\":\n return { allowed: true, reason: \"default\" };\n\n case \"deny\":\n return { allowed: false, reason: \"default\" };\n\n case \"basic\": {\n if (!authorization) return { allowed: false, reason: \"default\" };\n const creds = parseBasicAuth(authorization);\n if (creds) {\n const credString = `${creds.username}:${creds.password}`;\n if (config.basicAuth.includes(credString)) {\n return { allowed: true, reason: \"basic\" };\n }\n }\n // Also check tokens as fallback\n const token = parseBearerToken(authorization);\n if (token && config.tokens.includes(token)) {\n return { allowed: true, reason: \"token\" };\n }\n return { allowed: false, reason: \"default\" };\n }\n\n case \"token\": {\n if (!authorization) return { allowed: false, reason: \"default\" };\n const token = parseBearerToken(authorization);\n if (token && config.tokens.includes(token)) {\n return { allowed: true, reason: \"token\" };\n }\n return { allowed: false, reason: \"default\" };\n }\n\n default:\n return { allowed: false, reason: \"default\" };\n }\n}\n","import { readFileSync, existsSync, watch } from \"node:fs\";\nimport { execSync, execFile } from \"node:child_process\";\nimport type { DdevProject } from \"../types.js\";\n\n/** Strip a single pair of surrounding quotes. */\nfunction unquote(value: string): string {\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n return value.slice(1, -1);\n }\n\n return value;\n}\n\n/** A project name at the start of a line, e.g. `my-project:` */\nconst PROJECT_LINE = /^(\\S[^:]*):\\s*$/;\n\n/** An indented `approot: /path` belonging to the project above it */\nconst APPROOT_LINE = /^\\s+approot:\\s*(.+?)\\s*$/;\n\n/**\n * Load projects from DDEV's project_list.yaml\n * Returns a map of project name -> app root path\n *\n * The file nests the path under the project name:\n *\n * scalian:\n * approot: /home/ddev/www/scalian\n *\n * Indentation therefore has to be read. A flat key/value pass produces one\n * entry per line instead: the project mapped to an empty string, plus a\n * phantom project called `approot`. Both did damage — the empty path meant\n * `loadProjectConfig` looked for `.ddev/config.trafic.yaml` relative to the\n * working directory and never found it, so per-project settings were silently\n * ignored, and the phantom entered the hostname index, where the TLS ask\n * endpoint would vouch for `approot.<tld>` and have a certificate issued for\n * a project that does not exist.\n */\nexport function loadProjectList(\n projectListPath: string,\n): Map<string, string> {\n if (!existsSync(projectListPath)) {\n return new Map();\n }\n\n const projects = new Map<string, string>();\n let current: string | undefined;\n\n for (const line of readFileSync(projectListPath, \"utf-8\").split(\"\\n\")) {\n if (!line.trim() || line.trimStart().startsWith(\"#\")) {\n continue;\n }\n\n const project = PROJECT_LINE.exec(line);\n\n if (project) {\n current = unquote(project[1]!.trim());\n // Recorded now so a project keeps its hostname even if DDEV writes no\n // approot for it\n projects.set(current, \"\");\n continue;\n }\n\n const approot = APPROOT_LINE.exec(line);\n\n if (approot && current) {\n projects.set(current, unquote(approot[1]!));\n }\n }\n\n return projects;\n}\n\n/**\n * Build a hostname -> project name index\n */\nexport function buildHostnameIndex(\n projects: Map<string, string>,\n tld: string,\n): Map<string, string> {\n const index = new Map<string, string>();\n\n for (const [name] of projects) {\n // Primary hostname: project-name.tld\n index.set(`${name}.${tld}`, name);\n }\n\n return index;\n}\n\n/**\n * Get project name from hostname\n */\nexport function getProjectFromHostname(\n hostname: string,\n index: Map<string, string>,\n): string | undefined {\n return index.get(hostname);\n}\n\n/**\n * Watch project_list.yaml for changes\n */\nexport function watchProjectList(\n projectListPath: string,\n onChange: () => void,\n): void {\n if (!existsSync(projectListPath)) {\n console.warn(`Project list not found: ${projectListPath}`);\n return;\n }\n\n watch(projectListPath, (eventType) => {\n if (eventType === \"change\") {\n onChange();\n }\n });\n}\n\n/**\n * Run a ddev command without blocking the event loop.\n *\n * The agent is single-threaded and serves forward auth for every project, so\n * an `execSync` here stops the whole server for the duration of the command.\n * `ddev start` takes over a minute, which froze auth for every unrelated\n * project and produced request timeouts on a live server. Nothing about\n * \"background\" work with setTimeout helps: the timer callback still runs on\n * the same thread.\n */\nfunction ddev(\n args: string[],\n timeoutMs: number,\n): Promise<{ ok: boolean; stdout: string }> {\n return new Promise((resolve) => {\n execFile(\n \"ddev\",\n args,\n { encoding: \"utf-8\", timeout: timeoutMs },\n (error, stdout) => {\n if (error) {\n console.error(`ddev ${args.join(\" \")} failed:`, error.message);\n resolve({ ok: false, stdout: stdout ?? \"\" });\n return;\n }\n\n resolve({ ok: true, stdout: stdout ?? \"\" });\n },\n );\n });\n}\n\n/**\n * Get detailed project info using ddev describe\n */\nexport async function getProjectInfo(\n name: string,\n): Promise<DdevProject | undefined> {\n const { ok, stdout } = await ddev([\"describe\", name, \"-j\"], 10000);\n\n if (!ok) {\n return undefined;\n }\n\n try {\n const data = JSON.parse(stdout);\n\n if (!data.raw) return undefined;\n\n const raw = data.raw;\n return {\n name: raw.name,\n status: raw.status,\n appRoot: raw.approot,\n httpURLs: raw.httpURLs ?? [],\n httpsURLs: raw.httpsURLs ?? [],\n type: raw.type,\n phpVersion: raw.php_version,\n dbType: raw.dbinfo?.dbType,\n };\n } catch {\n return undefined;\n }\n}\n\n/**\n * Start a DDEV project.\n *\n * Resolves when the project is up. Callers in the request path must not await\n * it — respond first, then let this settle and record the outcome.\n */\nexport async function startProject(name: string): Promise<boolean> {\n const { ok } = await ddev([\"start\", name], 120000);\n return ok;\n}\n\n/**\n * Stop a DDEV project\n */\nexport async function stopProject(name: string): Promise<boolean> {\n const { ok } = await ddev([\"stop\", name], 60000);\n return ok;\n}\n\n/**\n * List all DDEV projects with their status\n */\nexport function listProjects(): DdevProject[] {\n try {\n const output = execSync(\"ddev list -j\", {\n encoding: \"utf-8\",\n timeout: 30000,\n });\n const data = JSON.parse(output);\n\n if (!Array.isArray(data)) return [];\n\n return data.map((p: Record<string, unknown>) => ({\n name: p.name as string,\n status: p.status as DdevProject[\"status\"],\n appRoot: p.approot as string,\n httpURLs: (p.httpURLs as string[]) ?? [],\n httpsURLs: (p.httpsURLs as string[]) ?? [],\n type: p.type as string,\n }));\n } catch {\n return [];\n }\n}\n","import { DatabaseSync } from \"node:sqlite\";\nimport type { ProjectRecord, AccessLog } from \"../types.js\";\n\nlet db: DatabaseSync | null = null;\n\n/**\n * Initialize the database connection and schema\n */\nexport function initDb(dbPath: string): DatabaseSync {\n db = new DatabaseSync(dbPath);\n db.exec(\"PRAGMA journal_mode=WAL\");\n\n // Create tables\n db.exec(`\n CREATE TABLE IF NOT EXISTS projects (\n name TEXT PRIMARY KEY,\n last_access INTEGER NOT NULL,\n status TEXT NOT NULL DEFAULT 'stopped'\n );\n\n CREATE TABLE IF NOT EXISTS access_logs (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project TEXT NOT NULL,\n timestamp INTEGER NOT NULL,\n ip TEXT NOT NULL,\n user_agent TEXT NOT NULL,\n path TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_access_logs_project ON access_logs(project);\n CREATE INDEX IF NOT EXISTS idx_access_logs_timestamp ON access_logs(timestamp);\n `);\n\n return db;\n}\n\n/**\n * Get the database instance\n */\nexport function getDb(): DatabaseSync {\n if (!db) {\n throw new Error(\"Database not initialized. Call initDb first.\");\n }\n return db;\n}\n\n/**\n * Close the database connection\n */\nexport function closeDb(): void {\n if (db) {\n db.close();\n db = null;\n }\n}\n\n/**\n * Update project last access time\n */\nexport function updateProjectAccess(name: string): 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, Date.now());\n}\n\n/**\n * Get project record\n */\nexport function getProject(name: string): ProjectRecord | undefined {\n const database = getDb();\n const stmt = database.prepare(\n \"SELECT name, last_access as lastAccess, status FROM projects WHERE name = ?\",\n );\n return stmt.get(name) as unknown as ProjectRecord | undefined;\n}\n\n/**\n * Set project status\n */\nexport function setProjectStatus(\n name: string,\n status: ProjectRecord[\"status\"],\n): void {\n const database = getDb();\n const stmt = database.prepare(`\n INSERT INTO projects (name, last_access, status)\n VALUES (?, ?, ?)\n ON CONFLICT(name) DO UPDATE SET status = excluded.status\n `);\n stmt.run(name, Date.now(), status);\n}\n\n/**\n * Get idle projects (last access older than threshold)\n */\nexport function getIdleProjects(thresholdMs: number): ProjectRecord[] {\n const database = getDb();\n const cutoff = Date.now() - thresholdMs;\n const stmt = database.prepare(`\n SELECT name, last_access as lastAccess, status\n FROM projects\n WHERE last_access < ? AND status = 'running'\n `);\n return stmt.all(cutoff) as unknown as ProjectRecord[];\n}\n\n/**\n * Log an access\n */\nexport function logAccess(log: Omit<AccessLog, \"id\">): void {\n const database = getDb();\n const stmt = database.prepare(`\n INSERT INTO access_logs (project, timestamp, ip, user_agent, path)\n VALUES (?, ?, ?, ?, ?)\n `);\n stmt.run(log.project, log.timestamp, log.ip, log.userAgent, log.path);\n}\n\n/**\n * Get recent access logs for a project\n */\nexport function getAccessLogs(project: string, limit = 100): AccessLog[] {\n const database = getDb();\n const stmt = database.prepare(`\n SELECT id, project, timestamp, ip, user_agent as userAgent, path\n FROM access_logs\n WHERE project = ?\n ORDER BY timestamp DESC\n LIMIT ?\n `);\n return stmt.all(project, limit) as unknown as AccessLog[];\n}\n\n/**\n * Clean old access logs (older than given days)\n */\nexport function cleanOldLogs(days: number): number {\n const database = getDb();\n const cutoff = Date.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 { execSync } from \"node:child_process\";\nimport { writeFileSync as fsWriteFile } from \"node:fs\";\n\nlet stepNumber = 0;\nlet dryRun = false;\n\n/**\n * Set dry-run mode\n */\nexport function setDryRun(value: boolean): void {\n dryRun = value;\n}\n\n/**\n * Reset step counter\n */\nexport function resetSteps(): void {\n stepNumber = 0;\n}\n\n/**\n * Log a step\n */\nexport function step(message: string): void {\n stepNumber++;\n console.log(`\\n\\x1b[36m[${stepNumber}]\\x1b[0m ${message}`);\n}\n\n/**\n * Log success\n */\nexport function success(message: string): void {\n console.log(` \\x1b[32m✓\\x1b[0m ${message}`);\n}\n\n/**\n * Log warning\n */\nexport function warn(message: string): void {\n console.log(` \\x1b[33m⚠\\x1b[0m ${message}`);\n}\n\n/**\n * Log error\n */\nexport function error(message: string): void {\n console.log(` \\x1b[31m✗\\x1b[0m ${message}`);\n}\n\n/**\n * Log info\n */\nexport function info(message: string): void {\n console.log(` ${message}`);\n}\n\n/**\n * Execute a command (or show in dry-run mode)\n */\nexport function exec(command: string, options?: { silent?: boolean }): string {\n if (dryRun) {\n if (!options?.silent) {\n console.log(` \\x1b[90m$ ${command}\\x1b[0m`);\n }\n return \"\";\n }\n\n try {\n return execSync(command, {\n encoding: \"utf-8\",\n stdio: options?.silent ? \"pipe\" : \"inherit\",\n });\n } catch (err) {\n const e = err as { status?: number; stderr?: string };\n throw new Error(`Command failed: ${command}\\n${e.stderr ?? \"\"}`);\n }\n}\n\n/**\n * Execute a command and return output (silent)\n */\nexport function execSilent(command: string): string {\n if (dryRun) {\n return \"\";\n }\n\n try {\n return execSync(command, {\n encoding: \"utf-8\",\n stdio: \"pipe\",\n }).trim();\n } catch {\n return \"\";\n }\n}\n\n/**\n * Check if a command exists\n */\nexport function commandExists(command: string): boolean {\n try {\n execSync(`which ${command}`, { stdio: \"pipe\" });\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Check if running as root\n */\nexport function isRoot(): boolean {\n return process.getuid?.() === 0;\n}\n\n/**\n * Write a file (or show in dry-run mode)\n */\nexport function writeFile(path: string, content: string): void {\n if (dryRun) {\n console.log(` \\x1b[90mWrite ${path}:\\x1b[0m`);\n const lines = content.split(\"\\n\").slice(0, 5);\n for (const line of lines) {\n console.log(` \\x1b[90m ${line}\\x1b[0m`);\n }\n if (content.split(\"\\n\").length > 5) {\n console.log(` \\x1b[90m ...\\x1b[0m`);\n }\n return;\n }\n\n fsWriteFile(path, content);\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { commandExists, exec, execSilent, writeFile } from \"./steps.js\";\n\n/**\n * The effectful operations the setup steps need.\n *\n * Injected into every setup function so tests can drive them with plain\n * fakes — no module mocking — and so dry-run has one place to intercept\n * writes instead of each module reaching for `node:fs` itself.\n */\nexport interface SetupIo {\n /** Run a shell command. Throws when it exits non-zero. */\n exec(command: string, options?: { silent?: boolean }): string;\n /**\n * Run a probe. Returns trimmed stdout, or \"\" when the command fails.\n *\n * Use this wherever a non-zero exit is an answer rather than an error —\n * `id -u ddev` on a server without that user, for instance.\n */\n execSilent(command: string): string;\n /** Whether a command is on the PATH. */\n commandExists(command: string): boolean;\n /** Write a file. Honours dry-run. */\n writeFile(path: string, content: string): void;\n /** Whether a path exists. */\n fileExists(path: string): boolean;\n /** Read a UTF-8 file. */\n readFile(path: string): string;\n /** Process environment, for values like SUDO_USER. */\n env: Record<string, string | undefined>;\n}\n\n/**\n * The real implementation, used when no io is injected.\n *\n * `writeFile` comes from steps.js rather than `node:fs` so writes are\n * skipped in dry-run mode. Reads stay direct: they change nothing.\n */\nexport const nodeIo: SetupIo = {\n exec,\n execSilent,\n commandExists,\n writeFile,\n fileExists: existsSync,\n readFile: (path) => readFileSync(path, \"utf-8\"),\n env: process.env,\n};\n","import { nodeIo, type SetupIo } from \"./io.js\";\nimport { step, success, info } from \"./steps.js\";\n\n/**\n * Install system dependencies required by Trafic\n */\nexport function installSystemDeps(io: SetupIo = nodeIo): void {\n step(\"Install system dependencies\");\n\n io.exec(\"apt-get update -qq\", { silent: true });\n // gnupg and ca-certificates are needed to add the DDEV and NodeSource\n // apt repositories — a minimal Ubuntu image ships with neither\n io.exec(\n \"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y jq curl rsync gnupg ca-certificates\",\n { silent: true },\n );\n\n success(\"System dependencies installed: jq, curl, rsync, gnupg, ca-certificates\");\n}\n\n/**\n * Add the official DDEV apt repository and GPG key.\n * Idempotent: safe to call even if the repository is already configured.\n */\nexport function addDdevAptRepo(io: SetupIo = nodeIo): void {\n io.exec(\"install -m 0755 -d /etc/apt/keyrings\", { silent: true });\n io.exec(\n \"curl -fsSL https://pkg.ddev.com/apt/gpg.key | gpg --dearmor | tee /etc/apt/keyrings/ddev.gpg > /dev/null\",\n { silent: true },\n );\n io.exec(\"chmod a+r /etc/apt/keyrings/ddev.gpg\", { silent: true });\n io.exec(\n \"echo \\\"deb [signed-by=/etc/apt/keyrings/ddev.gpg] https://pkg.ddev.com/apt/ * *\\\" | tee /etc/apt/sources.list.d/ddev.list > /dev/null\",\n { silent: true },\n );\n}\n\n/**\n * Install DDEV\n */\nexport function installDdev(io: SetupIo = nodeIo): void {\n step(\"Install DDEV\");\n\n if (io.commandExists(\"ddev\")) {\n const version = io.exec(\"ddev --version 2>/dev/null | head -1\", { silent: true });\n info(`DDEV already installed: ${version?.trim() ?? \"unknown version\"}`);\n return;\n }\n\n // Install DDEV via the official apt repository\n // See https://docs.ddev.com/en/stable/users/install/ddev-installation/#debianubuntu\n info(\"Adding DDEV apt repository...\");\n addDdevAptRepo(io);\n\n info(\"Installing DDEV...\");\n io.exec(\"apt-get update -qq\", { silent: true });\n io.exec(\"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ddev\", { silent: true });\n\n // Initialize mkcert certificate authority for the ddev user.\n // mkcert installs the CA into the home directory of the current user.\n // Since setup runs as root, we must point HOME to the ddev user's home\n // so the CA is trusted by DDEV when it runs as that user.\n io.exec(\"HOME=/home/ddev mkcert -install\", { silent: true });\n io.exec(\"chown -R ddev:ddev /home/ddev/.local/share/mkcert\", { silent: true });\n\n const version = io.exec(\"ddev --version 2>/dev/null | head -1\", { silent: true });\n success(`DDEV installed: ${version?.trim() ?? \"unknown version\"}`);\n}\n\n/**\n * Configure DDEV global settings for production\n *\n * Note on DNS and /etc/hosts:\n * When using a custom TLD (not ddev.site), DDEV may try to edit /etc/hosts\n * if it can't resolve project hostnames via DNS. To avoid this:\n * 1. Configure DNS so *.{tld} resolves to the server's IP (recommended)\n * 2. Or install dnsmasq for local wildcard DNS resolution\n *\n * With proper DNS, DDEV won't need sudo for hostname management.\n */\nexport function configureDdev(\n tld: string,\n email?: string,\n io: SetupIo = nodeIo,\n): void {\n step(\"Configure DDEV\");\n\n // Run as ddev user\n const ddevCmd = (cmd: string) => `su - ddev -c '${cmd}'`;\n\n // Configure all global settings in a single call\n io.exec(\n ddevCmd(\n `ddev config global --project-tld=${tld} --router-http-port=80 --router-https-port=443 --router-bind-all-interfaces=true --use-letsencrypt=${email ? \"true\" : \"false\"} ${email ? `--letsencrypt-email=${email}` : \"\"} --instrumentation-opt-in=false`,\n ),\n { silent: true },\n );\n\n success(`DDEV configured with TLD: ${tld}`);\n info(`Ensure DNS is configured: *.${tld} → server IP`);\n\n if (email) {\n success(`Let's Encrypt enabled with email: ${email}`);\n }\n\n success(\"DDEV global settings configured\");\n}\n\n/**\n * Install dnsmasq for local DNS resolution (optional)\n * This allows the server itself to resolve *.{tld} hostnames\n * without needing to edit /etc/hosts for each project\n */\nexport function installDnsmasq(tld: string, io: SetupIo = nodeIo): void {\n step(\"Install dnsmasq for local DNS\");\n\n io.exec(\"apt-get update && apt-get install -y dnsmasq\", { silent: true });\n\n // Get server's public IP\n const serverIp = io.exec(\n \"curl -4 -s ifconfig.me || hostname -I | awk '{print $1}'\",\n { silent: true },\n )?.trim() || \"127.0.0.1\";\n\n // Configure dnsmasq to resolve *.{tld} to server IP\n const dnsmasqConfig = `# Trafic: Local DNS for DDEV projects\n# Resolve all *.${tld} to this server\naddress=/${tld}/${serverIp}\n\n# Don't read /etc/resolv.conf\nno-resolv\n\n# Use upstream DNS for everything else\nserver=1.1.1.1\nserver=8.8.8.8\n\n# Listen on localhost only\nlisten-address=127.0.0.1\nbind-interfaces\n`;\n\n io.writeFile(\"/etc/dnsmasq.d/trafic.conf\", dnsmasqConfig);\n\n // Configure system to use local dnsmasq\n io.exec(\"systemctl restart dnsmasq\");\n io.exec(\"systemctl enable dnsmasq\");\n\n // Update resolv.conf to use local DNS first\n info(\"Configure /etc/resolv.conf to use 127.0.0.1 as primary DNS\");\n\n success(`dnsmasq configured: *.${tld} → ${serverIp}`);\n}\n\n/**\n * Get the Docker gateway IP reachable from inside DDEV containers on Linux.\n * DDEV runs containers on the `ddev_default` network — we inspect that first.\n * Falls back to the default `bridge` network, then to 172.17.0.1.\n * `host.docker.internal` is not available on Linux without extra configuration.\n */\nexport function getDockerGatewayIp(io: SetupIo = nodeIo): string {\n // Prefer the ddev_default network gateway — that's the network ddev-router is on\n const ddevIp = io.execSilent(\n \"docker network inspect ddev_default --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}'\",\n );\n if (ddevIp) return ddevIp;\n\n // Fall back to the default bridge network\n const bridgeIp = io.execSilent(\n \"docker network inspect bridge --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}'\",\n );\n return bridgeIp || \"172.17.0.1\";\n}\n\n/** 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/**\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 */\nexport function buildStaticConfig(\n toolPorts: string[],\n routerPorts: RouterPorts = DEFAULT_ROUTER_PORTS,\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 return `entryPoints:\n${web.map((port) => webEntryPoint(`http-${port}`)).join(\"\")}${tools.map(toolEntryPoint).join(\"\")}`;\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 # Lowest priority, so a DDEV project router always wins. These only match\n # when none does — which is what a stopped project produces, because DDEV\n # removes that project's router and Traefik would otherwise answer 404.\n # Without them the waiting page never appears and scale-to-zero never\n # restarts anything. Auth is unaffected: it is attached at the entry\n # point, not here, so this cannot reintroduce the bypass that #27 fixed.\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: 1\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: 1\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/**\n * Configure Traefik for forward auth\n */\nexport function configureTraefik(io: SetupIo = nodeIo): void {\n step(\"Configure Traefik for forward auth\");\n\n // Create custom Traefik config directories\n io.exec(\"mkdir -p /home/ddev/.ddev/traefik/custom-global-config\", { silent: true });\n io.exec(\"chown -R ddev:ddev /home/ddev/.ddev\", { silent: true });\n\n // Use the Docker bridge gateway IP instead of host.docker.internal —\n // the latter is not available on Linux without extra Docker configuration.\n const gatewayIp = getDockerGatewayIp(io);\n\n // 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(\"/home/ddev/.ddev/traefik/custom-global-config/trafic.yaml\", dynamicConfig);\n io.exec(\"chown ddev:ddev /home/ddev/.ddev/traefik/custom-global-config/trafic.yaml\", { silent: true });\n\n // Static configuration: attaches trafic-auth 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);\n\n io.writeFile(\"/home/ddev/.ddev/traefik/static_config.trafic.yaml\", staticConfig);\n io.exec(\"chown ddev:ddev /home/ddev/.ddev/traefik/static_config.trafic.yaml\", { silent: true });\n\n info(`Entry points: http-${routerPorts.http}, http-${routerPorts.https} (web) plus tool ports`);\n\n success(\"Traefik configured with Trafic middleware\");\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 { createServer, type IncomingMessage, type ServerResponse } from \"node:http\";\nimport { readFileSync } from \"node:fs\";\nimport { resolve, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { AgentConfig, AuthConfig } from \"./types.js\";\nimport { checkAuth, resolveClientIp } from \"./utils/auth.js\";\nimport {\n loadProjectList,\n buildHostnameIndex,\n watchProjectList,\n startProject,\n getProjectInfo,\n} from \"./utils/ddev.js\";\nimport {\n initDb,\n updateProjectAccess,\n logAccess,\n setProjectStatus,\n getProject,\n} from \"./utils/db.js\";\nimport { loadProjectConfig } from \"./utils/project-config.js\";\nimport { syncForwardAuthAddress } from \"./utils/traefik.js\";\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 * 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 */\nfunction getEffectiveAuthConfig(projectName: string | undefined): AuthConfig {\n if (!projectName) return config.auth;\n\n const projectConfig = projectConfigs.get(projectName);\n if (!projectConfig?.auth_policy) return config.auth;\n\n // Override default policy with project-specific policy\n return {\n ...config.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 */\nfunction handleAuth(req: IncomingMessage, res: ServerResponse): 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 = hostnameIndex.get(hostname);\n\n // Get effective auth config (global + per-project overrides)\n const authConfig = getEffectiveAuthConfig(projectName);\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 updateProjectAccess(projectName);\n 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 */\nasync function handleErrors(\n req: IncomingMessage,\n res: ServerResponse,\n): Promise<void> {\n const hostname = req.headers[\"x-forwarded-host\"] as string ?? req.headers.host ?? \"\";\n const projectName = hostnameIndex.get(hostname);\n\n if (!projectName) {\n // Unknown project\n const template = loadTemplate(\"error\");\n res.writeHead(404, { \"Content-Type\": \"text/html\" });\n res.end(template.replace(\"{{message}}\", \"Project not found\"));\n return;\n }\n\n // Check if project is already starting\n const record = getProject(projectName);\n if (record?.status === \"starting\") {\n // Show waiting page\n const template = 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 setProjectStatus(projectName, \"starting\");\n\n // Show waiting page immediately\n const template = 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 startProject(projectName)\n .then((success) => {\n setProjectStatus(projectName, success ? \"running\" : \"stopped\");\n })\n .catch((error: unknown) => {\n // Leaving it \"starting\" forever would wedge the waiting page\n setProjectStatus(projectName, \"stopped\");\n console.error(`Could not start ${projectName}:`, error);\n });\n}\n\n/**\n * Decide whether Caddy may obtain a certificate for a hostname.\n *\n * Caddy's on-demand TLS asks before issuing for a name it has not seen.\n * Without a check, anyone pointing a DNS record at this server would make it\n * request a certificate, and Let's Encrypt quota is per registered domain —\n * a handful of strangers could exhaust it for every preview.\n */\nexport function allowTlsFor(\n domain: string,\n index: Map<string, string>,\n): boolean {\n return index.has(domain.toLowerCase());\n}\n\n/**\n * Handle Caddy's on-demand TLS ask. 2xx allows issuance, anything else denies.\n */\nfunction handleTlsAsk(req: IncomingMessage, res: ServerResponse): void {\n const url = new URL(req.url ?? \"/\", `http://${req.headers.host}`);\n const domain = url.searchParams.get(\"domain\") ?? \"\";\n\n if (allowTlsFor(domain, hostnameIndex)) {\n res.writeHead(200);\n res.end(\"ok\");\n return;\n }\n\n console.log(`TLS ask denied for unknown hostname: ${domain || \"(none)\"}`);\n res.writeHead(404, { \"Content-Type\": \"text/plain\" });\n res.end(\"Unknown project\");\n}\n\n/**\n * Handle status polling requests\n */\nasync function handleStatus(\n req: IncomingMessage,\n res: ServerResponse,\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 getProjectInfo(projectName);\n const record = 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 */\nasync function handleRequest(\n req: IncomingMessage,\n res: ServerResponse,\n): Promise<void> {\n // Route on the path alone. Matching the raw URL meant a query string threw\n // every internal route off: Caddy's forward_auth passes the original query\n // through, so `/__auth__?s=term` missed the auth branch and fell to the\n // waiting page. Any URL carrying a query — a search, pagination, the\n // `wp-login.php?redirect_to=` an admin request lands on — answered 503 and\n // started an already-running project, and `startProject` is synchronous, so\n // it stalled the agent and every request waiting on it.\n const path = routePath(req.url);\n\n try {\n // Route requests\n if (path === \"/__auth__\" || path.startsWith(\"/__auth__/\")) {\n handleAuth(req, res);\n } else if (path === \"/__status__\" || path.startsWith(\"/__status__/\")) {\n await handleStatus(req, res);\n } else if (path === \"/__tls__\") {\n handleTlsAsk(req, res);\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);\n }\n } catch (error) {\n console.error(\"Request error:\", error);\n res.writeHead(500, { \"Content-Type\": \"text/plain\" });\n res.end(\"Internal Server Error\");\n }\n}\n\n/**\n * Reload project list and their configs\n */\nfunction reloadProjects(): void {\n projectList = loadProjectList(config.projectListPath);\n hostnameIndex = buildHostnameIndex(projectList, config.tld);\n\n // Load per-project configs\n projectConfigs.clear();\n for (const [name, projectDir] of projectList) {\n const projectConfig = loadProjectConfig(projectDir);\n if (projectConfig.auth_policy || projectConfig.idle_timeout) {\n projectConfigs.set(name, projectConfig);\n console.log(` ${name}: auth=${projectConfig.auth_policy ?? \"default\"}, idle=${projectConfig.idle_timeout ?? \"default\"}`);\n }\n }\n\n console.log(`Loaded ${projectList.size} projects (${projectConfigs.size} with custom config)`);\n}\n\n/**\n * Start the agent server\n */\nexport function startServer(agentConfig: AgentConfig): void {\n config = agentConfig;\n\n // Initialize database\n initDb(config.dbPath);\n\n // setup wrote the forward auth address before any DDEV network existed, so\n // correct it now that one may have appeared\n syncForwardAuthAddress(config.projectListPath);\n\n // 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 // Create HTTP server\n const server = createServer((req, res) => {\n handleRequest(req, res).catch((error) => {\n console.error(\"Unhandled error:\", error);\n if (!res.headersSent) {\n res.writeHead(500);\n res.end(\"Internal Server Error\");\n }\n });\n });\n\n server.listen(config.port, () => {\n console.log(`Trafic agent listening on port ${config.port}`);\n console.log(`TLD: ${config.tld}`);\n console.log(`Projects: ${projectList.size}`);\n });\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAc,MAAM,YAAY,CAAC;AA6K1D;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GACzB,OAAO,CAET;AA8CD;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAU5D;AA4DD;;GAEG;AACH,wBAAgB,WAAW,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAmC1D"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAc,MAAM,YAAY,CAAC;AAsL1D;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GACzB,OAAO,CAET;AAiDD;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAU5D;AA4DD;;GAEG;AACH,wBAAgB,WAAW,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAmC1D"}
package/dist/server.js CHANGED
@@ -1,2 +1,2 @@
1
- import { n as routePath, r as startServer, t as allowTlsFor } from "./server-C4j-C4Q1.js";
1
+ import { n as routePath, r as startServer, t as allowTlsFor } from "./server-3g76yism.js";
2
2
  export { allowTlsFor, routePath, startServer };
@@ -46,6 +46,20 @@ export declare function getDockerGatewayIp(io?: SetupIo): string;
46
46
  * are configurable, and an entry point Traefik does not know about is ignored.
47
47
  */
48
48
  export declare function readToolPorts(io?: SetupIo): string[];
49
+ /** The ports ddev-router serves projects on. */
50
+ export interface RouterPorts {
51
+ http: string;
52
+ https: string;
53
+ }
54
+ /**
55
+ * Read the ports ddev-router serves projects on.
56
+ *
57
+ * Traefik names an entry point after its port, so these decide what the
58
+ * catch-all routers and the auth middleware must attach to. Hardcoding 80 and
59
+ * 443 is wrong on any server where the router was moved — which is every
60
+ * server running a host ingress in front of it.
61
+ */
62
+ export declare function readRouterPorts(io?: SetupIo): RouterPorts;
49
63
  /**
50
64
  * Build the static config attaching forward auth to every entry point.
51
65
  *
@@ -53,7 +67,20 @@ export declare function readToolPorts(io?: SetupIo): string[];
53
67
  * stopped project should show the waiting page, while a tool port has nothing
54
68
  * to wait for.
55
69
  */
56
- export declare function buildStaticConfig(toolPorts: string[]): string;
70
+ export declare function buildStaticConfig(toolPorts: string[], routerPorts?: RouterPorts): string;
71
+ /**
72
+ * Build the dynamic config: the middlewares, the agent service, and the
73
+ * catch-all routers that hand a stopped project to the waiting page.
74
+ *
75
+ * One catch-all per web entry point, each naming the entry point it serves.
76
+ * A router that names none is instantiated by Traefik on *every* entry point,
77
+ * and DDEV's router health check compares the number of router definitions in
78
+ * its config files against the number Traefik reports. Two unpinned
79
+ * definitions became fourteen instances on a server with six tool entry
80
+ * points, so the counts never matched, the health check waited out its
81
+ * timeout, and every `ddev start` failed after 60 seconds.
82
+ */
83
+ export declare function buildDynamicConfig(gatewayIp: string, routerPorts?: RouterPorts): string;
57
84
  /**
58
85
  * Configure Traefik for forward auth
59
86
  */
@@ -1 +1 @@
1
- {"version":3,"file":"ddev.d.ts","sourceRoot":"","sources":["../../src/setup/ddev.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,OAAO,EAAE,MAAM,SAAS,CAAC;AAG/C;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,GAAE,OAAgB,GAAG,IAAI,CAY5D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAAE,GAAE,OAAgB,GAAG,IAAI,CAWzD;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,EAAE,GAAE,OAAgB,GAAG,IAAI,CA2BtD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,MAAM,EACd,EAAE,GAAE,OAAgB,GACnB,IAAI,CAsBN;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,GAAE,OAAgB,GAAG,IAAI,CAsCtE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,GAAE,OAAgB,GAAG,MAAM,CAY/D;AAUD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,EAAE,GAAE,OAAgB,GAAG,MAAM,EAAE,CAU5D;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,MAAM,CAgB7D;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,GAAE,OAAgB,GAAG,IAAI,CA4E3D"}
1
+ {"version":3,"file":"ddev.d.ts","sourceRoot":"","sources":["../../src/setup/ddev.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,OAAO,EAAE,MAAM,SAAS,CAAC;AAG/C;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,GAAE,OAAgB,GAAG,IAAI,CAY5D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAAE,GAAE,OAAgB,GAAG,IAAI,CAWzD;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,EAAE,GAAE,OAAgB,GAAG,IAAI,CA2BtD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,MAAM,EACd,EAAE,GAAE,OAAgB,GACnB,IAAI,CAsBN;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,GAAE,OAAgB,GAAG,IAAI,CAsCtE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,GAAE,OAAgB,GAAG,MAAM,CAY/D;AAUD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,EAAE,GAAE,OAAgB,GAAG,MAAM,EAAE,CAU5D;AAED,gDAAgD;AAChD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAKD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,EAAE,GAAE,OAAgB,GAAG,WAAW,CAUjE;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EAAE,EACnB,WAAW,GAAE,WAAkC,GAC9C,MAAM,CAqBR;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,WAAW,GAAE,WAAkC,GAC9C,MAAM,CAoDR;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,GAAE,OAAgB,GAAG,IAAI,CAoC3D"}
@@ -0,0 +1,23 @@
1
+ import type { Migration } from "../types.js";
2
+ /**
3
+ * Migration 0010: pin the catch-all routers to their entry points.
4
+ *
5
+ * The routers added by 0009 named no entry point, so Traefik instantiated
6
+ * each on every one. DDEV's router health check counts router *definitions*
7
+ * across its config files and compares that to what Traefik reports, so two
8
+ * definitions showing up as fourteen instances never matched. The check waited
9
+ * out its timeout and `ddev start` failed after 60 seconds — on every server
10
+ * setup produces, because setup configures the tool entry points.
11
+ *
12
+ * Observed on a live server: 37 routers loaded against 22 expected, health
13
+ * still "starting" after 91s. With the routers pinned: 23 loaded, healthy
14
+ * immediately.
15
+ *
16
+ * Traefik reads its config from the ddev-global-cache volume and DDEV only
17
+ * copies `~/.ddev/traefik` into it when a project starts, so this restarts
18
+ * DDEV rather than the router alone.
19
+ *
20
+ * Idempotent: skipped once the config names entry points.
21
+ */
22
+ export declare const migration0010CatchallEntrypoints: Migration;
23
+ //# sourceMappingURL=0010__catchall_entrypoints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"0010__catchall_entrypoints.d.ts","sourceRoot":"","sources":["../../../src/setup/migrations/0010__catchall_entrypoints.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAQ7C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,gCAAgC,EAAE,SA+B9C,CAAC"}
@@ -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;AAWjF,iDAAiD;AACjD,eAAO,MAAM,qBAAqB,iCAAiC,CAAC;AAEpE,+CAA+C;AAC/C,eAAO,MAAM,cAAc,EAAE,SAAS,EAUrC,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"}
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;AAYjF,iDAAiD;AACjD,eAAO,MAAM,qBAAqB,iCAAiC,CAAC;AAEpE,+CAA+C;AAC/C,eAAO,MAAM,cAAc,EAAE,SAAS,EAWrC,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"}