@studiometa/trafic-agent 0.1.0

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.
Files changed (51) hide show
  1. package/dist/cli.d.ts +3 -0
  2. package/dist/cli.d.ts.map +1 -0
  3. package/dist/cli.js +140 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/index-C7OaqqbZ.js +865 -0
  6. package/dist/index-C7OaqqbZ.js.map +1 -0
  7. package/dist/index.d.ts +12 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +41 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/server-CHT47EXr.js +618 -0
  12. package/dist/server-CHT47EXr.js.map +1 -0
  13. package/dist/server.d.ts +6 -0
  14. package/dist/server.d.ts.map +1 -0
  15. package/dist/server.js +9 -0
  16. package/dist/server.js.map +1 -0
  17. package/dist/setup/agent.d.ts +17 -0
  18. package/dist/setup/agent.d.ts.map +1 -0
  19. package/dist/setup/audit.d.ts +10 -0
  20. package/dist/setup/audit.d.ts.map +1 -0
  21. package/dist/setup/ddev.d.ts +27 -0
  22. package/dist/setup/ddev.d.ts.map +1 -0
  23. package/dist/setup/docker.d.ts +13 -0
  24. package/dist/setup/docker.d.ts.map +1 -0
  25. package/dist/setup/hardening.d.ts +29 -0
  26. package/dist/setup/hardening.d.ts.map +1 -0
  27. package/dist/setup/index.d.ts +12 -0
  28. package/dist/setup/index.d.ts.map +1 -0
  29. package/dist/setup/steps.d.ts +51 -0
  30. package/dist/setup/steps.d.ts.map +1 -0
  31. package/dist/setup/types.d.ts +37 -0
  32. package/dist/setup/types.d.ts.map +1 -0
  33. package/dist/setup/user.d.ts +9 -0
  34. package/dist/setup/user.d.ts.map +1 -0
  35. package/dist/tasks/stop-idle.d.ts +11 -0
  36. package/dist/tasks/stop-idle.d.ts.map +1 -0
  37. package/dist/types.d.ts +99 -0
  38. package/dist/types.d.ts.map +1 -0
  39. package/dist/utils/auth.d.ts +25 -0
  40. package/dist/utils/auth.d.ts.map +1 -0
  41. package/dist/utils/config.d.ts +14 -0
  42. package/dist/utils/config.d.ts.map +1 -0
  43. package/dist/utils/db.d.ts +43 -0
  44. package/dist/utils/db.d.ts.map +1 -0
  45. package/dist/utils/ddev.d.ts +35 -0
  46. package/dist/utils/ddev.d.ts.map +1 -0
  47. package/dist/utils/project-config.d.ts +23 -0
  48. package/dist/utils/project-config.d.ts.map +1 -0
  49. package/package.json +56 -0
  50. package/templates/error.html +59 -0
  51. package/templates/wait.html +86 -0
@@ -0,0 +1,865 @@
1
+ import { o as loadProjectList, f as getIdleProjects, n as loadProjectConfig, v as shouldNeverStop, h as getIdleTimeoutMs, k as getProjectInfo, u as setProjectStatus, y as stopProject } from "./server-CHT47EXr.js";
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { parse } from "smol-toml";
4
+ import { execSync } from "node:child_process";
5
+ const DEFAULT_CONFIG_PATHS = [
6
+ "/etc/trafic/config.toml",
7
+ "./config.toml",
8
+ "./trafic.toml"
9
+ ];
10
+ const DEFAULT_CONFIG = {
11
+ tld: "",
12
+ port: 9876,
13
+ dbPath: "/var/lib/trafic/db.sqlite",
14
+ projectListPath: "/home/ddev/.ddev/project_list.yaml",
15
+ projectsDir: "/home/ddev/www",
16
+ idleTimeout: "30m",
17
+ idleCheckInterval: "5m",
18
+ auth: {
19
+ defaultPolicy: "basic",
20
+ allowedIps: [],
21
+ tokens: [],
22
+ basicAuth: [],
23
+ rules: []
24
+ }
25
+ };
26
+ function parseDuration(duration) {
27
+ const regex = /(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/;
28
+ const match = duration.match(regex);
29
+ if (!match) return 0;
30
+ const hours = parseInt(match[1] ?? "0", 10);
31
+ const minutes = parseInt(match[2] ?? "0", 10);
32
+ const seconds = parseInt(match[3] ?? "0", 10);
33
+ return (hours * 3600 + minutes * 60 + seconds) * 1e3;
34
+ }
35
+ function loadConfig(configPath) {
36
+ let path = configPath;
37
+ if (!path) {
38
+ path = DEFAULT_CONFIG_PATHS.find((p) => existsSync(p));
39
+ }
40
+ if (!path || !existsSync(path)) {
41
+ console.warn("No config file found, using defaults");
42
+ return DEFAULT_CONFIG;
43
+ }
44
+ const content = readFileSync(path, "utf-8");
45
+ const parsed = parse(content);
46
+ const auth = mergeAuthConfig(parsed.auth);
47
+ return {
48
+ tld: parsed.tld ?? DEFAULT_CONFIG.tld,
49
+ port: parsed.port ?? DEFAULT_CONFIG.port,
50
+ dbPath: parsed.db_path ?? DEFAULT_CONFIG.dbPath,
51
+ projectListPath: parsed.project_list_path ?? DEFAULT_CONFIG.projectListPath,
52
+ projectsDir: parsed.projects_dir ?? DEFAULT_CONFIG.projectsDir,
53
+ idleTimeout: parsed.idle_timeout ?? DEFAULT_CONFIG.idleTimeout,
54
+ idleCheckInterval: parsed.idle_check_interval ?? DEFAULT_CONFIG.idleCheckInterval,
55
+ auth
56
+ };
57
+ }
58
+ function mergeAuthConfig(raw) {
59
+ if (!raw) return DEFAULT_CONFIG.auth;
60
+ const rules = raw.rules ?? [];
61
+ return {
62
+ defaultPolicy: raw.default_policy ?? DEFAULT_CONFIG.auth.defaultPolicy,
63
+ allowedIps: raw.allowed_ips ?? DEFAULT_CONFIG.auth.allowedIps,
64
+ tokens: raw.tokens ?? DEFAULT_CONFIG.auth.tokens,
65
+ basicAuth: raw.basic_auth ?? DEFAULT_CONFIG.auth.basicAuth,
66
+ rules: rules.map(
67
+ (r) => ({
68
+ match: r.match,
69
+ policy: r.policy,
70
+ tokens: r.tokens,
71
+ allowedIps: r.allowed_ips
72
+ })
73
+ )
74
+ };
75
+ }
76
+ function validateConfig(config) {
77
+ const errors = [];
78
+ if (!config.tld) {
79
+ errors.push("tld is required");
80
+ }
81
+ if (config.port < 1 || config.port > 65535) {
82
+ errors.push("port must be between 1 and 65535");
83
+ }
84
+ if (config.auth.defaultPolicy !== "allow" && config.auth.defaultPolicy !== "deny" && config.auth.defaultPolicy !== "basic" && config.auth.defaultPolicy !== "token") {
85
+ errors.push('auth.default_policy must be "allow", "deny", "basic", or "token"');
86
+ }
87
+ return errors;
88
+ }
89
+ function stopIdleProjects(config) {
90
+ const globalThresholdMs = parseDuration(config.idleTimeout);
91
+ const projects = loadProjectList(config.projectListPath);
92
+ const idleProjects = getIdleProjects(globalThresholdMs);
93
+ for (const project of idleProjects) {
94
+ const projectDir = projects.get(project.name);
95
+ if (projectDir) {
96
+ const projectConfig = loadProjectConfig(projectDir);
97
+ if (shouldNeverStop(projectConfig)) {
98
+ continue;
99
+ }
100
+ const projectThresholdMs = getIdleTimeoutMs(projectConfig);
101
+ if (projectThresholdMs !== void 0) {
102
+ const idleDuration = Date.now() - project.lastAccess;
103
+ if (idleDuration < projectThresholdMs) {
104
+ continue;
105
+ }
106
+ }
107
+ }
108
+ const info2 = getProjectInfo(project.name);
109
+ if (info2?.status !== "running") {
110
+ setProjectStatus(project.name, "stopped");
111
+ continue;
112
+ }
113
+ console.log(`Stopping idle project: ${project.name}`);
114
+ const success2 = stopProject(project.name);
115
+ if (success2) {
116
+ setProjectStatus(project.name, "stopped");
117
+ console.log(`Stopped: ${project.name}`);
118
+ } else {
119
+ console.error(`Failed to stop: ${project.name}`);
120
+ }
121
+ }
122
+ }
123
+ function startIdleScheduler(config) {
124
+ const intervalMs = parseDuration(config.idleCheckInterval);
125
+ console.log(
126
+ `Idle scheduler started: checking every ${config.idleCheckInterval}, timeout ${config.idleTimeout}`
127
+ );
128
+ stopIdleProjects(config);
129
+ return setInterval(() => {
130
+ stopIdleProjects(config);
131
+ }, intervalMs);
132
+ }
133
+ let stepNumber = 0;
134
+ let dryRun = false;
135
+ function setDryRun(value) {
136
+ dryRun = value;
137
+ }
138
+ function resetSteps() {
139
+ stepNumber = 0;
140
+ }
141
+ function step(message) {
142
+ stepNumber++;
143
+ console.log(`
144
+ \x1B[36m[${stepNumber}]\x1B[0m ${message}`);
145
+ }
146
+ function success(message) {
147
+ console.log(` \x1B[32m✓\x1B[0m ${message}`);
148
+ }
149
+ function warn(message) {
150
+ console.log(` \x1B[33m⚠\x1B[0m ${message}`);
151
+ }
152
+ function error(message) {
153
+ console.log(` \x1B[31m✗\x1B[0m ${message}`);
154
+ }
155
+ function info(message) {
156
+ console.log(` ${message}`);
157
+ }
158
+ function exec(command, options) {
159
+ if (dryRun) {
160
+ if (!options?.silent) {
161
+ console.log(` \x1B[90m$ ${command}\x1B[0m`);
162
+ }
163
+ return "";
164
+ }
165
+ try {
166
+ return execSync(command, {
167
+ encoding: "utf-8",
168
+ stdio: options?.silent ? "pipe" : "inherit"
169
+ });
170
+ } catch (err) {
171
+ const e = err;
172
+ throw new Error(`Command failed: ${command}
173
+ ${e.stderr ?? ""}`);
174
+ }
175
+ }
176
+ function execSilent$1(command) {
177
+ if (dryRun) {
178
+ return "";
179
+ }
180
+ try {
181
+ return execSync(command, {
182
+ encoding: "utf-8",
183
+ stdio: "pipe"
184
+ }).trim();
185
+ } catch {
186
+ return "";
187
+ }
188
+ }
189
+ function commandExists(command) {
190
+ try {
191
+ execSync(`which ${command}`, { stdio: "pipe" });
192
+ return true;
193
+ } catch {
194
+ return false;
195
+ }
196
+ }
197
+ function isRoot() {
198
+ return process.getuid?.() === 0;
199
+ }
200
+ function createDdevUser() {
201
+ step("Create ddev user");
202
+ const userExists = execSilent$1("id -u ddev 2>/dev/null");
203
+ if (userExists) {
204
+ info("User 'ddev' already exists");
205
+ } else {
206
+ exec("useradd -m -s /bin/bash ddev");
207
+ success("Created user 'ddev'");
208
+ }
209
+ exec("usermod -aG docker ddev 2>/dev/null || true", { silent: true });
210
+ exec("mkdir -p /home/ddev/www");
211
+ exec("chown ddev:ddev /home/ddev/www");
212
+ exec("chmod 750 /home/ddev/www");
213
+ success("Created /home/ddev/www");
214
+ exec("mkdir -p /home/ddev/.ssh");
215
+ exec("chmod 700 /home/ddev/.ssh");
216
+ exec("chown ddev:ddev /home/ddev/.ssh");
217
+ success("User ddev configured (no sudo needed with proper DNS)");
218
+ }
219
+ function installDocker() {
220
+ step("Install Docker");
221
+ if (commandExists("docker")) {
222
+ const version = exec("docker --version", { silent: true });
223
+ info(`Docker already installed: ${version?.trim() ?? "unknown version"}`);
224
+ return;
225
+ }
226
+ info("Downloading Docker install script...");
227
+ exec("curl -fsSL https://get.docker.com -o /tmp/get-docker.sh");
228
+ exec("sh /tmp/get-docker.sh");
229
+ exec("rm /tmp/get-docker.sh");
230
+ exec("systemctl enable docker");
231
+ exec("systemctl start docker");
232
+ success("Docker installed and started");
233
+ exec("usermod -aG docker ddev");
234
+ success("Added ddev user to docker group");
235
+ }
236
+ function configureDocker() {
237
+ step("Configure Docker");
238
+ const daemonConfig = {
239
+ "log-driver": "json-file",
240
+ "log-opts": {
241
+ "max-size": "10m",
242
+ "max-file": "3"
243
+ },
244
+ "storage-driver": "overlay2",
245
+ "live-restore": true
246
+ };
247
+ exec("mkdir -p /etc/docker");
248
+ const configPath = "/etc/docker/daemon.json";
249
+ const { writeFileSync: writeFileSync2, existsSync: existsSync2, readFileSync: readFileSync2 } = require("node:fs");
250
+ if (existsSync2(configPath)) {
251
+ try {
252
+ const existing = JSON.parse(readFileSync2(configPath, "utf-8"));
253
+ const merged = { ...existing, ...daemonConfig };
254
+ writeFileSync2(configPath, JSON.stringify(merged, null, 2));
255
+ info("Merged with existing Docker config");
256
+ } catch {
257
+ warn("Could not parse existing Docker config, skipping");
258
+ return;
259
+ }
260
+ } else {
261
+ writeFileSync2(configPath, JSON.stringify(daemonConfig, null, 2));
262
+ }
263
+ exec("systemctl reload docker || systemctl restart docker");
264
+ success("Docker configured with production settings");
265
+ }
266
+ function setupDockerPrune() {
267
+ step("Setup Docker cleanup cron");
268
+ const cronContent = `# Trafic: Clean up Docker resources weekly
269
+ 0 3 * * 0 root docker system prune -af --volumes 2>&1 | logger -t docker-prune
270
+ `;
271
+ const { writeFileSync: writeFileSync2 } = require("node:fs");
272
+ writeFileSync2("/etc/cron.d/trafic-docker-prune", cronContent);
273
+ exec("chmod 644 /etc/cron.d/trafic-docker-prune");
274
+ success("Docker prune scheduled weekly at 3am on Sundays");
275
+ }
276
+ function installDdev() {
277
+ step("Install DDEV");
278
+ if (commandExists("ddev")) {
279
+ const version = exec("ddev --version 2>/dev/null | head -1", { silent: true });
280
+ info(`DDEV already installed: ${version?.trim() ?? "unknown version"}`);
281
+ return;
282
+ }
283
+ info("Installing DDEV...");
284
+ exec(
285
+ "curl -fsSL https://ddev.com/install.sh | bash -s -- --version latest"
286
+ );
287
+ success("DDEV installed");
288
+ }
289
+ function configureDdev(tld, email) {
290
+ step("Configure DDEV");
291
+ const ddevCmd = (cmd) => `su - ddev -c '${cmd}'`;
292
+ exec(
293
+ ddevCmd(
294
+ `ddev config global --project-tld=${tld} --router-http-port=80 --router-https-port=443 --use-letsencrypt=${email ? "true" : "false"} ${email ? `--letsencrypt-email=${email}` : ""}`
295
+ )
296
+ );
297
+ success(`DDEV configured with TLD: ${tld}`);
298
+ info(`Ensure DNS is configured: *.${tld} → server IP`);
299
+ if (email) {
300
+ success(`Let's Encrypt enabled with email: ${email}`);
301
+ }
302
+ exec(ddevCmd("ddev config global --instrumentation-opt-in=false"));
303
+ exec(ddevCmd("ddev poweroff || true"));
304
+ exec(ddevCmd("ddev start"));
305
+ success("DDEV router started");
306
+ }
307
+ function configureTraefik() {
308
+ step("Configure Traefik for forward auth");
309
+ exec("mkdir -p /home/ddev/.ddev/traefik");
310
+ exec("chown -R ddev:ddev /home/ddev/.ddev");
311
+ const staticConfig = `# Trafic: Forward auth middleware
312
+ http:
313
+ middlewares:
314
+ trafic-auth:
315
+ forwardAuth:
316
+ address: "http://host.docker.internal:9876/__auth__"
317
+ authResponseHeaders:
318
+ - "X-Trafic-Project"
319
+
320
+ trafic-errors:
321
+ errors:
322
+ status:
323
+ - "502"
324
+ - "503"
325
+ service: trafic-service
326
+ query: "/"
327
+
328
+ services:
329
+ trafic-service:
330
+ loadBalancer:
331
+ servers:
332
+ - url: "http://host.docker.internal:9876"
333
+ `;
334
+ const { writeFileSync: writeFileSync2 } = require("node:fs");
335
+ writeFileSync2("/home/ddev/.ddev/traefik/trafic.yaml", staticConfig);
336
+ exec("chown ddev:ddev /home/ddev/.ddev/traefik/trafic.yaml");
337
+ const routerConfig = `# Trafic: Default router configuration
338
+ # This file makes all DDEV projects use the Trafic auth middleware
339
+ http:
340
+ routers:
341
+ trafic-catch-all:
342
+ rule: "HostRegexp(\`.+\`)"
343
+ priority: 1
344
+ middlewares:
345
+ - trafic-auth
346
+ - trafic-errors
347
+ service: api@internal
348
+ `;
349
+ writeFileSync2("/home/ddev/.ddev/traefik/dynamic.yaml", routerConfig);
350
+ exec("chown ddev:ddev /home/ddev/.ddev/traefik/dynamic.yaml");
351
+ success("Traefik configured with Trafic middleware");
352
+ }
353
+ function installNode() {
354
+ step("Install Node.js");
355
+ if (commandExists("node")) {
356
+ const version = exec("node --version", { silent: true });
357
+ info(`Node.js already installed: ${version?.trim()}`);
358
+ return;
359
+ }
360
+ exec(
361
+ 'curl -fsSL https://fnm.vercel.app/install | bash -s -- --install-dir "/opt/fnm" --skip-shell'
362
+ );
363
+ const profileContent = `# Trafic: fnm (Node.js version manager)
364
+ export FNM_DIR="/opt/fnm"
365
+ export PATH="/opt/fnm:$PATH"
366
+ eval "$(fnm env)"
367
+ `;
368
+ writeFileSync("/etc/profile.d/fnm.sh", profileContent);
369
+ exec("chmod 644 /etc/profile.d/fnm.sh");
370
+ exec("export FNM_DIR=/opt/fnm && /opt/fnm/fnm install 24");
371
+ exec("export FNM_DIR=/opt/fnm && /opt/fnm/fnm default 24");
372
+ exec(
373
+ "ln -sf /opt/fnm/aliases/default/bin/node /usr/local/bin/node || true"
374
+ );
375
+ exec("ln -sf /opt/fnm/aliases/default/bin/npm /usr/local/bin/npm || true");
376
+ exec("ln -sf /opt/fnm/aliases/default/bin/npx /usr/local/bin/npx || true");
377
+ success("Node.js 24 installed via fnm");
378
+ }
379
+ function installAgent() {
380
+ step("Install Trafic agent");
381
+ exec("npm install -g @studiometa/trafic-agent");
382
+ success("Trafic agent installed");
383
+ }
384
+ function createAgentConfig(tld, _email) {
385
+ step("Create agent configuration");
386
+ const config = `# Trafic agent configuration
387
+ # Generated by trafic setup
388
+
389
+ # Required: TLD for DDEV projects
390
+ tld = "${tld}"
391
+
392
+ # Agent HTTP server port
393
+ port = 9876
394
+
395
+ # Database and paths
396
+ db_path = "/var/lib/trafic/db.sqlite"
397
+ project_list_path = "/home/ddev/.ddev/project_list.yaml"
398
+ projects_dir = "/home/ddev/www"
399
+
400
+ # Scale-to-zero settings
401
+ idle_timeout = "4h"
402
+ idle_check_interval = "5m"
403
+
404
+ # Authentication
405
+ [auth]
406
+ default_policy = "basic"
407
+ allowed_ips = []
408
+ tokens = []
409
+ basic_auth = []
410
+ rules = []
411
+ `;
412
+ writeFileSync("/etc/trafic/config.toml", config);
413
+ exec("chmod 640 /etc/trafic/config.toml");
414
+ exec("chown root:ddev /etc/trafic/config.toml");
415
+ success("Configuration created at /etc/trafic/config.toml");
416
+ info("Edit this file to configure authentication");
417
+ }
418
+ function createSystemdService() {
419
+ step("Create systemd service");
420
+ const serviceContent = `[Unit]
421
+ Description=Trafic Agent - DDEV preview environment manager
422
+ After=network.target docker.service
423
+ Requires=docker.service
424
+
425
+ [Service]
426
+ Type=simple
427
+ User=ddev
428
+ Group=ddev
429
+ ExecStart=/usr/local/bin/npx trafic-agent start --config /etc/trafic/config.toml
430
+ Restart=always
431
+ RestartSec=5
432
+ StandardOutput=journal
433
+ StandardError=journal
434
+ SyslogIdentifier=trafic-agent
435
+
436
+ # Environment
437
+ Environment=NODE_ENV=production
438
+
439
+ # Security hardening
440
+ NoNewPrivileges=true
441
+ ProtectSystem=strict
442
+ ProtectHome=read-only
443
+ ReadWritePaths=/var/lib/trafic /home/ddev/.ddev
444
+ PrivateTmp=true
445
+
446
+ [Install]
447
+ WantedBy=multi-user.target
448
+ `;
449
+ writeFileSync("/etc/systemd/system/trafic-agent.service", serviceContent);
450
+ exec("chmod 644 /etc/systemd/system/trafic-agent.service");
451
+ exec("systemctl daemon-reload");
452
+ exec("systemctl enable trafic-agent");
453
+ exec("systemctl start trafic-agent");
454
+ success("Trafic agent service started");
455
+ info("View logs: journalctl -u trafic-agent -f");
456
+ }
457
+ function hardenSsh(allowedUsers = ["ddev"]) {
458
+ step("Harden SSH configuration");
459
+ const sshConfig = `# Trafic SSH hardening
460
+ # Generated by trafic setup
461
+
462
+ # Disable root login
463
+ PermitRootLogin no
464
+
465
+ # Disable password authentication (keys only)
466
+ PasswordAuthentication no
467
+ PubkeyAuthentication yes
468
+
469
+ # Limit authentication attempts
470
+ MaxAuthTries 3
471
+
472
+ # Idle timeout (5 minutes)
473
+ ClientAliveInterval 300
474
+ ClientAliveCountMax 2
475
+
476
+ # Allowed users
477
+ AllowUsers ${allowedUsers.join(" ")}
478
+
479
+ # Disable X11 forwarding
480
+ X11Forwarding no
481
+
482
+ # Disable TCP forwarding
483
+ AllowTcpForwarding no
484
+
485
+ # Use strong algorithms only
486
+ KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256
487
+ Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
488
+ MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
489
+ `;
490
+ writeFileSync("/etc/ssh/sshd_config.d/trafic.conf", sshConfig);
491
+ exec("chmod 644 /etc/ssh/sshd_config.d/trafic.conf");
492
+ const testResult = exec("sshd -t 2>&1 || echo 'error'", { silent: true });
493
+ if (testResult?.includes("error")) {
494
+ warn("SSH config test failed, reverting...");
495
+ exec("rm /etc/ssh/sshd_config.d/trafic.conf");
496
+ return;
497
+ }
498
+ exec("systemctl reload sshd || systemctl reload ssh");
499
+ success("SSH hardened: root login disabled, password auth disabled");
500
+ info(`Allowed users: ${allowedUsers.join(", ")}`);
501
+ }
502
+ function configureFirewall() {
503
+ step("Configure firewall (UFW)");
504
+ if (!commandExists("ufw")) {
505
+ exec("apt-get update && apt-get install -y ufw", { silent: true });
506
+ }
507
+ exec("ufw --force reset", { silent: true });
508
+ exec("ufw default deny incoming");
509
+ exec("ufw default allow outgoing");
510
+ exec("ufw allow 22/tcp comment 'SSH'");
511
+ exec("ufw allow 80/tcp comment 'HTTP'");
512
+ exec("ufw allow 443/tcp comment 'HTTPS'");
513
+ exec("ufw --force enable");
514
+ success("Firewall enabled: SSH (22), HTTP (80), HTTPS (443) allowed");
515
+ }
516
+ function configureUnattendedUpgrades() {
517
+ step("Configure automatic security updates");
518
+ exec(
519
+ "DEBIAN_FRONTEND=noninteractive apt-get install -y unattended-upgrades apt-listchanges",
520
+ { silent: true }
521
+ );
522
+ const upgradesConfig = `// Trafic: Automatic security updates
523
+ Unattended-Upgrade::Allowed-Origins {
524
+ "\${distro_id}:\${distro_codename}";
525
+ "\${distro_id}:\${distro_codename}-security";
526
+ "\${distro_id}ESMApps:\${distro_codename}-apps-security";
527
+ "\${distro_id}ESM:\${distro_codename}-infra-security";
528
+ };
529
+
530
+ // Do not automatically reboot
531
+ Unattended-Upgrade::Automatic-Reboot "false";
532
+
533
+ // Remove unused dependencies
534
+ Unattended-Upgrade::Remove-Unused-Dependencies "true";
535
+
536
+ // Email notifications (optional)
537
+ // Unattended-Upgrade::Mail "root";
538
+ `;
539
+ writeFileSync(
540
+ "/etc/apt/apt.conf.d/50unattended-upgrades",
541
+ upgradesConfig
542
+ );
543
+ const autoConfig = `APT::Periodic::Update-Package-Lists "1";
544
+ APT::Periodic::Download-Upgradeable-Packages "1";
545
+ APT::Periodic::AutocleanInterval "7";
546
+ APT::Periodic::Unattended-Upgrade "1";
547
+ `;
548
+ writeFileSync("/etc/apt/apt.conf.d/20auto-upgrades", autoConfig);
549
+ exec("systemctl enable unattended-upgrades");
550
+ exec("systemctl start unattended-upgrades");
551
+ success("Automatic security updates enabled");
552
+ }
553
+ function configureFail2ban() {
554
+ step("Configure Fail2ban");
555
+ if (!commandExists("fail2ban-client")) {
556
+ exec("apt-get update && apt-get install -y fail2ban", { silent: true });
557
+ }
558
+ const jailConfig = `# Trafic: Fail2ban configuration
559
+ [DEFAULT]
560
+ bantime = 3600
561
+ findtime = 600
562
+ maxretry = 5
563
+ backend = auto
564
+
565
+ [sshd]
566
+ enabled = true
567
+ port = ssh
568
+ filter = sshd
569
+ logpath = /var/log/auth.log
570
+ maxretry = 3
571
+ bantime = 7200
572
+ `;
573
+ writeFileSync("/etc/fail2ban/jail.local", jailConfig);
574
+ exec("chmod 644 /etc/fail2ban/jail.local");
575
+ exec("systemctl enable fail2ban");
576
+ exec("systemctl restart fail2ban");
577
+ success("Fail2ban enabled: SSH brute force protection active");
578
+ }
579
+ function configureSystemLimits() {
580
+ step("Configure system limits");
581
+ const limitsConfig = `# Trafic: System limits for ddev user
582
+ ddev soft nofile 65536
583
+ ddev hard nofile 65536
584
+ ddev soft nproc 4096
585
+ ddev hard nproc 4096
586
+ `;
587
+ writeFileSync("/etc/security/limits.d/trafic.conf", limitsConfig);
588
+ exec("chmod 644 /etc/security/limits.d/trafic.conf");
589
+ success("System limits configured for ddev user");
590
+ }
591
+ function configureFilePermissions() {
592
+ step("Configure file permissions");
593
+ exec("mkdir -p /etc/trafic");
594
+ exec("mkdir -p /var/lib/trafic");
595
+ exec("mkdir -p /var/backups/trafic");
596
+ exec("chmod 755 /etc/trafic");
597
+ exec("chmod 700 /var/lib/trafic");
598
+ exec("chmod 700 /var/backups/trafic");
599
+ exec("chown root:ddev /etc/trafic");
600
+ exec("chown ddev:ddev /var/lib/trafic");
601
+ exec("chown ddev:ddev /var/backups/trafic");
602
+ if (existsSync("/etc/trafic/config.toml")) {
603
+ exec("chmod 640 /etc/trafic/config.toml");
604
+ exec("chown root:ddev /etc/trafic/config.toml");
605
+ }
606
+ success("File permissions configured");
607
+ }
608
+ function hardenServer(sshUsers = ["ddev"]) {
609
+ hardenSsh(sshUsers);
610
+ configureFirewall();
611
+ configureUnattendedUpgrades();
612
+ configureFail2ban();
613
+ configureSystemLimits();
614
+ configureFilePermissions();
615
+ }
616
+ function execSilent(command) {
617
+ try {
618
+ return execSync(command, { encoding: "utf-8", stdio: "pipe" }).trim();
619
+ } catch {
620
+ return "";
621
+ }
622
+ }
623
+ function isServiceActive(service) {
624
+ const status = execSilent(`systemctl is-active ${service}`);
625
+ return status === "active";
626
+ }
627
+ function auditSsh() {
628
+ const checks = [];
629
+ const sshdConfig = existsSync("/etc/ssh/sshd_config") ? readFileSync("/etc/ssh/sshd_config", "utf-8") : "";
630
+ const traficConfig = existsSync("/etc/ssh/sshd_config.d/trafic.conf") ? readFileSync("/etc/ssh/sshd_config.d/trafic.conf", "utf-8") : "";
631
+ const config = sshdConfig + "\n" + traficConfig;
632
+ const rootLoginDisabled = config.includes("PermitRootLogin no") || config.includes("PermitRootLogin prohibit-password");
633
+ checks.push({
634
+ name: "SSH root login",
635
+ status: rootLoginDisabled ? "pass" : "fail",
636
+ message: rootLoginDisabled ? "Root login disabled" : "Root login may be enabled",
637
+ fix: "Add 'PermitRootLogin no' to /etc/ssh/sshd_config.d/trafic.conf"
638
+ });
639
+ const passwordAuthDisabled = config.includes("PasswordAuthentication no");
640
+ checks.push({
641
+ name: "SSH password auth",
642
+ status: passwordAuthDisabled ? "pass" : "warn",
643
+ message: passwordAuthDisabled ? "Password authentication disabled" : "Password authentication may be enabled",
644
+ fix: "Add 'PasswordAuthentication no' to /etc/ssh/sshd_config.d/trafic.conf"
645
+ });
646
+ return checks;
647
+ }
648
+ function auditFirewall() {
649
+ const checks = [];
650
+ const ufwStatus = execSilent("ufw status");
651
+ const isActive = ufwStatus.includes("Status: active");
652
+ checks.push({
653
+ name: "Firewall (UFW)",
654
+ status: isActive ? "pass" : "fail",
655
+ message: isActive ? "UFW is active" : "UFW is not active",
656
+ fix: "Run: ufw enable"
657
+ });
658
+ if (isActive) {
659
+ const allows22 = ufwStatus.includes("22/tcp");
660
+ const allows80 = ufwStatus.includes("80/tcp");
661
+ const allows443 = ufwStatus.includes("443/tcp");
662
+ if (allows22 && allows80 && allows443) {
663
+ checks.push({
664
+ name: "Firewall ports",
665
+ status: "pass",
666
+ message: "Required ports (22, 80, 443) are open"
667
+ });
668
+ } else {
669
+ checks.push({
670
+ name: "Firewall ports",
671
+ status: "warn",
672
+ message: "Some required ports may be blocked",
673
+ fix: "Run: ufw allow 22/tcp && ufw allow 80/tcp && ufw allow 443/tcp"
674
+ });
675
+ }
676
+ }
677
+ return checks;
678
+ }
679
+ function auditServices() {
680
+ const checks = [];
681
+ const fail2banActive = isServiceActive("fail2ban");
682
+ checks.push({
683
+ name: "Fail2ban",
684
+ status: fail2banActive ? "pass" : "warn",
685
+ message: fail2banActive ? "Fail2ban is active" : "Fail2ban is not running",
686
+ fix: "Run: apt install fail2ban && systemctl enable fail2ban --now"
687
+ });
688
+ const unattendedActive = isServiceActive("unattended-upgrades");
689
+ checks.push({
690
+ name: "Automatic updates",
691
+ status: unattendedActive ? "pass" : "warn",
692
+ message: unattendedActive ? "Unattended upgrades enabled" : "Automatic updates not configured",
693
+ fix: "Run: apt install unattended-upgrades && dpkg-reconfigure unattended-upgrades"
694
+ });
695
+ const agentActive = isServiceActive("trafic-agent");
696
+ checks.push({
697
+ name: "Trafic agent",
698
+ status: agentActive ? "pass" : "fail",
699
+ message: agentActive ? "Trafic agent is running" : "Trafic agent is not running",
700
+ fix: "Run: systemctl start trafic-agent"
701
+ });
702
+ return checks;
703
+ }
704
+ function auditDocker() {
705
+ const checks = [];
706
+ const dockerActive = isServiceActive("docker");
707
+ checks.push({
708
+ name: "Docker",
709
+ status: dockerActive ? "pass" : "fail",
710
+ message: dockerActive ? "Docker is running" : "Docker is not running",
711
+ fix: "Run: systemctl start docker"
712
+ });
713
+ const danglingImages = execSilent(
714
+ "docker images -f dangling=true -q | wc -l"
715
+ );
716
+ const count = parseInt(danglingImages, 10) || 0;
717
+ if (count > 0) {
718
+ checks.push({
719
+ name: "Docker cleanup",
720
+ status: "warn",
721
+ message: `${count} dangling Docker images`,
722
+ fix: "Run: docker system prune -f"
723
+ });
724
+ }
725
+ const diskUsage = execSilent("df -h / | awk 'NR==2{print $5}'");
726
+ const usagePercent = parseInt(diskUsage, 10) || 0;
727
+ checks.push({
728
+ name: "Disk usage",
729
+ status: usagePercent > 90 ? "fail" : usagePercent > 80 ? "warn" : "pass",
730
+ message: `Disk usage: ${diskUsage}`,
731
+ fix: usagePercent > 80 ? "Consider cleaning up old projects or Docker resources" : void 0
732
+ });
733
+ return checks;
734
+ }
735
+ function auditPermissions() {
736
+ const checks = [];
737
+ if (existsSync("/etc/trafic/config.toml")) {
738
+ const stats = execSilent("stat -c '%a' /etc/trafic/config.toml");
739
+ const isSecure = stats === "640" || stats === "600";
740
+ checks.push({
741
+ name: "Config permissions",
742
+ status: isSecure ? "pass" : "warn",
743
+ message: isSecure ? "Config file has secure permissions" : `Config file permissions: ${stats} (should be 640)`,
744
+ fix: "Run: chmod 640 /etc/trafic/config.toml"
745
+ });
746
+ }
747
+ if (existsSync("/home/ddev/www")) {
748
+ const owner = execSilent("stat -c '%U:%G' /home/ddev/www");
749
+ const isCorrect = owner === "ddev:ddev";
750
+ checks.push({
751
+ name: "Projects directory",
752
+ status: isCorrect ? "pass" : "warn",
753
+ message: isCorrect ? "Projects directory owned by ddev" : `Projects directory owner: ${owner}`,
754
+ fix: "Run: chown ddev:ddev /home/ddev/www"
755
+ });
756
+ }
757
+ return checks;
758
+ }
759
+ function runAudit() {
760
+ return [
761
+ ...auditSsh(),
762
+ ...auditFirewall(),
763
+ ...auditServices(),
764
+ ...auditDocker(),
765
+ ...auditPermissions()
766
+ ];
767
+ }
768
+ function printAuditResults(checks) {
769
+ console.log("\n\x1B[1mSecurity Audit Results\x1B[0m\n");
770
+ for (const check of checks) {
771
+ const icon = check.status === "pass" ? "\x1B[32m✓\x1B[0m" : check.status === "warn" ? "\x1B[33m⚠\x1B[0m" : "\x1B[31m✗\x1B[0m";
772
+ console.log(`${icon} \x1B[1m${check.name}\x1B[0m: ${check.message}`);
773
+ if (check.fix && check.status !== "pass") {
774
+ console.log(` \x1B[90m${check.fix}\x1B[0m`);
775
+ }
776
+ }
777
+ const passed = checks.filter((c) => c.status === "pass").length;
778
+ const warnings = checks.filter((c) => c.status === "warn").length;
779
+ const failed = checks.filter((c) => c.status === "fail").length;
780
+ console.log(
781
+ `
782
+ \x1B[1mSummary:\x1B[0m ${passed} passed, ${warnings} warnings, ${failed} failed`
783
+ );
784
+ }
785
+ async function setup(options) {
786
+ resetSteps();
787
+ setDryRun(options.dryRun ?? false);
788
+ console.log("\n\x1B[1m🚦 Trafic Server Setup\x1B[0m\n");
789
+ if (options.dryRun) {
790
+ console.log("\x1B[33m⚠ Dry-run mode: no changes will be made\x1B[0m\n");
791
+ }
792
+ if (!isRoot() && !options.dryRun) {
793
+ error("This command must be run as root");
794
+ console.log(" Run: sudo trafic-agent setup ...");
795
+ process.exit(1);
796
+ }
797
+ info(`TLD: ${options.tld}`);
798
+ if (options.email) {
799
+ info(`Let's Encrypt email: ${options.email}`);
800
+ }
801
+ if (options.noHardening) {
802
+ info("Hardening: disabled");
803
+ }
804
+ try {
805
+ createDdevUser();
806
+ if (!options.noDocker) {
807
+ installDocker();
808
+ configureDocker();
809
+ setupDockerPrune();
810
+ } else {
811
+ step("Install Docker");
812
+ info("Skipped (--no-docker)");
813
+ }
814
+ if (!options.noDdev) {
815
+ installDdev();
816
+ configureDdev(options.tld, options.email);
817
+ configureTraefik();
818
+ } else {
819
+ step("Install DDEV");
820
+ info("Skipped (--no-ddev)");
821
+ }
822
+ installNode();
823
+ installAgent();
824
+ createAgentConfig(options.tld, options.email);
825
+ createSystemdService();
826
+ if (!options.noHardening) {
827
+ const sshUsers = options.sshUsers ?? ["ddev"];
828
+ hardenServer(sshUsers);
829
+ } else {
830
+ step("Server hardening");
831
+ info("Skipped (--no-hardening)");
832
+ }
833
+ console.log("\n\x1B[32m✓ Setup complete!\x1B[0m\n");
834
+ console.log("Next steps:");
835
+ console.log(` 1. Edit /etc/trafic/config.toml to configure authentication`);
836
+ console.log(` 2. Add SSH public keys to /home/ddev/.ssh/authorized_keys`);
837
+ console.log(` 3. Deploy your first project:`);
838
+ console.log(` npx @studiometa/trafic-cli deploy --host=<this-server> --name=my-app`);
839
+ console.log(`
840
+ Your projects will be available at https://<name>.${options.tld}`);
841
+ } catch (err) {
842
+ error(`Setup failed: ${err instanceof Error ? err.message : err}`);
843
+ process.exit(1);
844
+ }
845
+ }
846
+ async function audit() {
847
+ const checks = runAudit();
848
+ printAuditResults(checks);
849
+ const failed = checks.filter((c) => c.status === "fail").length;
850
+ if (failed > 0) {
851
+ process.exit(1);
852
+ }
853
+ }
854
+ export {
855
+ audit as a,
856
+ printAuditResults as b,
857
+ startIdleScheduler as c,
858
+ stopIdleProjects as d,
859
+ loadConfig as l,
860
+ parseDuration as p,
861
+ runAudit as r,
862
+ setup as s,
863
+ validateConfig as v
864
+ };
865
+ //# sourceMappingURL=index-C7OaqqbZ.js.map