@studiometa/trafic-agent 0.1.25 → 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 gnupg ca-certificates", { silent: true });
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 });
347
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,7 +481,7 @@ 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
@@ -468,13 +497,13 @@ var NODESOURCE_KEYRING = "/etc/apt/keyrings/nodesource.gpg";
468
497
  * nodejs.org points at NodeSource for apt installs. The Ubuntu 24.04
469
498
  * `nodejs` package is 18.x, far below what the agent needs.
470
499
  */
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 });
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 });
478
507
  }
479
508
  /**
480
509
  * Install Node.js from the NodeSource apt repository.
@@ -483,37 +512,37 @@ function addNodeSourceAptRepo() {
483
512
  * trafic-agent` resolves for the systemd unit — and unattended-upgrades
484
513
  * patches them, which a version manager install never gets.
485
514
  */
486
- function installNode() {
515
+ function installNode(io = nodeIo) {
487
516
  step("Install Node.js");
488
- if (commandExists("node")) {
489
- 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()}`);
490
519
  return;
491
520
  }
492
521
  info("Adding NodeSource apt repository...");
493
- addNodeSourceAptRepo();
522
+ addNodeSourceAptRepo(io);
494
523
  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 });
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 });
497
526
  success(`Node.js ${NODE_MAJOR} installed from apt`);
498
527
  }
499
528
  /**
500
529
  * Install the Trafic agent
501
530
  */
502
- function installAgent() {
531
+ function installAgent(io = nodeIo) {
503
532
  step("Install Trafic agent");
504
- exec("npm install -g @studiometa/trafic-agent");
533
+ io.exec("npm install -g @studiometa/trafic-agent");
505
534
  success("Trafic agent installed");
506
535
  }
507
536
  /**
508
537
  * Create the agent configuration
509
538
  */
510
- function createAgentConfig(tld, _email) {
539
+ function createAgentConfig(tld, _email, io = nodeIo) {
511
540
  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");
541
+ io.exec("mkdir -p /etc/trafic");
542
+ io.exec("mkdir -p /var/lib/trafic");
543
+ io.exec("chown ddev:ddev /var/lib/trafic");
515
544
  const configPath = "/etc/trafic/config.toml";
516
- if (existsSync(configPath)) {
545
+ if (io.fileExists(configPath)) {
517
546
  info("Configuration already exists at /etc/trafic/config.toml — skipping");
518
547
  info("Edit this file to configure authentication");
519
548
  return;
@@ -544,16 +573,16 @@ tokens = []
544
573
  basic_auth = []
545
574
  rules = []
546
575
  `;
547
- writeFileSync(configPath, config);
548
- exec("chmod 640 /etc/trafic/config.toml");
549
- 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");
550
579
  success("Configuration created at /etc/trafic/config.toml");
551
580
  info("Edit this file to configure authentication");
552
581
  }
553
582
  /**
554
583
  * Create systemd service for the agent
555
584
  */
556
- function createSystemdService() {
585
+ function createSystemdService(io = nodeIo) {
557
586
  step("Create systemd service");
558
587
  const serviceContent = `[Unit]
559
588
  Description=Trafic Agent - DDEV preview environment manager
@@ -564,7 +593,7 @@ Requires=docker.service
564
593
  Type=simple
565
594
  User=ddev
566
595
  Group=ddev
567
- 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
568
597
  Restart=always
569
598
  RestartSec=5
570
599
  StandardOutput=journal
@@ -588,21 +617,35 @@ PrivateTmp=true
588
617
  [Install]
589
618
  WantedBy=multi-user.target
590
619
  `;
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");
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");
596
625
  success("Trafic agent service started");
597
626
  info("View logs: journalctl -u trafic-agent -f");
598
627
  }
599
628
  //#endregion
600
629
  //#region src/setup/hardening.ts
601
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
+ /**
602
644
  * Configure SSH hardening
603
645
  */
604
- function hardenSsh(allowedUsers = ["ddev"]) {
646
+ function hardenSsh(allowedUsers = ["ddev"], io = nodeIo) {
605
647
  step("Harden SSH configuration");
648
+ const users = resolveAllowedUsers(allowedUsers, io.env.SUDO_USER);
606
649
  const sshConfig = `# Trafic SSH hardening
607
650
  # Generated by trafic setup
608
651
 
@@ -621,7 +664,7 @@ ClientAliveInterval 300
621
664
  ClientAliveCountMax 2
622
665
 
623
666
  # Allowed users (root kept for emergency access)
624
- AllowUsers root ${allowedUsers.join(" ")}
667
+ AllowUsers root ${users.join(" ")}
625
668
 
626
669
  # Disable X11 forwarding
627
670
  X11Forwarding no
@@ -634,40 +677,40 @@ KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256
634
677
  Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
635
678
  MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
636
679
  `;
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")) {
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")) {
640
683
  warn("SSH config test failed, reverting...");
641
- exec("rm /etc/ssh/sshd_config.d/trafic.conf");
684
+ io.exec("rm /etc/ssh/sshd_config.d/trafic.conf");
642
685
  return;
643
686
  }
644
- exec("systemctl reload ssh || systemctl reload sshd");
687
+ io.exec("systemctl reload ssh || systemctl reload sshd");
645
688
  success("SSH hardened: password auth disabled, root key-only login retained");
646
- info(`Allowed users: root, ${allowedUsers.join(", ")}`);
689
+ info(`Allowed users: root, ${users.join(", ")}`);
647
690
  }
648
691
  /**
649
692
  * Configure UFW firewall
650
693
  */
651
- function configureFirewall() {
694
+ function configureFirewall(io = nodeIo) {
652
695
  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");
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");
662
705
  success("Firewall enabled: SSH (22), HTTP (80), HTTPS (443), trafic-agent (9876 from Docker) allowed");
663
706
  }
664
707
  /**
665
708
  * Configure automatic security updates
666
709
  */
667
- function configureUnattendedUpgrades() {
710
+ function configureUnattendedUpgrades(io = nodeIo) {
668
711
  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
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
671
714
  Unattended-Upgrade::Allowed-Origins {
672
715
  "\${distro_id}:\${distro_codename}";
673
716
  "\${distro_id}:\${distro_codename}-security";
@@ -684,22 +727,22 @@ Unattended-Upgrade::Remove-Unused-Dependencies "true";
684
727
  // Email notifications (optional)
685
728
  // Unattended-Upgrade::Mail "root";
686
729
  `);
687
- 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";
688
731
  APT::Periodic::Download-Upgradeable-Packages "1";
689
732
  APT::Periodic::AutocleanInterval "7";
690
733
  APT::Periodic::Unattended-Upgrade "1";
691
734
  `);
692
- exec("systemctl enable unattended-upgrades");
693
- exec("systemctl start unattended-upgrades");
735
+ io.exec("systemctl enable unattended-upgrades");
736
+ io.exec("systemctl start unattended-upgrades");
694
737
  success("Automatic security updates enabled");
695
738
  }
696
739
  /**
697
740
  * Configure Fail2ban
698
741
  */
699
- function configureFail2ban() {
742
+ function configureFail2ban(io = nodeIo) {
700
743
  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
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
703
746
  [DEFAULT]
704
747
  bantime = 3600
705
748
  findtime = 600
@@ -714,55 +757,55 @@ logpath = /var/log/auth.log
714
757
  maxretry = 3
715
758
  bantime = 7200
716
759
  `);
717
- exec("chmod 644 /etc/fail2ban/jail.local");
718
- exec("systemctl enable fail2ban");
719
- 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");
720
763
  success("Fail2ban enabled: SSH brute force protection active");
721
764
  }
722
765
  /**
723
766
  * Configure system limits
724
767
  */
725
- function configureSystemLimits() {
768
+ function configureSystemLimits(io = nodeIo) {
726
769
  step("Configure system limits");
727
- 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
728
771
  ddev soft nofile 65536
729
772
  ddev hard nofile 65536
730
773
  ddev soft nproc 4096
731
774
  ddev hard nproc 4096
732
775
  `);
733
- exec("chmod 644 /etc/security/limits.d/trafic.conf");
776
+ io.exec("chmod 644 /etc/security/limits.d/trafic.conf");
734
777
  success("System limits configured for ddev user");
735
778
  }
736
779
  /**
737
780
  * Set proper file permissions
738
781
  */
739
- function configureFilePermissions() {
782
+ function configureFilePermissions(io = nodeIo) {
740
783
  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");
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");
753
796
  }
754
797
  success("File permissions configured");
755
798
  }
756
799
  /**
757
800
  * Run all hardening steps
758
801
  */
759
- function hardenServer(sshUsers = ["ddev"]) {
760
- hardenSsh(sshUsers);
761
- configureFirewall();
762
- configureUnattendedUpgrades();
763
- configureFail2ban();
764
- configureSystemLimits();
765
- 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);
766
809
  }
767
810
  //#endregion
768
811
  //#region src/setup/audit.ts
@@ -1340,4 +1383,4 @@ async function audit() {
1340
1383
  //#endregion
1341
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 };
1342
1385
 
1343
- //# sourceMappingURL=setup-D61uhYiJ.js.map
1386
+ //# sourceMappingURL=setup-4TI2lreq.js.map