@studiometa/trafic-agent 0.1.25 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { _ as getProjectInfo, i as shouldNeverStop, l as getIdleProjects, n as getIdleTimeoutMs, p as setProjectStatus, r as loadProjectConfig, x as stopProject, y as loadProjectList } from "./server-VlzPosuk.js";
1
+ import { _ as getProjectInfo, i as shouldNeverStop, l as getIdleProjects, n as getIdleTimeoutMs, p as setProjectStatus, r as loadProjectConfig, x as stopProject, y as loadProjectList } from "./server-zVWzPuj2.js";
2
2
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { parse } from "smol-toml";
4
4
  import { execSync } from "node:child_process";
@@ -21,7 +21,8 @@ var DEFAULT_CONFIG = {
21
21
  allowedIps: [],
22
22
  tokens: [],
23
23
  basicAuth: [],
24
- rules: []
24
+ rules: [],
25
+ trustedProxyHops: 1
25
26
  }
26
27
  };
27
28
  /**
@@ -87,10 +88,23 @@ function mergeAuthConfig(raw) {
87
88
  policy: r.policy,
88
89
  tokens: r.tokens,
89
90
  allowedIps: r.allowed_ips
90
- }))
91
+ })),
92
+ trustedProxyHops: parseTrustedProxyHops(raw.trusted_proxy_hops)
91
93
  };
92
94
  }
93
95
  /**
96
+ * Parse trusted_proxy_hops, ignoring values that cannot be a hop count.
97
+ *
98
+ * A bad value falls back to the default rather than 0, because 0 would read
99
+ * the socket peer — always the proxy — and quietly stop the IP allowlist and
100
+ * per-hostname IP rules from ever matching.
101
+ */
102
+ function parseTrustedProxyHops(raw) {
103
+ const hops = typeof raw === "number" ? raw : Number(raw);
104
+ if (!Number.isInteger(hops) || hops < 1) return DEFAULT_CONFIG.auth.trustedProxyHops;
105
+ return hops;
106
+ }
107
+ /**
94
108
  * Validate configuration
95
109
  */
96
110
  function validateConfig(config) {
@@ -237,39 +251,68 @@ function commandExists(command) {
237
251
  function isRoot() {
238
252
  return process.getuid?.() === 0;
239
253
  }
254
+ /**
255
+ * Write a file (or show in dry-run mode)
256
+ */
257
+ function writeFile(path, content) {
258
+ if (dryRun) {
259
+ console.log(` \x1b[90mWrite ${path}:\x1b[0m`);
260
+ const lines = content.split("\n").slice(0, 5);
261
+ for (const line of lines) console.log(` \x1b[90m ${line}\x1b[0m`);
262
+ if (content.split("\n").length > 5) console.log(` \x1b[90m ...\x1b[0m`);
263
+ return;
264
+ }
265
+ writeFileSync(path, content);
266
+ }
267
+ //#endregion
268
+ //#region src/setup/io.ts
269
+ /**
270
+ * The real implementation, used when no io is injected.
271
+ *
272
+ * `writeFile` comes from steps.js rather than `node:fs` so writes are
273
+ * skipped in dry-run mode. Reads stay direct: they change nothing.
274
+ */
275
+ var nodeIo = {
276
+ exec,
277
+ commandExists,
278
+ writeFile,
279
+ fileExists: existsSync,
280
+ readFile: (path) => readFileSync(path, "utf-8"),
281
+ env: process.env
282
+ };
240
283
  //#endregion
241
284
  //#region src/setup/user.ts
242
285
  /**
243
286
  * Write /etc/sudoers.d/trafic-ddev allowing the ddev user to run
244
287
  * ddev-hostname without a password. Idempotent — safe to call multiple times.
245
288
  */
246
- function configureDdevSudoers() {
247
- writeFileSync("/etc/sudoers.d/trafic-ddev", `# Trafic: allow ddev user to run ddev-hostname without a password.
289
+ function configureDdevSudoers(io = nodeIo) {
290
+ io.writeFile("/etc/sudoers.d/trafic-ddev", `# Trafic: allow ddev user to run ddev-hostname without a password.
248
291
  # ddev-hostname manages /etc/hosts and always calls sudo internally.
249
292
  ddev ALL=(ALL) NOPASSWD: /usr/bin/ddev-hostname
250
293
  `);
251
- exec("chmod 440 /etc/sudoers.d/trafic-ddev", { silent: true });
294
+ io.exec("chmod 440 /etc/sudoers.d/trafic-ddev", { silent: true });
252
295
  success("Configured passwordless sudo for ddev-hostname");
253
296
  }
254
297
  /**
255
298
  * Create the ddev user with proper permissions
256
299
  */
257
- function createDdevUser() {
300
+ function createDdevUser(io = nodeIo) {
258
301
  step("Create ddev user");
259
- if (execSilent$1("id -u ddev 2>/dev/null")) info("User 'ddev' already exists");
302
+ if (io.exec("id -u ddev 2>/dev/null", { silent: true })) info("User 'ddev' already exists");
260
303
  else {
261
- exec("useradd -m -s /bin/bash ddev");
304
+ io.exec("useradd -m -s /bin/bash ddev");
262
305
  success("Created user 'ddev'");
263
306
  }
264
- exec("usermod -aG docker ddev 2>/dev/null || true", { silent: true });
265
- exec("mkdir -p /home/ddev/www");
266
- exec("chown ddev:ddev /home/ddev/www");
267
- exec("chmod 750 /home/ddev/www");
307
+ io.exec("usermod -aG docker ddev 2>/dev/null || true", { silent: true });
308
+ io.exec("mkdir -p /home/ddev/www");
309
+ io.exec("chown ddev:ddev /home/ddev/www");
310
+ io.exec("chmod 750 /home/ddev/www");
268
311
  success("Created /home/ddev/www");
269
- exec("mkdir -p /home/ddev/.ssh");
270
- exec("chmod 700 /home/ddev/.ssh");
271
- exec("chown ddev:ddev /home/ddev/.ssh");
272
- configureDdevSudoers();
312
+ io.exec("mkdir -p /home/ddev/.ssh");
313
+ io.exec("chmod 700 /home/ddev/.ssh");
314
+ io.exec("chown ddev:ddev /home/ddev/.ssh");
315
+ configureDdevSudoers(io);
273
316
  success("User ddev configured");
274
317
  }
275
318
  //#endregion
@@ -277,26 +320,26 @@ function createDdevUser() {
277
320
  /**
278
321
  * Install Docker using the official script
279
322
  */
280
- function installDocker() {
323
+ function installDocker(io = nodeIo) {
281
324
  step("Install Docker");
282
- if (commandExists("docker")) {
283
- info(`Docker already installed: ${exec("docker --version", { silent: true })?.trim() ?? "unknown version"}`);
325
+ if (io.commandExists("docker")) {
326
+ info(`Docker already installed: ${io.exec("docker --version", { silent: true })?.trim() ?? "unknown version"}`);
284
327
  return;
285
328
  }
286
329
  info("Downloading and running Docker install script...");
287
- exec("curl -fsSL https://get.docker.com -o /tmp/get-docker.sh");
288
- exec("sh /tmp/get-docker.sh", { silent: true });
289
- exec("rm /tmp/get-docker.sh");
290
- exec("systemctl enable docker", { silent: true });
291
- exec("systemctl start docker", { silent: true });
330
+ io.exec("curl -fsSL https://get.docker.com -o /tmp/get-docker.sh");
331
+ io.exec("sh /tmp/get-docker.sh", { silent: true });
332
+ io.exec("rm /tmp/get-docker.sh");
333
+ io.exec("systemctl enable docker", { silent: true });
334
+ io.exec("systemctl start docker", { silent: true });
292
335
  success("Docker installed and started");
293
- exec("usermod -aG docker ddev", { silent: true });
336
+ io.exec("usermod -aG docker ddev", { silent: true });
294
337
  success("Added ddev user to docker group");
295
338
  }
296
339
  /**
297
340
  * Configure Docker for production use
298
341
  */
299
- function configureDocker() {
342
+ function configureDocker(io = nodeIo) {
300
343
  step("Configure Docker");
301
344
  const daemonConfig = {
302
345
  "log-driver": "json-file",
@@ -307,32 +350,32 @@ function configureDocker() {
307
350
  "storage-driver": "overlay2",
308
351
  "live-restore": true
309
352
  };
310
- exec("mkdir -p /etc/docker");
353
+ io.exec("mkdir -p /etc/docker");
311
354
  const configPath = "/etc/docker/daemon.json";
312
- if (existsSync(configPath)) try {
355
+ if (io.fileExists(configPath)) try {
313
356
  const merged = {
314
- ...JSON.parse(readFileSync(configPath, "utf-8")),
357
+ ...JSON.parse(io.readFile(configPath)),
315
358
  ...daemonConfig
316
359
  };
317
- writeFileSync(configPath, JSON.stringify(merged, null, 2));
360
+ io.writeFile(configPath, JSON.stringify(merged, null, 2));
318
361
  info("Merged with existing Docker config");
319
362
  } catch {
320
363
  warn("Could not parse existing Docker config, skipping");
321
364
  return;
322
365
  }
323
- else writeFileSync(configPath, JSON.stringify(daemonConfig, null, 2));
324
- exec("systemctl reload docker || systemctl restart docker");
366
+ else io.writeFile(configPath, JSON.stringify(daemonConfig, null, 2));
367
+ io.exec("systemctl reload docker || systemctl restart docker");
325
368
  success("Docker configured with production settings");
326
369
  }
327
370
  /**
328
371
  * Setup Docker system prune cron job
329
372
  */
330
- function setupDockerPrune() {
373
+ function setupDockerPrune(io = nodeIo) {
331
374
  step("Setup Docker cleanup cron");
332
- writeFileSync("/etc/cron.d/trafic-docker-prune", `# Trafic: Clean up Docker resources weekly
375
+ io.writeFile("/etc/cron.d/trafic-docker-prune", `# Trafic: Clean up Docker resources weekly
333
376
  0 3 * * 0 root docker system prune -af --volumes 2>&1 | logger -t docker-prune
334
377
  `);
335
- exec("chmod 644 /etc/cron.d/trafic-docker-prune");
378
+ io.exec("chmod 644 /etc/cron.d/trafic-docker-prune");
336
379
  success("Docker prune scheduled weekly at 3am on Sundays");
337
380
  }
338
381
  //#endregion
@@ -340,39 +383,39 @@ function setupDockerPrune() {
340
383
  /**
341
384
  * Install system dependencies required by Trafic
342
385
  */
343
- function installSystemDeps() {
386
+ function installSystemDeps(io = nodeIo) {
344
387
  step("Install system dependencies");
345
- exec("apt-get update -qq", { silent: true });
346
- exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y jq curl rsync gnupg ca-certificates", { silent: true });
388
+ io.exec("apt-get update -qq", { silent: true });
389
+ io.exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y jq curl rsync gnupg ca-certificates", { silent: true });
347
390
  success("System dependencies installed: jq, curl, rsync, gnupg, ca-certificates");
348
391
  }
349
392
  /**
350
393
  * Add the official DDEV apt repository and GPG key.
351
394
  * Idempotent: safe to call even if the repository is already configured.
352
395
  */
353
- function addDdevAptRepo() {
354
- exec("install -m 0755 -d /etc/apt/keyrings", { silent: true });
355
- exec("curl -fsSL https://pkg.ddev.com/apt/gpg.key | gpg --dearmor | tee /etc/apt/keyrings/ddev.gpg > /dev/null", { silent: true });
356
- exec("chmod a+r /etc/apt/keyrings/ddev.gpg", { silent: true });
357
- exec("echo \"deb [signed-by=/etc/apt/keyrings/ddev.gpg] https://pkg.ddev.com/apt/ * *\" | tee /etc/apt/sources.list.d/ddev.list > /dev/null", { silent: true });
396
+ function addDdevAptRepo(io = nodeIo) {
397
+ io.exec("install -m 0755 -d /etc/apt/keyrings", { silent: true });
398
+ io.exec("curl -fsSL https://pkg.ddev.com/apt/gpg.key | gpg --dearmor | tee /etc/apt/keyrings/ddev.gpg > /dev/null", { silent: true });
399
+ io.exec("chmod a+r /etc/apt/keyrings/ddev.gpg", { silent: true });
400
+ io.exec("echo \"deb [signed-by=/etc/apt/keyrings/ddev.gpg] https://pkg.ddev.com/apt/ * *\" | tee /etc/apt/sources.list.d/ddev.list > /dev/null", { silent: true });
358
401
  }
359
402
  /**
360
403
  * Install DDEV
361
404
  */
362
- function installDdev() {
405
+ function installDdev(io = nodeIo) {
363
406
  step("Install DDEV");
364
- if (commandExists("ddev")) {
365
- info(`DDEV already installed: ${exec("ddev --version 2>/dev/null | head -1", { silent: true })?.trim() ?? "unknown version"}`);
407
+ if (io.commandExists("ddev")) {
408
+ info(`DDEV already installed: ${io.exec("ddev --version 2>/dev/null | head -1", { silent: true })?.trim() ?? "unknown version"}`);
366
409
  return;
367
410
  }
368
411
  info("Adding DDEV apt repository...");
369
- addDdevAptRepo();
412
+ addDdevAptRepo(io);
370
413
  info("Installing DDEV...");
371
- exec("apt-get update -qq", { silent: true });
372
- exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ddev", { silent: true });
373
- exec("HOME=/home/ddev mkcert -install", { silent: true });
374
- exec("chown -R ddev:ddev /home/ddev/.local/share/mkcert", { silent: true });
375
- success(`DDEV installed: ${exec("ddev --version 2>/dev/null | head -1", { silent: true })?.trim() ?? "unknown version"}`);
414
+ io.exec("apt-get update -qq", { silent: true });
415
+ io.exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ddev", { silent: true });
416
+ io.exec("HOME=/home/ddev mkcert -install", { silent: true });
417
+ io.exec("chown -R ddev:ddev /home/ddev/.local/share/mkcert", { silent: true });
418
+ success(`DDEV installed: ${io.exec("ddev --version 2>/dev/null | head -1", { silent: true })?.trim() ?? "unknown version"}`);
376
419
  }
377
420
  /**
378
421
  * Configure DDEV global settings for production
@@ -385,10 +428,10 @@ function installDdev() {
385
428
  *
386
429
  * With proper DNS, DDEV won't need sudo for hostname management.
387
430
  */
388
- function configureDdev(tld, email) {
431
+ function configureDdev(tld, email, io = nodeIo) {
389
432
  step("Configure DDEV");
390
433
  const ddevCmd = (cmd) => `su - ddev -c '${cmd}'`;
391
- exec(ddevCmd(`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`), { silent: true });
434
+ io.exec(ddevCmd(`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`), { silent: true });
392
435
  success(`DDEV configured with TLD: ${tld}`);
393
436
  info(`Ensure DNS is configured: *.${tld} → server IP`);
394
437
  if (email) success(`Let's Encrypt enabled with email: ${email}`);
@@ -400,19 +443,19 @@ function configureDdev(tld, email) {
400
443
  * Falls back to the default `bridge` network, then to 172.17.0.1.
401
444
  * `host.docker.internal` is not available on Linux without extra configuration.
402
445
  */
403
- function getDockerGatewayIp() {
404
- const ddevIp = exec("docker network inspect ddev_default --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}' 2>/dev/null", { silent: true })?.trim();
446
+ function getDockerGatewayIp(io = nodeIo) {
447
+ const ddevIp = io.exec("docker network inspect ddev_default --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}' 2>/dev/null", { silent: true })?.trim();
405
448
  if (ddevIp) return ddevIp;
406
- return exec("docker network inspect bridge --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}' 2>/dev/null", { silent: true })?.trim() || "172.17.0.1";
449
+ return io.exec("docker network inspect bridge --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}' 2>/dev/null", { silent: true })?.trim() || "172.17.0.1";
407
450
  }
408
451
  /**
409
452
  * Configure Traefik for forward auth
410
453
  */
411
- function configureTraefik() {
454
+ function configureTraefik(io = nodeIo) {
412
455
  step("Configure Traefik for forward auth");
413
- exec("mkdir -p /home/ddev/.ddev/traefik/custom-global-config", { silent: true });
414
- exec("chown -R ddev:ddev /home/ddev/.ddev", { silent: true });
415
- const gatewayIp = getDockerGatewayIp();
456
+ io.exec("mkdir -p /home/ddev/.ddev/traefik/custom-global-config", { silent: true });
457
+ io.exec("chown -R ddev:ddev /home/ddev/.ddev", { silent: true });
458
+ const gatewayIp = getDockerGatewayIp(io);
416
459
  const dynamicConfig = `# Trafic: Forward auth middleware definition
417
460
  http:
418
461
  middlewares:
@@ -436,11 +479,11 @@ http:
436
479
  servers:
437
480
  - url: "http://${gatewayIp}:9876"
438
481
  `;
439
- writeFileSync("/home/ddev/.ddev/traefik/trafic.yaml", dynamicConfig);
440
- exec("chown ddev:ddev /home/ddev/.ddev/traefik/trafic.yaml", { silent: true });
441
- writeFileSync("/home/ddev/.ddev/traefik/custom-global-config/trafic.yaml", dynamicConfig);
442
- exec("chown ddev:ddev /home/ddev/.ddev/traefik/custom-global-config/trafic.yaml", { silent: true });
443
- writeFileSync("/home/ddev/.ddev/traefik/static_config.trafic.yaml", `entryPoints:
482
+ io.writeFile("/home/ddev/.ddev/traefik/trafic.yaml", dynamicConfig);
483
+ io.exec("chown ddev:ddev /home/ddev/.ddev/traefik/trafic.yaml", { silent: true });
484
+ io.writeFile("/home/ddev/.ddev/traefik/custom-global-config/trafic.yaml", dynamicConfig);
485
+ io.exec("chown ddev:ddev /home/ddev/.ddev/traefik/custom-global-config/trafic.yaml", { silent: true });
486
+ io.writeFile("/home/ddev/.ddev/traefik/static_config.trafic.yaml", `entryPoints:
444
487
  http-80:
445
488
  http:
446
489
  middlewares:
@@ -452,7 +495,7 @@ http:
452
495
  - trafic-auth@file
453
496
  - trafic-errors@file
454
497
  `);
455
- exec("chown ddev:ddev /home/ddev/.ddev/traefik/static_config.trafic.yaml", { silent: true });
498
+ io.exec("chown ddev:ddev /home/ddev/.ddev/traefik/static_config.trafic.yaml", { silent: true });
456
499
  success("Traefik configured with Trafic middleware");
457
500
  }
458
501
  //#endregion
@@ -468,13 +511,13 @@ var NODESOURCE_KEYRING = "/etc/apt/keyrings/nodesource.gpg";
468
511
  * nodejs.org points at NodeSource for apt installs. The Ubuntu 24.04
469
512
  * `nodejs` package is 18.x, far below what the agent needs.
470
513
  */
471
- function addNodeSourceAptRepo() {
472
- exec("install -m 0755 -d /etc/apt/keyrings", { silent: true });
473
- exec("curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o /tmp/trafic-nodesource.key", { silent: true });
474
- exec("gpg --batch --yes --dearmor -o /tmp/trafic-nodesource.gpg /tmp/trafic-nodesource.key", { silent: true });
475
- exec(`install -m 0644 /tmp/trafic-nodesource.gpg ${NODESOURCE_KEYRING}`, { silent: true });
476
- exec("rm -f /tmp/trafic-nodesource.key /tmp/trafic-nodesource.gpg", { silent: true });
477
- exec(`echo "deb [signed-by=${NODESOURCE_KEYRING}] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list > /dev/null`, { silent: true });
514
+ function addNodeSourceAptRepo(io = nodeIo) {
515
+ io.exec("install -m 0755 -d /etc/apt/keyrings", { silent: true });
516
+ io.exec("curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o /tmp/trafic-nodesource.key", { silent: true });
517
+ io.exec("gpg --batch --yes --dearmor -o /tmp/trafic-nodesource.gpg /tmp/trafic-nodesource.key", { silent: true });
518
+ io.exec(`install -m 0644 /tmp/trafic-nodesource.gpg ${NODESOURCE_KEYRING}`, { silent: true });
519
+ io.exec("rm -f /tmp/trafic-nodesource.key /tmp/trafic-nodesource.gpg", { silent: true });
520
+ io.exec(`echo "deb [signed-by=${NODESOURCE_KEYRING}] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list > /dev/null`, { silent: true });
478
521
  }
479
522
  /**
480
523
  * Install Node.js from the NodeSource apt repository.
@@ -483,37 +526,38 @@ function addNodeSourceAptRepo() {
483
526
  * trafic-agent` resolves for the systemd unit — and unattended-upgrades
484
527
  * patches them, which a version manager install never gets.
485
528
  */
486
- function installNode() {
529
+ function installNode(io = nodeIo) {
487
530
  step("Install Node.js");
488
- if (commandExists("node")) {
489
- info(`Node.js already installed: ${exec("node --version", { silent: true })?.trim()}`);
531
+ if (io.commandExists("node")) {
532
+ info(`Node.js already installed: ${io.exec("node --version", { silent: true })?.trim()}`);
490
533
  return;
491
534
  }
492
535
  info("Adding NodeSource apt repository...");
493
- addNodeSourceAptRepo();
536
+ addNodeSourceAptRepo(io);
494
537
  info("Installing Node.js...");
495
- exec("apt-get update -qq", { silent: true });
496
- exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y nodejs", { silent: true });
538
+ io.exec("apt-get update -qq", { silent: true });
539
+ io.exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y nodejs", { silent: true });
497
540
  success(`Node.js ${NODE_MAJOR} installed from apt`);
498
541
  }
499
542
  /**
500
543
  * Install the Trafic agent
501
544
  */
502
- function installAgent() {
545
+ function installAgent(io = nodeIo) {
503
546
  step("Install Trafic agent");
504
- exec("npm install -g @studiometa/trafic-agent");
547
+ io.exec("npm install -g @studiometa/trafic-agent");
505
548
  success("Trafic agent installed");
506
549
  }
507
550
  /**
508
551
  * Create the agent configuration
509
552
  */
510
- function createAgentConfig(tld, _email) {
553
+ function createAgentConfig(options, io = nodeIo) {
554
+ const { tld, trustedProxyHops = 1 } = options;
511
555
  step("Create agent configuration");
512
- exec("mkdir -p /etc/trafic");
513
- exec("mkdir -p /var/lib/trafic");
514
- exec("chown ddev:ddev /var/lib/trafic");
556
+ io.exec("mkdir -p /etc/trafic");
557
+ io.exec("mkdir -p /var/lib/trafic");
558
+ io.exec("chown ddev:ddev /var/lib/trafic");
515
559
  const configPath = "/etc/trafic/config.toml";
516
- if (existsSync(configPath)) {
560
+ if (io.fileExists(configPath)) {
517
561
  info("Configuration already exists at /etc/trafic/config.toml — skipping");
518
562
  info("Edit this file to configure authentication");
519
563
  return;
@@ -543,17 +587,24 @@ allowed_ips = []
543
587
  tokens = []
544
588
  basic_auth = []
545
589
  rules = []
590
+
591
+ # Proxies in front of the agent, counted so the client address can be read
592
+ # from X-Forwarded-For. 1 is ddev-router/Traefik alone. Raise it by one for
593
+ # every extra proxy — a CDN or load balancer ahead of Traefik makes it 2.
594
+ # Too low and allowed_ips matches a proxy instead of the client; too high and
595
+ # it can be spoofed by a client-supplied header.
596
+ trusted_proxy_hops = ${trustedProxyHops}
546
597
  `;
547
- writeFileSync(configPath, config);
548
- exec("chmod 640 /etc/trafic/config.toml");
549
- exec("chown root:ddev /etc/trafic/config.toml");
598
+ io.writeFile(configPath, config);
599
+ io.exec("chmod 640 /etc/trafic/config.toml");
600
+ io.exec("chown root:ddev /etc/trafic/config.toml");
550
601
  success("Configuration created at /etc/trafic/config.toml");
551
602
  info("Edit this file to configure authentication");
552
603
  }
553
604
  /**
554
605
  * Create systemd service for the agent
555
606
  */
556
- function createSystemdService() {
607
+ function createSystemdService(io = nodeIo) {
557
608
  step("Create systemd service");
558
609
  const serviceContent = `[Unit]
559
610
  Description=Trafic Agent - DDEV preview environment manager
@@ -564,7 +615,7 @@ Requires=docker.service
564
615
  Type=simple
565
616
  User=ddev
566
617
  Group=ddev
567
- ExecStart=${exec("which trafic-agent", { silent: true })?.trim() || "/usr/bin/trafic-agent"} start --config /etc/trafic/config.toml
618
+ ExecStart=${io.exec("which trafic-agent", { silent: true })?.trim() || "/usr/bin/trafic-agent"} start --config /etc/trafic/config.toml
568
619
  Restart=always
569
620
  RestartSec=5
570
621
  StandardOutput=journal
@@ -588,21 +639,35 @@ PrivateTmp=true
588
639
  [Install]
589
640
  WantedBy=multi-user.target
590
641
  `;
591
- writeFileSync("/etc/systemd/system/trafic-agent.service", serviceContent);
592
- exec("chmod 644 /etc/systemd/system/trafic-agent.service");
593
- exec("systemctl daemon-reload");
594
- exec("systemctl enable trafic-agent");
595
- exec("systemctl start trafic-agent");
642
+ io.writeFile("/etc/systemd/system/trafic-agent.service", serviceContent);
643
+ io.exec("chmod 644 /etc/systemd/system/trafic-agent.service");
644
+ io.exec("systemctl daemon-reload");
645
+ io.exec("systemctl enable trafic-agent");
646
+ io.exec("systemctl start trafic-agent");
596
647
  success("Trafic agent service started");
597
648
  info("View logs: journalctl -u trafic-agent -f");
598
649
  }
599
650
  //#endregion
600
651
  //#region src/setup/hardening.ts
601
652
  /**
653
+ * Resolve the users listed in AllowUsers.
654
+ *
655
+ * The user who invoked sudo is always included. Without them, the
656
+ * AllowUsers directive locks them out on their next connection, and
657
+ * reloading sshd keeps the current session alive so nothing looks wrong
658
+ * until it is too late. root needs no entry — it is always listed.
659
+ */
660
+ function resolveAllowedUsers(allowedUsers, sudoUser) {
661
+ const users = allowedUsers.map((user) => user.trim()).filter(Boolean);
662
+ if (sudoUser && sudoUser !== "root" && !users.includes(sudoUser)) users.push(sudoUser);
663
+ return users;
664
+ }
665
+ /**
602
666
  * Configure SSH hardening
603
667
  */
604
- function hardenSsh(allowedUsers = ["ddev"]) {
668
+ function hardenSsh(allowedUsers = ["ddev"], io = nodeIo) {
605
669
  step("Harden SSH configuration");
670
+ const users = resolveAllowedUsers(allowedUsers, io.env.SUDO_USER);
606
671
  const sshConfig = `# Trafic SSH hardening
607
672
  # Generated by trafic setup
608
673
 
@@ -621,7 +686,7 @@ ClientAliveInterval 300
621
686
  ClientAliveCountMax 2
622
687
 
623
688
  # Allowed users (root kept for emergency access)
624
- AllowUsers root ${allowedUsers.join(" ")}
689
+ AllowUsers root ${users.join(" ")}
625
690
 
626
691
  # Disable X11 forwarding
627
692
  X11Forwarding no
@@ -634,40 +699,40 @@ KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256
634
699
  Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
635
700
  MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
636
701
  `;
637
- writeFileSync("/etc/ssh/sshd_config.d/trafic.conf", sshConfig);
638
- exec("chmod 644 /etc/ssh/sshd_config.d/trafic.conf");
639
- if (exec("sshd -t 2>&1 || echo 'error'", { silent: true })?.includes("error")) {
702
+ io.writeFile("/etc/ssh/sshd_config.d/trafic.conf", sshConfig);
703
+ io.exec("chmod 644 /etc/ssh/sshd_config.d/trafic.conf");
704
+ if (io.exec("sshd -t 2>&1 || echo 'error'", { silent: true })?.includes("error")) {
640
705
  warn("SSH config test failed, reverting...");
641
- exec("rm /etc/ssh/sshd_config.d/trafic.conf");
706
+ io.exec("rm /etc/ssh/sshd_config.d/trafic.conf");
642
707
  return;
643
708
  }
644
- exec("systemctl reload ssh || systemctl reload sshd");
709
+ io.exec("systemctl reload ssh || systemctl reload sshd");
645
710
  success("SSH hardened: password auth disabled, root key-only login retained");
646
- info(`Allowed users: root, ${allowedUsers.join(", ")}`);
711
+ info(`Allowed users: root, ${users.join(", ")}`);
647
712
  }
648
713
  /**
649
714
  * Configure UFW firewall
650
715
  */
651
- function configureFirewall() {
716
+ function configureFirewall(io = nodeIo) {
652
717
  step("Configure firewall (UFW)");
653
- if (!commandExists("ufw")) exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ufw", { silent: true });
654
- exec("ufw --force reset", { silent: true });
655
- exec("ufw default deny incoming");
656
- exec("ufw default allow outgoing");
657
- exec("ufw allow 22/tcp comment 'SSH'");
658
- exec("ufw allow 80/tcp comment 'HTTP'");
659
- exec("ufw allow 443/tcp comment 'HTTPS'");
660
- exec("ufw allow from 172.16.0.0/12 to any port 9876 comment 'trafic-agent from Docker'");
661
- exec("ufw --force enable");
718
+ if (!io.commandExists("ufw")) io.exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ufw", { silent: true });
719
+ io.exec("ufw --force reset", { silent: true });
720
+ io.exec("ufw default deny incoming");
721
+ io.exec("ufw default allow outgoing");
722
+ io.exec("ufw allow 22/tcp comment 'SSH'");
723
+ io.exec("ufw allow 80/tcp comment 'HTTP'");
724
+ io.exec("ufw allow 443/tcp comment 'HTTPS'");
725
+ io.exec("ufw allow from 172.16.0.0/12 to any port 9876 comment 'trafic-agent from Docker'");
726
+ io.exec("ufw --force enable");
662
727
  success("Firewall enabled: SSH (22), HTTP (80), HTTPS (443), trafic-agent (9876 from Docker) allowed");
663
728
  }
664
729
  /**
665
730
  * Configure automatic security updates
666
731
  */
667
- function configureUnattendedUpgrades() {
732
+ function configureUnattendedUpgrades(io = nodeIo) {
668
733
  step("Configure automatic security updates");
669
- exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y unattended-upgrades apt-listchanges", { silent: true });
670
- writeFileSync("/etc/apt/apt.conf.d/50unattended-upgrades", `// Trafic: Automatic security updates
734
+ io.exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y unattended-upgrades apt-listchanges", { silent: true });
735
+ io.writeFile("/etc/apt/apt.conf.d/50unattended-upgrades", `// Trafic: Automatic security updates
671
736
  Unattended-Upgrade::Allowed-Origins {
672
737
  "\${distro_id}:\${distro_codename}";
673
738
  "\${distro_id}:\${distro_codename}-security";
@@ -684,22 +749,22 @@ Unattended-Upgrade::Remove-Unused-Dependencies "true";
684
749
  // Email notifications (optional)
685
750
  // Unattended-Upgrade::Mail "root";
686
751
  `);
687
- writeFileSync("/etc/apt/apt.conf.d/20auto-upgrades", `APT::Periodic::Update-Package-Lists "1";
752
+ io.writeFile("/etc/apt/apt.conf.d/20auto-upgrades", `APT::Periodic::Update-Package-Lists "1";
688
753
  APT::Periodic::Download-Upgradeable-Packages "1";
689
754
  APT::Periodic::AutocleanInterval "7";
690
755
  APT::Periodic::Unattended-Upgrade "1";
691
756
  `);
692
- exec("systemctl enable unattended-upgrades");
693
- exec("systemctl start unattended-upgrades");
757
+ io.exec("systemctl enable unattended-upgrades");
758
+ io.exec("systemctl start unattended-upgrades");
694
759
  success("Automatic security updates enabled");
695
760
  }
696
761
  /**
697
762
  * Configure Fail2ban
698
763
  */
699
- function configureFail2ban() {
764
+ function configureFail2ban(io = nodeIo) {
700
765
  step("Configure Fail2ban");
701
- if (!commandExists("fail2ban-client")) exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y fail2ban", { silent: true });
702
- writeFileSync("/etc/fail2ban/jail.local", `# Trafic: Fail2ban configuration
766
+ if (!io.commandExists("fail2ban-client")) io.exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y fail2ban", { silent: true });
767
+ io.writeFile("/etc/fail2ban/jail.local", `# Trafic: Fail2ban configuration
703
768
  [DEFAULT]
704
769
  bantime = 3600
705
770
  findtime = 600
@@ -714,55 +779,55 @@ logpath = /var/log/auth.log
714
779
  maxretry = 3
715
780
  bantime = 7200
716
781
  `);
717
- exec("chmod 644 /etc/fail2ban/jail.local");
718
- exec("systemctl enable fail2ban");
719
- exec("systemctl restart fail2ban");
782
+ io.exec("chmod 644 /etc/fail2ban/jail.local");
783
+ io.exec("systemctl enable fail2ban");
784
+ io.exec("systemctl restart fail2ban");
720
785
  success("Fail2ban enabled: SSH brute force protection active");
721
786
  }
722
787
  /**
723
788
  * Configure system limits
724
789
  */
725
- function configureSystemLimits() {
790
+ function configureSystemLimits(io = nodeIo) {
726
791
  step("Configure system limits");
727
- writeFileSync("/etc/security/limits.d/trafic.conf", `# Trafic: System limits for ddev user
792
+ io.writeFile("/etc/security/limits.d/trafic.conf", `# Trafic: System limits for ddev user
728
793
  ddev soft nofile 65536
729
794
  ddev hard nofile 65536
730
795
  ddev soft nproc 4096
731
796
  ddev hard nproc 4096
732
797
  `);
733
- exec("chmod 644 /etc/security/limits.d/trafic.conf");
798
+ io.exec("chmod 644 /etc/security/limits.d/trafic.conf");
734
799
  success("System limits configured for ddev user");
735
800
  }
736
801
  /**
737
802
  * Set proper file permissions
738
803
  */
739
- function configureFilePermissions() {
804
+ function configureFilePermissions(io = nodeIo) {
740
805
  step("Configure file permissions");
741
- exec("mkdir -p /etc/trafic");
742
- exec("mkdir -p /var/lib/trafic");
743
- exec("mkdir -p /var/backups/trafic");
744
- exec("chmod 755 /etc/trafic");
745
- exec("chmod 700 /var/lib/trafic");
746
- exec("chmod 700 /var/backups/trafic");
747
- exec("chown root:ddev /etc/trafic");
748
- exec("chown ddev:ddev /var/lib/trafic");
749
- exec("chown ddev:ddev /var/backups/trafic");
750
- if (existsSync("/etc/trafic/config.toml")) {
751
- exec("chmod 640 /etc/trafic/config.toml");
752
- exec("chown root:ddev /etc/trafic/config.toml");
806
+ io.exec("mkdir -p /etc/trafic");
807
+ io.exec("mkdir -p /var/lib/trafic");
808
+ io.exec("mkdir -p /var/backups/trafic");
809
+ io.exec("chmod 755 /etc/trafic");
810
+ io.exec("chmod 700 /var/lib/trafic");
811
+ io.exec("chmod 700 /var/backups/trafic");
812
+ io.exec("chown root:ddev /etc/trafic");
813
+ io.exec("chown ddev:ddev /var/lib/trafic");
814
+ io.exec("chown ddev:ddev /var/backups/trafic");
815
+ if (io.fileExists("/etc/trafic/config.toml")) {
816
+ io.exec("chmod 640 /etc/trafic/config.toml");
817
+ io.exec("chown root:ddev /etc/trafic/config.toml");
753
818
  }
754
819
  success("File permissions configured");
755
820
  }
756
821
  /**
757
822
  * Run all hardening steps
758
823
  */
759
- function hardenServer(sshUsers = ["ddev"]) {
760
- hardenSsh(sshUsers);
761
- configureFirewall();
762
- configureUnattendedUpgrades();
763
- configureFail2ban();
764
- configureSystemLimits();
765
- configureFilePermissions();
824
+ function hardenServer(sshUsers = ["ddev"], io = nodeIo) {
825
+ hardenSsh(sshUsers, io);
826
+ configureFirewall(io);
827
+ configureUnattendedUpgrades(io);
828
+ configureFail2ban(io);
829
+ configureSystemLimits(io);
830
+ configureFilePermissions(io);
766
831
  }
767
832
  //#endregion
768
833
  //#region src/setup/audit.ts
@@ -1309,7 +1374,10 @@ async function setup(options) {
1309
1374
  }
1310
1375
  installNode();
1311
1376
  installAgent();
1312
- createAgentConfig(options.tld, options.email);
1377
+ createAgentConfig({
1378
+ tld: options.tld,
1379
+ trustedProxyHops: options.trustedProxyHops
1380
+ });
1313
1381
  createSystemdService();
1314
1382
  if (!options.noHardening) hardenServer(options.sshUsers ?? ["ddev"]);
1315
1383
  else {
@@ -1340,4 +1408,4 @@ async function audit() {
1340
1408
  //#endregion
1341
1409
  export { validateConfig as _, printAuditResults as a, info as c, success as d, warn as f, parseDuration as g, loadConfig as h, runPendingMigrations as i, isRoot as l, stopIdleProjects as m, setup as n, runAudit as o, startIdleScheduler as p, listMigrations as r, exec as s, audit as t, step as u };
1342
1410
 
1343
- //# sourceMappingURL=setup-D61uhYiJ.js.map
1411
+ //# sourceMappingURL=setup-BcY2feHr.js.map