@studiometa/trafic-agent 0.1.24 → 0.1.26

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.
@@ -237,39 +237,68 @@ function commandExists(command) {
237
237
  function isRoot() {
238
238
  return process.getuid?.() === 0;
239
239
  }
240
+ /**
241
+ * Write a file (or show in dry-run mode)
242
+ */
243
+ function writeFile(path, content) {
244
+ if (dryRun) {
245
+ console.log(` \x1b[90mWrite ${path}:\x1b[0m`);
246
+ const lines = content.split("\n").slice(0, 5);
247
+ for (const line of lines) console.log(` \x1b[90m ${line}\x1b[0m`);
248
+ if (content.split("\n").length > 5) console.log(` \x1b[90m ...\x1b[0m`);
249
+ return;
250
+ }
251
+ writeFileSync(path, content);
252
+ }
253
+ //#endregion
254
+ //#region src/setup/io.ts
255
+ /**
256
+ * The real implementation, used when no io is injected.
257
+ *
258
+ * `writeFile` comes from steps.js rather than `node:fs` so writes are
259
+ * skipped in dry-run mode. Reads stay direct: they change nothing.
260
+ */
261
+ var nodeIo = {
262
+ exec,
263
+ commandExists,
264
+ writeFile,
265
+ fileExists: existsSync,
266
+ readFile: (path) => readFileSync(path, "utf-8"),
267
+ env: process.env
268
+ };
240
269
  //#endregion
241
270
  //#region src/setup/user.ts
242
271
  /**
243
272
  * Write /etc/sudoers.d/trafic-ddev allowing the ddev user to run
244
273
  * ddev-hostname without a password. Idempotent — safe to call multiple times.
245
274
  */
246
- function configureDdevSudoers() {
247
- writeFileSync("/etc/sudoers.d/trafic-ddev", `# Trafic: allow ddev user to run ddev-hostname without a password.
275
+ function configureDdevSudoers(io = nodeIo) {
276
+ io.writeFile("/etc/sudoers.d/trafic-ddev", `# Trafic: allow ddev user to run ddev-hostname without a password.
248
277
  # ddev-hostname manages /etc/hosts and always calls sudo internally.
249
278
  ddev ALL=(ALL) NOPASSWD: /usr/bin/ddev-hostname
250
279
  `);
251
- exec("chmod 440 /etc/sudoers.d/trafic-ddev", { silent: true });
280
+ io.exec("chmod 440 /etc/sudoers.d/trafic-ddev", { silent: true });
252
281
  success("Configured passwordless sudo for ddev-hostname");
253
282
  }
254
283
  /**
255
284
  * Create the ddev user with proper permissions
256
285
  */
257
- function createDdevUser() {
286
+ function createDdevUser(io = nodeIo) {
258
287
  step("Create ddev user");
259
- if (execSilent$1("id -u ddev 2>/dev/null")) info("User 'ddev' already exists");
288
+ if (io.exec("id -u ddev 2>/dev/null", { silent: true })) info("User 'ddev' already exists");
260
289
  else {
261
- exec("useradd -m -s /bin/bash ddev");
290
+ io.exec("useradd -m -s /bin/bash ddev");
262
291
  success("Created user 'ddev'");
263
292
  }
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");
293
+ io.exec("usermod -aG docker ddev 2>/dev/null || true", { silent: true });
294
+ io.exec("mkdir -p /home/ddev/www");
295
+ io.exec("chown ddev:ddev /home/ddev/www");
296
+ io.exec("chmod 750 /home/ddev/www");
268
297
  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();
298
+ io.exec("mkdir -p /home/ddev/.ssh");
299
+ io.exec("chmod 700 /home/ddev/.ssh");
300
+ io.exec("chown ddev:ddev /home/ddev/.ssh");
301
+ configureDdevSudoers(io);
273
302
  success("User ddev configured");
274
303
  }
275
304
  //#endregion
@@ -277,26 +306,26 @@ function createDdevUser() {
277
306
  /**
278
307
  * Install Docker using the official script
279
308
  */
280
- function installDocker() {
309
+ function installDocker(io = nodeIo) {
281
310
  step("Install Docker");
282
- if (commandExists("docker")) {
283
- info(`Docker already installed: ${exec("docker --version", { silent: true })?.trim() ?? "unknown version"}`);
311
+ if (io.commandExists("docker")) {
312
+ info(`Docker already installed: ${io.exec("docker --version", { silent: true })?.trim() ?? "unknown version"}`);
284
313
  return;
285
314
  }
286
315
  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 });
316
+ io.exec("curl -fsSL https://get.docker.com -o /tmp/get-docker.sh");
317
+ io.exec("sh /tmp/get-docker.sh", { silent: true });
318
+ io.exec("rm /tmp/get-docker.sh");
319
+ io.exec("systemctl enable docker", { silent: true });
320
+ io.exec("systemctl start docker", { silent: true });
292
321
  success("Docker installed and started");
293
- exec("usermod -aG docker ddev", { silent: true });
322
+ io.exec("usermod -aG docker ddev", { silent: true });
294
323
  success("Added ddev user to docker group");
295
324
  }
296
325
  /**
297
326
  * Configure Docker for production use
298
327
  */
299
- function configureDocker() {
328
+ function configureDocker(io = nodeIo) {
300
329
  step("Configure Docker");
301
330
  const daemonConfig = {
302
331
  "log-driver": "json-file",
@@ -307,32 +336,32 @@ function configureDocker() {
307
336
  "storage-driver": "overlay2",
308
337
  "live-restore": true
309
338
  };
310
- exec("mkdir -p /etc/docker");
339
+ io.exec("mkdir -p /etc/docker");
311
340
  const configPath = "/etc/docker/daemon.json";
312
- if (existsSync(configPath)) try {
341
+ if (io.fileExists(configPath)) try {
313
342
  const merged = {
314
- ...JSON.parse(readFileSync(configPath, "utf-8")),
343
+ ...JSON.parse(io.readFile(configPath)),
315
344
  ...daemonConfig
316
345
  };
317
- writeFileSync(configPath, JSON.stringify(merged, null, 2));
346
+ io.writeFile(configPath, JSON.stringify(merged, null, 2));
318
347
  info("Merged with existing Docker config");
319
348
  } catch {
320
349
  warn("Could not parse existing Docker config, skipping");
321
350
  return;
322
351
  }
323
- else writeFileSync(configPath, JSON.stringify(daemonConfig, null, 2));
324
- exec("systemctl reload docker || systemctl restart docker");
352
+ else io.writeFile(configPath, JSON.stringify(daemonConfig, null, 2));
353
+ io.exec("systemctl reload docker || systemctl restart docker");
325
354
  success("Docker configured with production settings");
326
355
  }
327
356
  /**
328
357
  * Setup Docker system prune cron job
329
358
  */
330
- function setupDockerPrune() {
359
+ function setupDockerPrune(io = nodeIo) {
331
360
  step("Setup Docker cleanup cron");
332
- writeFileSync("/etc/cron.d/trafic-docker-prune", `# Trafic: Clean up Docker resources weekly
361
+ io.writeFile("/etc/cron.d/trafic-docker-prune", `# Trafic: Clean up Docker resources weekly
333
362
  0 3 * * 0 root docker system prune -af --volumes 2>&1 | logger -t docker-prune
334
363
  `);
335
- exec("chmod 644 /etc/cron.d/trafic-docker-prune");
364
+ io.exec("chmod 644 /etc/cron.d/trafic-docker-prune");
336
365
  success("Docker prune scheduled weekly at 3am on Sundays");
337
366
  }
338
367
  //#endregion
@@ -340,39 +369,39 @@ function setupDockerPrune() {
340
369
  /**
341
370
  * Install system dependencies required by Trafic
342
371
  */
343
- function installSystemDeps() {
372
+ function installSystemDeps(io = nodeIo) {
344
373
  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", { silent: true });
347
- success("System dependencies installed: jq, curl, rsync");
374
+ io.exec("apt-get update -qq", { silent: true });
375
+ io.exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y jq curl rsync gnupg ca-certificates", { silent: true });
376
+ success("System dependencies installed: jq, curl, rsync, gnupg, ca-certificates");
348
377
  }
349
378
  /**
350
379
  * Add the official DDEV apt repository and GPG key.
351
380
  * Idempotent: safe to call even if the repository is already configured.
352
381
  */
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 });
382
+ function addDdevAptRepo(io = nodeIo) {
383
+ io.exec("install -m 0755 -d /etc/apt/keyrings", { silent: true });
384
+ io.exec("curl -fsSL https://pkg.ddev.com/apt/gpg.key | gpg --dearmor | tee /etc/apt/keyrings/ddev.gpg > /dev/null", { silent: true });
385
+ io.exec("chmod a+r /etc/apt/keyrings/ddev.gpg", { silent: true });
386
+ 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
387
  }
359
388
  /**
360
389
  * Install DDEV
361
390
  */
362
- function installDdev() {
391
+ function installDdev(io = nodeIo) {
363
392
  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"}`);
393
+ if (io.commandExists("ddev")) {
394
+ info(`DDEV already installed: ${io.exec("ddev --version 2>/dev/null | head -1", { silent: true })?.trim() ?? "unknown version"}`);
366
395
  return;
367
396
  }
368
397
  info("Adding DDEV apt repository...");
369
- addDdevAptRepo();
398
+ addDdevAptRepo(io);
370
399
  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"}`);
400
+ io.exec("apt-get update -qq", { silent: true });
401
+ io.exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ddev", { silent: true });
402
+ io.exec("HOME=/home/ddev mkcert -install", { silent: true });
403
+ io.exec("chown -R ddev:ddev /home/ddev/.local/share/mkcert", { silent: true });
404
+ success(`DDEV installed: ${io.exec("ddev --version 2>/dev/null | head -1", { silent: true })?.trim() ?? "unknown version"}`);
376
405
  }
377
406
  /**
378
407
  * Configure DDEV global settings for production
@@ -385,10 +414,10 @@ function installDdev() {
385
414
  *
386
415
  * With proper DNS, DDEV won't need sudo for hostname management.
387
416
  */
388
- function configureDdev(tld, email) {
417
+ function configureDdev(tld, email, io = nodeIo) {
389
418
  step("Configure DDEV");
390
419
  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 });
420
+ 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
421
  success(`DDEV configured with TLD: ${tld}`);
393
422
  info(`Ensure DNS is configured: *.${tld} → server IP`);
394
423
  if (email) success(`Let's Encrypt enabled with email: ${email}`);
@@ -400,19 +429,19 @@ function configureDdev(tld, email) {
400
429
  * Falls back to the default `bridge` network, then to 172.17.0.1.
401
430
  * `host.docker.internal` is not available on Linux without extra configuration.
402
431
  */
403
- function getDockerGatewayIp() {
404
- const ddevIp = exec("docker network inspect ddev_default --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}' 2>/dev/null", { silent: true })?.trim();
432
+ function getDockerGatewayIp(io = nodeIo) {
433
+ const ddevIp = io.exec("docker network inspect ddev_default --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}' 2>/dev/null", { silent: true })?.trim();
405
434
  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";
435
+ return io.exec("docker network inspect bridge --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}' 2>/dev/null", { silent: true })?.trim() || "172.17.0.1";
407
436
  }
408
437
  /**
409
438
  * Configure Traefik for forward auth
410
439
  */
411
- function configureTraefik() {
440
+ function configureTraefik(io = nodeIo) {
412
441
  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();
442
+ io.exec("mkdir -p /home/ddev/.ddev/traefik/custom-global-config", { silent: true });
443
+ io.exec("chown -R ddev:ddev /home/ddev/.ddev", { silent: true });
444
+ const gatewayIp = getDockerGatewayIp(io);
416
445
  const dynamicConfig = `# Trafic: Forward auth middleware definition
417
446
  http:
418
447
  middlewares:
@@ -436,11 +465,11 @@ http:
436
465
  servers:
437
466
  - url: "http://${gatewayIp}:9876"
438
467
  `;
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:
468
+ io.writeFile("/home/ddev/.ddev/traefik/trafic.yaml", dynamicConfig);
469
+ io.exec("chown ddev:ddev /home/ddev/.ddev/traefik/trafic.yaml", { silent: true });
470
+ io.writeFile("/home/ddev/.ddev/traefik/custom-global-config/trafic.yaml", dynamicConfig);
471
+ io.exec("chown ddev:ddev /home/ddev/.ddev/traefik/custom-global-config/trafic.yaml", { silent: true });
472
+ io.writeFile("/home/ddev/.ddev/traefik/static_config.trafic.yaml", `entryPoints:
444
473
  http-80:
445
474
  http:
446
475
  middlewares:
@@ -452,52 +481,68 @@ http:
452
481
  - trafic-auth@file
453
482
  - trafic-errors@file
454
483
  `);
455
- exec("chown ddev:ddev /home/ddev/.ddev/traefik/static_config.trafic.yaml", { silent: true });
484
+ io.exec("chown ddev:ddev /home/ddev/.ddev/traefik/static_config.trafic.yaml", { silent: true });
456
485
  success("Traefik configured with Trafic middleware");
457
486
  }
458
487
  //#endregion
459
488
  //#region src/setup/agent.ts
489
+ /** Node.js major version required by the agent */
490
+ var NODE_MAJOR = 24;
491
+ /** NodeSource keyring path */
492
+ var NODESOURCE_KEYRING = "/etc/apt/keyrings/nodesource.gpg";
460
493
  /**
461
- * Install Node.js using fnm
494
+ * Add the NodeSource apt repository and GPG key.
495
+ * Idempotent: safe to call even if the repository is already configured.
496
+ *
497
+ * nodejs.org points at NodeSource for apt installs. The Ubuntu 24.04
498
+ * `nodejs` package is 18.x, far below what the agent needs.
499
+ */
500
+ function addNodeSourceAptRepo(io = nodeIo) {
501
+ io.exec("install -m 0755 -d /etc/apt/keyrings", { silent: true });
502
+ io.exec("curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o /tmp/trafic-nodesource.key", { silent: true });
503
+ io.exec("gpg --batch --yes --dearmor -o /tmp/trafic-nodesource.gpg /tmp/trafic-nodesource.key", { silent: true });
504
+ io.exec(`install -m 0644 /tmp/trafic-nodesource.gpg ${NODESOURCE_KEYRING}`, { silent: true });
505
+ io.exec("rm -f /tmp/trafic-nodesource.key /tmp/trafic-nodesource.gpg", { silent: true });
506
+ 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 });
507
+ }
508
+ /**
509
+ * Install Node.js from the NodeSource apt repository.
510
+ *
511
+ * apt keeps node and npm in /usr/bin — on root's PATH, so `which
512
+ * trafic-agent` resolves for the systemd unit — and unattended-upgrades
513
+ * patches them, which a version manager install never gets.
462
514
  */
463
- function installNode() {
515
+ function installNode(io = nodeIo) {
464
516
  step("Install Node.js");
465
- if (commandExists("node")) {
466
- info(`Node.js already installed: ${exec("node --version", { silent: true })?.trim()}`);
517
+ if (io.commandExists("node")) {
518
+ info(`Node.js already installed: ${io.exec("node --version", { silent: true })?.trim()}`);
467
519
  return;
468
520
  }
469
- exec("curl -fsSL https://fnm.vercel.app/install | bash -s -- --install-dir \"/opt/fnm\" --skip-shell");
470
- writeFileSync("/etc/profile.d/fnm.sh", `# Trafic: fnm (Node.js version manager)
471
- export FNM_DIR="/opt/fnm"
472
- export PATH="/opt/fnm:$PATH"
473
- eval "$(fnm env)"
474
- `);
475
- exec("chmod 644 /etc/profile.d/fnm.sh");
476
- exec("export FNM_DIR=/opt/fnm && /opt/fnm/fnm install 24");
477
- exec("export FNM_DIR=/opt/fnm && /opt/fnm/fnm default 24");
478
- exec("ln -sf /opt/fnm/aliases/default/bin/node /usr/local/bin/node || true");
479
- exec("ln -sf /opt/fnm/aliases/default/bin/npm /usr/local/bin/npm || true");
480
- exec("ln -sf /opt/fnm/aliases/default/bin/npx /usr/local/bin/npx || true");
481
- success("Node.js 24 installed via fnm");
521
+ info("Adding NodeSource apt repository...");
522
+ addNodeSourceAptRepo(io);
523
+ info("Installing Node.js...");
524
+ io.exec("apt-get update -qq", { silent: true });
525
+ io.exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y nodejs", { silent: true });
526
+ success(`Node.js ${NODE_MAJOR} installed from apt`);
482
527
  }
483
528
  /**
484
529
  * Install the Trafic agent
485
530
  */
486
- function installAgent() {
531
+ function installAgent(io = nodeIo) {
487
532
  step("Install Trafic agent");
488
- exec("npm install -g @studiometa/trafic-agent");
533
+ io.exec("npm install -g @studiometa/trafic-agent");
489
534
  success("Trafic agent installed");
490
535
  }
491
536
  /**
492
537
  * Create the agent configuration
493
538
  */
494
- function createAgentConfig(tld, _email) {
539
+ function createAgentConfig(tld, _email, io = nodeIo) {
495
540
  step("Create agent configuration");
496
- exec("mkdir -p /etc/trafic");
497
- exec("mkdir -p /var/lib/trafic");
498
- exec("chown ddev:ddev /var/lib/trafic");
541
+ io.exec("mkdir -p /etc/trafic");
542
+ io.exec("mkdir -p /var/lib/trafic");
543
+ io.exec("chown ddev:ddev /var/lib/trafic");
499
544
  const configPath = "/etc/trafic/config.toml";
500
- if (existsSync(configPath)) {
545
+ if (io.fileExists(configPath)) {
501
546
  info("Configuration already exists at /etc/trafic/config.toml — skipping");
502
547
  info("Edit this file to configure authentication");
503
548
  return;
@@ -528,16 +573,16 @@ tokens = []
528
573
  basic_auth = []
529
574
  rules = []
530
575
  `;
531
- writeFileSync(configPath, config);
532
- exec("chmod 640 /etc/trafic/config.toml");
533
- exec("chown root:ddev /etc/trafic/config.toml");
576
+ io.writeFile(configPath, config);
577
+ io.exec("chmod 640 /etc/trafic/config.toml");
578
+ io.exec("chown root:ddev /etc/trafic/config.toml");
534
579
  success("Configuration created at /etc/trafic/config.toml");
535
580
  info("Edit this file to configure authentication");
536
581
  }
537
582
  /**
538
583
  * Create systemd service for the agent
539
584
  */
540
- function createSystemdService() {
585
+ function createSystemdService(io = nodeIo) {
541
586
  step("Create systemd service");
542
587
  const serviceContent = `[Unit]
543
588
  Description=Trafic Agent - DDEV preview environment manager
@@ -548,7 +593,7 @@ Requires=docker.service
548
593
  Type=simple
549
594
  User=ddev
550
595
  Group=ddev
551
- ExecStart=${exec("which trafic-agent", { silent: true })?.trim() || "/usr/bin/trafic-agent"} start --config /etc/trafic/config.toml
596
+ ExecStart=${io.exec("which trafic-agent", { silent: true })?.trim() || "/usr/bin/trafic-agent"} start --config /etc/trafic/config.toml
552
597
  Restart=always
553
598
  RestartSec=5
554
599
  StandardOutput=journal
@@ -572,21 +617,35 @@ PrivateTmp=true
572
617
  [Install]
573
618
  WantedBy=multi-user.target
574
619
  `;
575
- writeFileSync("/etc/systemd/system/trafic-agent.service", serviceContent);
576
- exec("chmod 644 /etc/systemd/system/trafic-agent.service");
577
- exec("systemctl daemon-reload");
578
- exec("systemctl enable trafic-agent");
579
- exec("systemctl start trafic-agent");
620
+ io.writeFile("/etc/systemd/system/trafic-agent.service", serviceContent);
621
+ io.exec("chmod 644 /etc/systemd/system/trafic-agent.service");
622
+ io.exec("systemctl daemon-reload");
623
+ io.exec("systemctl enable trafic-agent");
624
+ io.exec("systemctl start trafic-agent");
580
625
  success("Trafic agent service started");
581
626
  info("View logs: journalctl -u trafic-agent -f");
582
627
  }
583
628
  //#endregion
584
629
  //#region src/setup/hardening.ts
585
630
  /**
631
+ * Resolve the users listed in AllowUsers.
632
+ *
633
+ * The user who invoked sudo is always included. Without them, the
634
+ * AllowUsers directive locks them out on their next connection, and
635
+ * reloading sshd keeps the current session alive so nothing looks wrong
636
+ * until it is too late. root needs no entry — it is always listed.
637
+ */
638
+ function resolveAllowedUsers(allowedUsers, sudoUser) {
639
+ const users = allowedUsers.map((user) => user.trim()).filter(Boolean);
640
+ if (sudoUser && sudoUser !== "root" && !users.includes(sudoUser)) users.push(sudoUser);
641
+ return users;
642
+ }
643
+ /**
586
644
  * Configure SSH hardening
587
645
  */
588
- function hardenSsh(allowedUsers = ["ddev"]) {
646
+ function hardenSsh(allowedUsers = ["ddev"], io = nodeIo) {
589
647
  step("Harden SSH configuration");
648
+ const users = resolveAllowedUsers(allowedUsers, io.env.SUDO_USER);
590
649
  const sshConfig = `# Trafic SSH hardening
591
650
  # Generated by trafic setup
592
651
 
@@ -605,7 +664,7 @@ ClientAliveInterval 300
605
664
  ClientAliveCountMax 2
606
665
 
607
666
  # Allowed users (root kept for emergency access)
608
- AllowUsers root ${allowedUsers.join(" ")}
667
+ AllowUsers root ${users.join(" ")}
609
668
 
610
669
  # Disable X11 forwarding
611
670
  X11Forwarding no
@@ -618,40 +677,40 @@ KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256
618
677
  Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
619
678
  MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
620
679
  `;
621
- writeFileSync("/etc/ssh/sshd_config.d/trafic.conf", sshConfig);
622
- exec("chmod 644 /etc/ssh/sshd_config.d/trafic.conf");
623
- if (exec("sshd -t 2>&1 || echo 'error'", { silent: true })?.includes("error")) {
680
+ io.writeFile("/etc/ssh/sshd_config.d/trafic.conf", sshConfig);
681
+ io.exec("chmod 644 /etc/ssh/sshd_config.d/trafic.conf");
682
+ if (io.exec("sshd -t 2>&1 || echo 'error'", { silent: true })?.includes("error")) {
624
683
  warn("SSH config test failed, reverting...");
625
- exec("rm /etc/ssh/sshd_config.d/trafic.conf");
684
+ io.exec("rm /etc/ssh/sshd_config.d/trafic.conf");
626
685
  return;
627
686
  }
628
- exec("systemctl reload ssh || systemctl reload sshd");
687
+ io.exec("systemctl reload ssh || systemctl reload sshd");
629
688
  success("SSH hardened: password auth disabled, root key-only login retained");
630
- info(`Allowed users: root, ${allowedUsers.join(", ")}`);
689
+ info(`Allowed users: root, ${users.join(", ")}`);
631
690
  }
632
691
  /**
633
692
  * Configure UFW firewall
634
693
  */
635
- function configureFirewall() {
694
+ function configureFirewall(io = nodeIo) {
636
695
  step("Configure firewall (UFW)");
637
- if (!commandExists("ufw")) exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ufw", { silent: true });
638
- exec("ufw --force reset", { silent: true });
639
- exec("ufw default deny incoming");
640
- exec("ufw default allow outgoing");
641
- exec("ufw allow 22/tcp comment 'SSH'");
642
- exec("ufw allow 80/tcp comment 'HTTP'");
643
- exec("ufw allow 443/tcp comment 'HTTPS'");
644
- exec("ufw allow from 172.16.0.0/12 to any port 9876 comment 'trafic-agent from Docker'");
645
- exec("ufw --force enable");
696
+ if (!io.commandExists("ufw")) io.exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ufw", { silent: true });
697
+ io.exec("ufw --force reset", { silent: true });
698
+ io.exec("ufw default deny incoming");
699
+ io.exec("ufw default allow outgoing");
700
+ io.exec("ufw allow 22/tcp comment 'SSH'");
701
+ io.exec("ufw allow 80/tcp comment 'HTTP'");
702
+ io.exec("ufw allow 443/tcp comment 'HTTPS'");
703
+ io.exec("ufw allow from 172.16.0.0/12 to any port 9876 comment 'trafic-agent from Docker'");
704
+ io.exec("ufw --force enable");
646
705
  success("Firewall enabled: SSH (22), HTTP (80), HTTPS (443), trafic-agent (9876 from Docker) allowed");
647
706
  }
648
707
  /**
649
708
  * Configure automatic security updates
650
709
  */
651
- function configureUnattendedUpgrades() {
710
+ function configureUnattendedUpgrades(io = nodeIo) {
652
711
  step("Configure automatic security updates");
653
- exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y unattended-upgrades apt-listchanges", { silent: true });
654
- writeFileSync("/etc/apt/apt.conf.d/50unattended-upgrades", `// Trafic: Automatic security updates
712
+ io.exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y unattended-upgrades apt-listchanges", { silent: true });
713
+ io.writeFile("/etc/apt/apt.conf.d/50unattended-upgrades", `// Trafic: Automatic security updates
655
714
  Unattended-Upgrade::Allowed-Origins {
656
715
  "\${distro_id}:\${distro_codename}";
657
716
  "\${distro_id}:\${distro_codename}-security";
@@ -668,22 +727,22 @@ Unattended-Upgrade::Remove-Unused-Dependencies "true";
668
727
  // Email notifications (optional)
669
728
  // Unattended-Upgrade::Mail "root";
670
729
  `);
671
- writeFileSync("/etc/apt/apt.conf.d/20auto-upgrades", `APT::Periodic::Update-Package-Lists "1";
730
+ io.writeFile("/etc/apt/apt.conf.d/20auto-upgrades", `APT::Periodic::Update-Package-Lists "1";
672
731
  APT::Periodic::Download-Upgradeable-Packages "1";
673
732
  APT::Periodic::AutocleanInterval "7";
674
733
  APT::Periodic::Unattended-Upgrade "1";
675
734
  `);
676
- exec("systemctl enable unattended-upgrades");
677
- exec("systemctl start unattended-upgrades");
735
+ io.exec("systemctl enable unattended-upgrades");
736
+ io.exec("systemctl start unattended-upgrades");
678
737
  success("Automatic security updates enabled");
679
738
  }
680
739
  /**
681
740
  * Configure Fail2ban
682
741
  */
683
- function configureFail2ban() {
742
+ function configureFail2ban(io = nodeIo) {
684
743
  step("Configure Fail2ban");
685
- if (!commandExists("fail2ban-client")) exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y fail2ban", { silent: true });
686
- writeFileSync("/etc/fail2ban/jail.local", `# Trafic: Fail2ban configuration
744
+ if (!io.commandExists("fail2ban-client")) io.exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y fail2ban", { silent: true });
745
+ io.writeFile("/etc/fail2ban/jail.local", `# Trafic: Fail2ban configuration
687
746
  [DEFAULT]
688
747
  bantime = 3600
689
748
  findtime = 600
@@ -698,55 +757,55 @@ logpath = /var/log/auth.log
698
757
  maxretry = 3
699
758
  bantime = 7200
700
759
  `);
701
- exec("chmod 644 /etc/fail2ban/jail.local");
702
- exec("systemctl enable fail2ban");
703
- exec("systemctl restart fail2ban");
760
+ io.exec("chmod 644 /etc/fail2ban/jail.local");
761
+ io.exec("systemctl enable fail2ban");
762
+ io.exec("systemctl restart fail2ban");
704
763
  success("Fail2ban enabled: SSH brute force protection active");
705
764
  }
706
765
  /**
707
766
  * Configure system limits
708
767
  */
709
- function configureSystemLimits() {
768
+ function configureSystemLimits(io = nodeIo) {
710
769
  step("Configure system limits");
711
- writeFileSync("/etc/security/limits.d/trafic.conf", `# Trafic: System limits for ddev user
770
+ io.writeFile("/etc/security/limits.d/trafic.conf", `# Trafic: System limits for ddev user
712
771
  ddev soft nofile 65536
713
772
  ddev hard nofile 65536
714
773
  ddev soft nproc 4096
715
774
  ddev hard nproc 4096
716
775
  `);
717
- exec("chmod 644 /etc/security/limits.d/trafic.conf");
776
+ io.exec("chmod 644 /etc/security/limits.d/trafic.conf");
718
777
  success("System limits configured for ddev user");
719
778
  }
720
779
  /**
721
780
  * Set proper file permissions
722
781
  */
723
- function configureFilePermissions() {
782
+ function configureFilePermissions(io = nodeIo) {
724
783
  step("Configure file permissions");
725
- exec("mkdir -p /etc/trafic");
726
- exec("mkdir -p /var/lib/trafic");
727
- exec("mkdir -p /var/backups/trafic");
728
- exec("chmod 755 /etc/trafic");
729
- exec("chmod 700 /var/lib/trafic");
730
- exec("chmod 700 /var/backups/trafic");
731
- exec("chown root:ddev /etc/trafic");
732
- exec("chown ddev:ddev /var/lib/trafic");
733
- exec("chown ddev:ddev /var/backups/trafic");
734
- if (existsSync("/etc/trafic/config.toml")) {
735
- exec("chmod 640 /etc/trafic/config.toml");
736
- exec("chown root:ddev /etc/trafic/config.toml");
784
+ io.exec("mkdir -p /etc/trafic");
785
+ io.exec("mkdir -p /var/lib/trafic");
786
+ io.exec("mkdir -p /var/backups/trafic");
787
+ io.exec("chmod 755 /etc/trafic");
788
+ io.exec("chmod 700 /var/lib/trafic");
789
+ io.exec("chmod 700 /var/backups/trafic");
790
+ io.exec("chown root:ddev /etc/trafic");
791
+ io.exec("chown ddev:ddev /var/lib/trafic");
792
+ io.exec("chown ddev:ddev /var/backups/trafic");
793
+ if (io.fileExists("/etc/trafic/config.toml")) {
794
+ io.exec("chmod 640 /etc/trafic/config.toml");
795
+ io.exec("chown root:ddev /etc/trafic/config.toml");
737
796
  }
738
797
  success("File permissions configured");
739
798
  }
740
799
  /**
741
800
  * Run all hardening steps
742
801
  */
743
- function hardenServer(sshUsers = ["ddev"]) {
744
- hardenSsh(sshUsers);
745
- configureFirewall();
746
- configureUnattendedUpgrades();
747
- configureFail2ban();
748
- configureSystemLimits();
749
- configureFilePermissions();
802
+ function hardenServer(sshUsers = ["ddev"], io = nodeIo) {
803
+ hardenSsh(sshUsers, io);
804
+ configureFirewall(io);
805
+ configureUnattendedUpgrades(io);
806
+ configureFail2ban(io);
807
+ configureSystemLimits(io);
808
+ configureFilePermissions(io);
750
809
  }
751
810
  //#endregion
752
811
  //#region src/setup/audit.ts
@@ -1324,4 +1383,4 @@ async function audit() {
1324
1383
  //#endregion
1325
1384
  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 };
1326
1385
 
1327
- //# sourceMappingURL=setup-C-gTXd-K.js.map
1386
+ //# sourceMappingURL=setup-4TI2lreq.js.map