@studiometa/trafic-agent 0.1.37 → 0.1.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -214,29 +214,52 @@ ddev-router publishes for DDEV's tools:
214
214
  | 8025, 8026 | Mailpit |
215
215
  | 8142, 8143 | xhgui |
216
216
 
217
- Those are protected by **forward auth, not by the firewall** — the middleware
218
- is attached to every entry point ddev-router publishes, so an unauthenticated
219
- request gets `401` on a tool port exactly as it does on 443. An earlier
220
- version attached it only to 80 and 443, which left xhgui answering from the
221
- internet with no authentication at all; that is fixed, and it is the reason
222
- the middleware is attached per entry point rather than per project router.
223
-
224
- If you want those ports closed at the network level rather than answered with
225
- a `401`, two options actually work:
226
-
227
- 1. **A firewall in front of the server.** The packets never reach the host, so
228
- Docker's rules are irrelevant. On OVH dedicated servers this is the Network
229
- Firewall in the control panel — note it is *stateless*, so return traffic
230
- needs allowing explicitly. This is the recommended defence in depth. It is
231
- deliberately not automated: `setup` cannot create or verify it, and a tool
232
- that pretends to configure something it cannot check is worse than one that
233
- tells you to do it yourself.
234
- 2. **Rules in the `DOCKER-USER` chain**, which Docker evaluates first in
235
- `FORWARD` and never overwrites. Match on the pre-DNAT port
236
- (`-m conntrack --ctorigdstport 8025`), because by `FORWARD` the destination
237
- has already been rewritten to the container. Remember these live outside
238
- UFW: they need their own persistence across reboots, an `ip6tables`
239
- equivalent, and they will not show up in `ufw status`.
217
+ Both layers now apply:
218
+
219
+ 1. **`DOCKER-USER` rules**, installed by `setup`. That is the chain Docker
220
+ jumps to first in `FORWARD` and never flushes, so it is the only host-level
221
+ hook that can filter a published port. The rules drop traffic to the tool
222
+ ports unless it comes from Docker's own networks. They match on the
223
+ *original* destination port (`-m conntrack --ctorigdstport`), because by
224
+ `FORWARD` the destination has already been rewritten to the container.
225
+
226
+ They live in `/usr/local/sbin/trafic-docker-firewall`, reapplied at boot and
227
+ after any Docker restart by `trafic-docker-firewall.service` — Docker
228
+ recreates `DOCKER-USER` empty and nothing else would restore them.
229
+ **They will not appear in `ufw status`.** Inspect them with
230
+ `iptables -S DOCKER-USER`.
231
+
232
+ 2. **Forward auth**, attached to every entry point ddev-router publishes, so a
233
+ request that does reach a tool port still needs credentials. An earlier
234
+ version attached it only to 80 and 443, which left xhgui answering from the
235
+ internet with no authentication at all.
236
+
237
+ Access from the host itself is unaffected: a connection to `127.0.0.1:8026`
238
+ goes through Docker's userland proxy rather than `FORWARD`, so an SSH tunnel
239
+ still works:
240
+
241
+ ```bash
242
+ ssh -L 8026:127.0.0.1:8026 ddev@server.example.com
243
+ # then open https://localhost:8026
244
+ ```
245
+
246
+ ### A provider firewall is not a substitute
247
+
248
+ Recommended as an extra layer, but do not rely on it alone. Measured on an OVH
249
+ dedicated server with the Edge Firewall enabled and a correct rule denying
250
+ these ports: a connection from **another host inside OVH** still completed in
251
+ 113ms, while a port with nothing listening was dropped and timed out at 12s.
252
+ The deny applies to traffic crossing the provider's edge; traffic that never
253
+ crosses it is not filtered.
254
+
255
+ Anyone able to rent a VM from the same provider is inside that blind spot,
256
+ which for a large host is a very low bar. That is why the `DOCKER-USER` rules
257
+ above exist: they apply to every packet regardless of origin.
258
+
259
+ On OVH the setting is the Network Firewall in the control panel. Two things to
260
+ know: it is *stateless*, so `permit tcp established` is required or return
261
+ traffic for outbound connections is dropped, and it is IPv4-only. `setup` does
262
+ not configure it — it can neither create nor verify it.
240
263
 
241
264
  Binding ddev-router to loopback only (`router-bind-all-interfaces=false`)
242
265
  closes everything at once, but then nothing serves the public and you need a
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { d as validateConfig, i as runPendingMigrations, l as loadConfig, n as setup, r as listMigrations, s as startIdleScheduler, t as audit } from "./setup-DHhQF7wW.js";
3
- import { T as closeDb, b as warn, f as exec, h as isRoot, m as info, n as startServer, v as step, y as success } from "./server-CLrB1zpR.js";
2
+ import { g as validateConfig, i as runPendingMigrations, m as loadConfig, n as setup, r as listMigrations, s as startIdleScheduler, t as audit } from "./setup-DxoFhMfT.js";
3
+ import { L as step, N as info, P as isRoot, R as success, b as closeDb, j as exec, s as startServer, z as warn } from "./server-BAjHxgqh.js";
4
4
  import { readFileSync } from "node:fs";
5
5
  import { execSync, spawnSync } from "node:child_process";
6
6
  import { parseArgs } from "node:util";
@@ -92,6 +92,20 @@ function reExecNewBinary(args) {
92
92
  function restartAgentService(dryRun) {
93
93
  exec("systemctl restart trafic-agent", { silent: !dryRun });
94
94
  }
95
+ /** The real collaborators, used unless a caller passes their own. */
96
+ function nodeUpgradeIo() {
97
+ return {
98
+ currentVersion: "0.1.39",
99
+ alreadyReExeced: process.env["TRAFIC_UPGRADE_REEXEC"] === "1",
100
+ isRoot,
101
+ fetchLatestVersion,
102
+ installLatestAgent,
103
+ getInstalledVersion,
104
+ reExecNewBinary,
105
+ restartAgentService,
106
+ runPendingMigrations
107
+ };
108
+ }
95
109
  /**
96
110
  * Full upgrade sequence:
97
111
  * 1. Check for a new version on npm
@@ -100,34 +114,34 @@ function restartAgentService(dryRun) {
100
114
  * 3. Run pending migrations
101
115
  * 4. Restart the systemd service
102
116
  */
103
- function runUpgrade(dryRun = false, reExecArgs) {
104
- if (!isRoot() && !dryRun) {
117
+ function runUpgrade(dryRun = false, reExecArgs, io = nodeUpgradeIo()) {
118
+ if (!io.isRoot() && !dryRun) {
105
119
  console.error(" \x1B[31m✗\x1B[0m This command must be run as root");
106
120
  console.log(" Run: sudo trafic-agent upgrade");
107
121
  process.exit(1);
108
122
  }
109
- const alreadyReExeced = process.env["TRAFIC_UPGRADE_REEXEC"] === "1";
123
+ const alreadyReExeced = io.alreadyReExeced;
110
124
  step("Check for updates");
111
- const current = "0.1.37";
125
+ const current = io.currentVersion;
112
126
  info(`Current version: ${current}`);
113
- const latest = fetchLatestVersion();
127
+ const latest = io.fetchLatestVersion();
114
128
  if (!latest) warn("Could not reach npm registry — skipping version check");
115
129
  else if (isNewer(current, latest)) {
116
130
  success(`New version available: ${latest}`);
117
131
  step("Install latest version");
118
- installLatestAgent(dryRun);
132
+ io.installLatestAgent(dryRun);
119
133
  if (!dryRun) {
120
- const installedVersion = getInstalledVersion();
134
+ const installedVersion = io.getInstalledVersion();
121
135
  if (!alreadyReExeced && installedVersion && isNewer(current, installedVersion)) {
122
136
  success(`Installed @studiometa/trafic-agent@${installedVersion}`);
123
- reExecNewBinary(reExecArgs ?? ["upgrade"]);
137
+ io.reExecNewBinary(reExecArgs ?? ["upgrade"]);
124
138
  } else if (installedVersion) success(`Installed @studiometa/trafic-agent@${installedVersion}`);
125
139
  }
126
140
  } else success(`Already up to date (${current})`);
127
141
  step("Run pending migrations");
128
- runPendingMigrations(dryRun);
142
+ io.runPendingMigrations(dryRun);
129
143
  step("Restart trafic-agent service");
130
- restartAgentService(dryRun);
144
+ io.restartAgentService(dryRun);
131
145
  if (!dryRun) success("Service restarted");
132
146
  }
133
147
  //#endregion
@@ -191,7 +205,7 @@ function printHelp() {
191
205
  console.log(HELP.trim());
192
206
  }
193
207
  function printVersion() {
194
- console.log("0.1.37");
208
+ console.log("0.1.39");
195
209
  }
196
210
  async function runStart(values) {
197
211
  const config = loadConfig(values.config);
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":[],"sources":["../src/setup/upgrade.ts","../src/cli.ts"],"sourcesContent":["import { execSync, spawnSync } from \"node:child_process\";\nimport { readFileSync } from \"node:fs\";\nimport { step, success, info, warn, exec, isRoot } from \"./steps.js\";\nimport { runPendingMigrations } from \"./migrations/index.js\";\n\ndeclare const __VERSION__: string;\n\n/**\n * Fetch the latest published version of @studiometa/trafic-agent from the npm registry.\n * Returns null if the registry is unreachable.\n */\nexport function fetchLatestVersion(): string | null {\n try {\n return execSync(\"npm show @studiometa/trafic-agent version\", {\n encoding: \"utf-8\",\n stdio: \"pipe\",\n }).trim();\n } catch {\n return null;\n }\n}\n\n/**\n * Compare two semver strings. Returns true if `next` is strictly greater than `current`.\n * Handles simple MAJOR.MINOR.PATCH without pre-release suffixes.\n */\nexport function isNewer(current: string, next: string): boolean {\n const parse = (v: string): [number, number, number] => {\n const parts = v.replace(/^v/, \"\").split(\".\").map(Number);\n return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];\n };\n\n const [cMaj, cMin, cPat] = parse(current);\n const [nMaj, nMin, nPat] = parse(next);\n\n if (nMaj !== cMaj) return nMaj > cMaj;\n if (nMin !== cMin) return nMin > cMin;\n return nPat > cPat;\n}\n\n/**\n * Install the latest version of @studiometa/trafic-agent globally via npm.\n * Uses --prefer-online to bypass the local npm cache and always fetch the\n * latest published tarball.\n */\nexport function installLatestAgent(dryRun: boolean): void {\n exec(\"npm install -g @studiometa/trafic-agent@latest --prefer-online\", { silent: !dryRun });\n}\n\n/**\n * Read the actual installed version of @studiometa/trafic-agent from its\n * package.json on disk — not from __VERSION__ which is baked in at build time.\n *\n * Resolves the package root by walking up from the trafic-agent binary:\n * /usr/bin/trafic-agent -> /usr/lib/node_modules/@studiometa/trafic-agent/dist/cli.js\n * -> /usr/lib/node_modules/@studiometa/trafic-agent/package.json\n *\n * Returns null if the file can't be read.\n */\nexport function getInstalledVersion(): string | null {\n try {\n // Resolve the real path of the binary (follow symlinks)\n const realBinary = execSync(\"readlink -f $(which trafic-agent)\", {\n encoding: \"utf-8\",\n stdio: \"pipe\",\n }).trim();\n // dist/cli.js -> up two dirs -> package root\n const pkgJson = `${realBinary.replace(/\\/dist\\/cli\\.js$/, \"\")}/package.json`;\n const pkg = JSON.parse(readFileSync(pkgJson, \"utf-8\")) as { version: string };\n return pkg.version;\n } catch {\n return null;\n }\n}\n\n/**\n * Re-exec the newly installed trafic-agent binary with the same arguments.\n * Used after a self-update so the new binary runs its own migrations.\n * This function never returns — it replaces the current process.\n */\nexport function reExecNewBinary(args: string[]): never {\n const binary = execSync(\"which trafic-agent\", { encoding: \"utf-8\", stdio: \"pipe\" }).trim();\n const result = spawnSync(binary, args, {\n stdio: \"inherit\",\n env: { ...process.env, TRAFIC_UPGRADE_REEXEC: \"1\" },\n });\n process.exit(result.status ?? 0);\n}\n\n/**\n * Restart the trafic-agent systemd service.\n */\nexport function restartAgentService(dryRun: boolean): void {\n exec(\"systemctl restart trafic-agent\", { silent: !dryRun });\n}\n\n/**\n * Full upgrade sequence:\n * 1. Check for a new version on npm\n * 2. Install it globally if one is found — then re-exec the new binary\n * so that steps 3 and 4 run with the new migration registry\n * 3. Run pending migrations\n * 4. Restart the systemd service\n */\nexport function runUpgrade(dryRun = false, reExecArgs?: string[]): void {\n if (!isRoot() && !dryRun) {\n console.error(\" \\x1b[31m✗\\x1b[0m This command must be run as root\");\n console.log(\" Run: sudo trafic-agent upgrade\");\n process.exit(1);\n }\n\n // Guard against infinite re-exec loops: only re-exec once per upgrade run.\n const alreadyReExeced = process.env[\"TRAFIC_UPGRADE_REEXEC\"] === \"1\";\n\n // ── Step 1: Check for updates ─────────────────────────────────────────────\n step(\"Check for updates\");\n\n const current = __VERSION__;\n info(`Current version: ${current}`);\n\n const latest = fetchLatestVersion();\n\n if (!latest) {\n warn(\"Could not reach npm registry — skipping version check\");\n } else if (isNewer(current, latest)) {\n success(`New version available: ${latest}`);\n\n // ── Step 2: Install ───────────────────────────────────────────────────\n step(\"Install latest version\");\n installLatestAgent(dryRun);\n\n if (!dryRun) {\n // Verify the installed version actually changed before re-execing —\n // npm can serve stale cache and leave the old binary in place.\n const installedVersion = getInstalledVersion();\n\n if (!alreadyReExeced && installedVersion && isNewer(current, installedVersion)) {\n success(`Installed @studiometa/trafic-agent@${installedVersion}`);\n // Re-exec the newly installed binary so steps 3 and 4 run with the\n // new migration registry — the current process only knows about\n // migrations that existed at the time it was compiled.\n reExecNewBinary(reExecArgs ?? [\"upgrade\"]);\n } else if (installedVersion) {\n success(`Installed @studiometa/trafic-agent@${installedVersion}`);\n }\n }\n } else {\n success(`Already up to date (${current})`);\n }\n\n // ── Step 3: Run pending migrations ───────────────────────────────────────\n step(\"Run pending migrations\");\n runPendingMigrations(dryRun);\n\n // ── Step 4: Restart service ───────────────────────────────────────────────\n step(\"Restart trafic-agent service\");\n restartAgentService(dryRun);\n if (!dryRun) {\n success(\"Service restarted\");\n }\n}\n","#!/usr/bin/env node\n\nimport { parseArgs } from \"node:util\";\nimport { loadConfig, validateConfig } from \"./utils/config.js\";\nimport { startServer } from \"./server.js\";\nimport { startIdleScheduler } from \"./tasks/stop-idle.js\";\nimport { closeDb } from \"./utils/db.js\";\nimport { setup, audit } from \"./setup/index.js\";\nimport { listMigrations } from \"./setup/migrations/index.js\";\nimport { runUpgrade } from \"./setup/upgrade.js\";\n\ndeclare const __VERSION__: string;\n\nconst HELP = `\ntrafic-agent — Server agent for DDEV preview environments\n\nUsage:\n trafic-agent <command> [options]\n\nCommands:\n start Start the agent server\n setup Setup a new server (Docker, DDEV, hardening)\n upgrade Check for a new version, install it, run migrations, restart the service\n update Alias for upgrade\n audit Run security audit checks\n version Show version\n help Show this help\n\nStart options:\n -c, --config <path> Path to config file (default: /etc/trafic/config.toml)\n -p, --port <port> Override port from config\n\nSetup options:\n --tld <domain> TLD for DDEV projects (required on first run, reused from config on re-runs)\n --email <email> Email for Let's Encrypt certificates\n --no-hardening Skip server hardening steps\n --no-root-ssh Disable root SSH login (recovery via rescue mode only)\n --no-docker Skip Docker installation\n --no-ddev Skip DDEV installation\n --ssh-users <users> Comma-separated list of SSH users to allow (default: ddev)\n --trusted-proxy-hops <n> Proxies in front of the agent (default: 1; 2 behind a CDN).\n Written to the generated config; ignored if one exists\n --dry-run Show what would be done without making changes\n\nUpgrade/update options:\n --dry-run Show what would be done without making changes\n --list List all migrations and their status (no install or restart)\n\nExamples:\n # Start the agent\n trafic-agent start\n trafic-agent start --config /path/to/config.toml\n\n # Setup a new server\n sudo trafic-agent setup --tld=previews.example.com\n sudo trafic-agent setup --tld=previews.example.com --trusted-proxy-hops=2\n sudo trafic-agent setup --tld=previews.example.com --email=admin@example.com\n sudo trafic-agent setup --tld=previews.example.com --no-hardening --dry-run\n\n # Upgrade: install latest version, run migrations, restart service\n sudo trafic-agent upgrade\n sudo trafic-agent upgrade --dry-run\n trafic-agent upgrade --list\n sudo trafic-agent update # alias for upgrade\n\n # Run security audit\n trafic-agent audit\n`;\n\nfunction printHelp(): void {\n console.log(HELP.trim());\n}\n\nfunction printVersion(): void {\n console.log(__VERSION__);\n}\n\nasync function runStart(values: Record<string, unknown>): Promise<void> {\n // Load config\n const config = loadConfig(values.config as string | undefined);\n\n // Override port if specified\n if (values.port) {\n config.port = parseInt(values.port as string, 10);\n }\n\n // Validate config\n const errors = validateConfig(config);\n if (errors.length > 0) {\n console.error(\"Configuration errors:\");\n for (const error of errors) {\n console.error(` - ${error}`);\n }\n process.exit(1);\n }\n\n // Graceful shutdown\n const shutdown = (): void => {\n console.log(\"\\nShutting down...\");\n closeDb();\n process.exit(0);\n };\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n\n // Start server and scheduler\n startServer(config);\n startIdleScheduler(config);\n}\n\nfunction handleUpgrade(values: Record<string, unknown>): void {\n const dryRun = values[\"dry-run\"] as boolean | undefined;\n const list = values.list as boolean | undefined;\n\n if (list) {\n const entries = listMigrations();\n console.log(\"\\nMigrations:\\n\");\n\n for (const { migration, status } of entries) {\n const icon = status === \"applied\" ? \"\\x1b[32m✓\\x1b[0m\" : \"\\x1b[36m→\\x1b[0m\";\n const label = status === \"applied\" ? \"\\x1b[90m(applied)\\x1b[0m\" : \"(pending)\";\n console.log(` ${icon} \\x1b[1m${migration.id.padEnd(30)}\\x1b[0m ${migration.description.padEnd(55)} ${label}`);\n }\n\n console.log(\"\");\n return;\n }\n\n console.log(\"\\n\\x1b[1m🚦 Trafic Server Upgrade\\x1b[0m\\n\");\n\n if (dryRun) {\n console.log(\"\\x1b[33m⚠ Dry-run mode: no changes will be made\\x1b[0m\\n\");\n }\n\n // Build re-exec args so flags survive the binary swap after a self-update\n const reExecArgs = [\"upgrade\", ...(dryRun ? [\"--dry-run\"] : [])];\n runUpgrade(dryRun ?? false, reExecArgs);\n\n if (!dryRun) {\n console.log(\"\\n\\x1b[32m✓ Upgrade complete!\\x1b[0m\\n\");\n }\n}\n\n/**\n * Parse --trusted-proxy-hops, failing loudly rather than writing a config\n * that would quietly break the IP allowlist.\n */\nfunction parseTrustedProxyHops(raw?: string): number | undefined {\n if (raw === undefined) {\n return undefined;\n }\n\n const hops = Number(raw);\n\n if (!Number.isInteger(hops) || hops < 1) {\n console.error(\n `Error: --trusted-proxy-hops must be a whole number of 1 or more, got \"${raw}\"`,\n );\n console.error(\" 1 = ddev-router/Traefik alone, 2 = a CDN in front of it\");\n process.exit(1);\n }\n\n return hops;\n}\n\nasync function runSetup(values: Record<string, unknown>): Promise<void> {\n // Load existing config to use as defaults on re-runs\n const existingConfig = loadConfig();\n const tld = (values.tld as string | undefined) ?? (existingConfig.tld || undefined);\n\n if (!tld) {\n console.error(\"Error: --tld is required for setup\");\n console.error(\"Example: trafic-agent setup --tld=previews.example.com\");\n process.exit(1);\n }\n\n await setup({\n tld,\n trustedProxyHops: parseTrustedProxyHops(values[\"trusted-proxy-hops\"] as string | undefined),\n email: values.email as string | undefined,\n noHardening: values[\"no-hardening\"] as boolean | undefined,\n noRootSsh: values[\"no-root-ssh\"] as boolean | undefined,\n noDocker: values[\"no-docker\"] as boolean | undefined,\n noDdev: values[\"no-ddev\"] as boolean | undefined,\n sshUsers: values[\"ssh-users\"]\n ? (values[\"ssh-users\"] as string).split(\",\").map((s) => s.trim())\n : undefined,\n dryRun: values[\"dry-run\"] as boolean | undefined,\n });\n}\n\nasync function main(): Promise<void> {\n const { values, positionals } = parseArgs({\n allowPositionals: true,\n options: {\n // Common\n help: { type: \"boolean\", short: \"h\" },\n\n // Start options\n config: { type: \"string\", short: \"c\" },\n port: { type: \"string\", short: \"p\" },\n\n // Setup options\n tld: { type: \"string\" },\n email: { type: \"string\" },\n \"no-hardening\": { type: \"boolean\" },\n \"no-root-ssh\": { type: \"boolean\" },\n \"no-docker\": { type: \"boolean\" },\n \"no-ddev\": { type: \"boolean\" },\n \"ssh-users\": { type: \"string\" },\n \"trusted-proxy-hops\": { type: \"string\" },\n \"dry-run\": { type: \"boolean\" },\n\n // Upgrade options\n list: { type: \"boolean\" },\n },\n });\n\n const command = positionals[0] ?? \"help\";\n\n if (values.help) {\n printHelp();\n process.exit(0);\n }\n\n switch (command) {\n case \"help\":\n printHelp();\n break;\n\n case \"version\":\n printVersion();\n break;\n\n case \"start\":\n await runStart(values);\n break;\n\n case \"setup\":\n await runSetup(values);\n break;\n\n case \"upgrade\":\n case \"update\":\n handleUpgrade(values);\n break;\n\n case \"audit\":\n await audit();\n break;\n\n default:\n console.error(`Unknown command: ${command}`);\n printHelp();\n process.exit(1);\n }\n}\n\nmain().catch((error) => {\n console.error(\"Fatal error:\", error);\n process.exit(1);\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/setup/upgrade.ts","../src/cli.ts"],"sourcesContent":["import { execSync, spawnSync } from \"node:child_process\";\nimport { readFileSync } from \"node:fs\";\nimport { step, success, info, warn, exec, isRoot } from \"./steps.js\";\nimport { runPendingMigrations } from \"./migrations/index.js\";\n\ndeclare const __VERSION__: string;\n\n/**\n * Fetch the latest published version of @studiometa/trafic-agent from the npm registry.\n * Returns null if the registry is unreachable.\n */\nexport function fetchLatestVersion(): string | null {\n try {\n return execSync(\"npm show @studiometa/trafic-agent version\", {\n encoding: \"utf-8\",\n stdio: \"pipe\",\n }).trim();\n } catch {\n return null;\n }\n}\n\n/**\n * Compare two semver strings. Returns true if `next` is strictly greater than `current`.\n * Handles simple MAJOR.MINOR.PATCH without pre-release suffixes.\n */\nexport function isNewer(current: string, next: string): boolean {\n const parse = (v: string): [number, number, number] => {\n const parts = v.replace(/^v/, \"\").split(\".\").map(Number);\n return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];\n };\n\n const [cMaj, cMin, cPat] = parse(current);\n const [nMaj, nMin, nPat] = parse(next);\n\n if (nMaj !== cMaj) return nMaj > cMaj;\n if (nMin !== cMin) return nMin > cMin;\n return nPat > cPat;\n}\n\n/**\n * Install the latest version of @studiometa/trafic-agent globally via npm.\n * Uses --prefer-online to bypass the local npm cache and always fetch the\n * latest published tarball.\n */\nexport function installLatestAgent(dryRun: boolean): void {\n exec(\"npm install -g @studiometa/trafic-agent@latest --prefer-online\", { silent: !dryRun });\n}\n\n/**\n * Read the actual installed version of @studiometa/trafic-agent from its\n * package.json on disk — not from __VERSION__ which is baked in at build time.\n *\n * Resolves the package root by walking up from the trafic-agent binary:\n * /usr/bin/trafic-agent -> /usr/lib/node_modules/@studiometa/trafic-agent/dist/cli.js\n * -> /usr/lib/node_modules/@studiometa/trafic-agent/package.json\n *\n * Returns null if the file can't be read.\n */\nexport function getInstalledVersion(): string | null {\n try {\n // Resolve the real path of the binary (follow symlinks)\n const realBinary = execSync(\"readlink -f $(which trafic-agent)\", {\n encoding: \"utf-8\",\n stdio: \"pipe\",\n }).trim();\n // dist/cli.js -> up two dirs -> package root\n const pkgJson = `${realBinary.replace(/\\/dist\\/cli\\.js$/, \"\")}/package.json`;\n const pkg = JSON.parse(readFileSync(pkgJson, \"utf-8\")) as { version: string };\n return pkg.version;\n } catch {\n return null;\n }\n}\n\n/**\n * Re-exec the newly installed trafic-agent binary with the same arguments.\n * Used after a self-update so the new binary runs its own migrations.\n * This function never returns — it replaces the current process.\n */\nexport function reExecNewBinary(args: string[]): never {\n const binary = execSync(\"which trafic-agent\", { encoding: \"utf-8\", stdio: \"pipe\" }).trim();\n const result = spawnSync(binary, args, {\n stdio: \"inherit\",\n env: { ...process.env, TRAFIC_UPGRADE_REEXEC: \"1\" },\n });\n process.exit(result.status ?? 0);\n}\n\n/**\n * Restart the trafic-agent systemd service.\n */\nexport function restartAgentService(dryRun: boolean): void {\n exec(\"systemctl restart trafic-agent\", { silent: !dryRun });\n}\n\n/**\n * What the upgrade sequence needs from the outside world.\n *\n * Injected so the orchestration can be tested without reaching npm, writing\n * to the filesystem or replacing the running process — the last of which is\n * why `reExecNewBinary` is here rather than called directly.\n */\nexport interface UpgradeIo {\n /** The version of the process running the upgrade. */\n currentVersion: string;\n /** Whether an earlier run already re-execed, guarding against a loop. */\n alreadyReExeced: boolean;\n isRoot: () => boolean;\n fetchLatestVersion: () => string | null;\n installLatestAgent: (dryRun: boolean) => void;\n getInstalledVersion: () => string | null;\n reExecNewBinary: (args: string[]) => never;\n restartAgentService: (dryRun: boolean) => void;\n runPendingMigrations: (dryRun: boolean) => void;\n}\n\n/** The real collaborators, used unless a caller passes their own. */\nexport function nodeUpgradeIo(): UpgradeIo {\n return {\n currentVersion: __VERSION__,\n alreadyReExeced: process.env[\"TRAFIC_UPGRADE_REEXEC\"] === \"1\",\n isRoot,\n fetchLatestVersion,\n installLatestAgent,\n getInstalledVersion,\n reExecNewBinary,\n restartAgentService,\n runPendingMigrations,\n };\n}\n\n/**\n * Full upgrade sequence:\n * 1. Check for a new version on npm\n * 2. Install it globally if one is found — then re-exec the new binary\n * so that steps 3 and 4 run with the new migration registry\n * 3. Run pending migrations\n * 4. Restart the systemd service\n */\nexport function runUpgrade(\n dryRun = false,\n reExecArgs?: string[],\n io: UpgradeIo = nodeUpgradeIo(),\n): void {\n if (!io.isRoot() && !dryRun) {\n console.error(\" \\x1b[31m✗\\x1b[0m This command must be run as root\");\n console.log(\" Run: sudo trafic-agent upgrade\");\n process.exit(1);\n }\n\n // Guard against infinite re-exec loops: only re-exec once per upgrade run.\n const alreadyReExeced = io.alreadyReExeced;\n\n // ── Step 1: Check for updates ─────────────────────────────────────────────\n step(\"Check for updates\");\n\n const current = io.currentVersion;\n info(`Current version: ${current}`);\n\n const latest = io.fetchLatestVersion();\n\n if (!latest) {\n warn(\"Could not reach npm registry — skipping version check\");\n } else if (isNewer(current, latest)) {\n success(`New version available: ${latest}`);\n\n // ── Step 2: Install ───────────────────────────────────────────────────\n step(\"Install latest version\");\n io.installLatestAgent(dryRun);\n\n if (!dryRun) {\n // Verify the installed version actually changed before re-execing —\n // npm can serve stale cache and leave the old binary in place.\n const installedVersion = io.getInstalledVersion();\n\n if (!alreadyReExeced && installedVersion && isNewer(current, installedVersion)) {\n success(`Installed @studiometa/trafic-agent@${installedVersion}`);\n // Re-exec the newly installed binary so steps 3 and 4 run with the\n // new migration registry — the current process only knows about\n // migrations that existed at the time it was compiled.\n io.reExecNewBinary(reExecArgs ?? [\"upgrade\"]);\n } else if (installedVersion) {\n success(`Installed @studiometa/trafic-agent@${installedVersion}`);\n }\n }\n } else {\n success(`Already up to date (${current})`);\n }\n\n // ── Step 3: Run pending migrations ───────────────────────────────────────\n step(\"Run pending migrations\");\n io.runPendingMigrations(dryRun);\n\n // ── Step 4: Restart service ───────────────────────────────────────────────\n step(\"Restart trafic-agent service\");\n io.restartAgentService(dryRun);\n if (!dryRun) {\n success(\"Service restarted\");\n }\n}\n","#!/usr/bin/env node\n\nimport { parseArgs } from \"node:util\";\nimport { loadConfig, validateConfig } from \"./utils/config.js\";\nimport { startServer } from \"./server.js\";\nimport { startIdleScheduler } from \"./tasks/stop-idle.js\";\nimport { closeDb } from \"./utils/db.js\";\nimport { setup, audit } from \"./setup/index.js\";\nimport { listMigrations } from \"./setup/migrations/index.js\";\nimport { runUpgrade } from \"./setup/upgrade.js\";\n\ndeclare const __VERSION__: string;\n\nconst HELP = `\ntrafic-agent — Server agent for DDEV preview environments\n\nUsage:\n trafic-agent <command> [options]\n\nCommands:\n start Start the agent server\n setup Setup a new server (Docker, DDEV, hardening)\n upgrade Check for a new version, install it, run migrations, restart the service\n update Alias for upgrade\n audit Run security audit checks\n version Show version\n help Show this help\n\nStart options:\n -c, --config <path> Path to config file (default: /etc/trafic/config.toml)\n -p, --port <port> Override port from config\n\nSetup options:\n --tld <domain> TLD for DDEV projects (required on first run, reused from config on re-runs)\n --email <email> Email for Let's Encrypt certificates\n --no-hardening Skip server hardening steps\n --no-root-ssh Disable root SSH login (recovery via rescue mode only)\n --no-docker Skip Docker installation\n --no-ddev Skip DDEV installation\n --ssh-users <users> Comma-separated list of SSH users to allow (default: ddev)\n --trusted-proxy-hops <n> Proxies in front of the agent (default: 1; 2 behind a CDN).\n Written to the generated config; ignored if one exists\n --dry-run Show what would be done without making changes\n\nUpgrade/update options:\n --dry-run Show what would be done without making changes\n --list List all migrations and their status (no install or restart)\n\nExamples:\n # Start the agent\n trafic-agent start\n trafic-agent start --config /path/to/config.toml\n\n # Setup a new server\n sudo trafic-agent setup --tld=previews.example.com\n sudo trafic-agent setup --tld=previews.example.com --trusted-proxy-hops=2\n sudo trafic-agent setup --tld=previews.example.com --email=admin@example.com\n sudo trafic-agent setup --tld=previews.example.com --no-hardening --dry-run\n\n # Upgrade: install latest version, run migrations, restart service\n sudo trafic-agent upgrade\n sudo trafic-agent upgrade --dry-run\n trafic-agent upgrade --list\n sudo trafic-agent update # alias for upgrade\n\n # Run security audit\n trafic-agent audit\n`;\n\nfunction printHelp(): void {\n console.log(HELP.trim());\n}\n\nfunction printVersion(): void {\n console.log(__VERSION__);\n}\n\nasync function runStart(values: Record<string, unknown>): Promise<void> {\n // Load config\n const config = loadConfig(values.config as string | undefined);\n\n // Override port if specified\n if (values.port) {\n config.port = parseInt(values.port as string, 10);\n }\n\n // Validate config\n const errors = validateConfig(config);\n if (errors.length > 0) {\n console.error(\"Configuration errors:\");\n for (const error of errors) {\n console.error(` - ${error}`);\n }\n process.exit(1);\n }\n\n // Graceful shutdown\n const shutdown = (): void => {\n console.log(\"\\nShutting down...\");\n closeDb();\n process.exit(0);\n };\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n\n // Start server and scheduler\n startServer(config);\n startIdleScheduler(config);\n}\n\nfunction handleUpgrade(values: Record<string, unknown>): void {\n const dryRun = values[\"dry-run\"] as boolean | undefined;\n const list = values.list as boolean | undefined;\n\n if (list) {\n const entries = listMigrations();\n console.log(\"\\nMigrations:\\n\");\n\n for (const { migration, status } of entries) {\n const icon = status === \"applied\" ? \"\\x1b[32m✓\\x1b[0m\" : \"\\x1b[36m→\\x1b[0m\";\n const label = status === \"applied\" ? \"\\x1b[90m(applied)\\x1b[0m\" : \"(pending)\";\n console.log(` ${icon} \\x1b[1m${migration.id.padEnd(30)}\\x1b[0m ${migration.description.padEnd(55)} ${label}`);\n }\n\n console.log(\"\");\n return;\n }\n\n console.log(\"\\n\\x1b[1m🚦 Trafic Server Upgrade\\x1b[0m\\n\");\n\n if (dryRun) {\n console.log(\"\\x1b[33m⚠ Dry-run mode: no changes will be made\\x1b[0m\\n\");\n }\n\n // Build re-exec args so flags survive the binary swap after a self-update\n const reExecArgs = [\"upgrade\", ...(dryRun ? [\"--dry-run\"] : [])];\n runUpgrade(dryRun ?? false, reExecArgs);\n\n if (!dryRun) {\n console.log(\"\\n\\x1b[32m✓ Upgrade complete!\\x1b[0m\\n\");\n }\n}\n\n/**\n * Parse --trusted-proxy-hops, failing loudly rather than writing a config\n * that would quietly break the IP allowlist.\n */\nfunction parseTrustedProxyHops(raw?: string): number | undefined {\n if (raw === undefined) {\n return undefined;\n }\n\n const hops = Number(raw);\n\n if (!Number.isInteger(hops) || hops < 1) {\n console.error(\n `Error: --trusted-proxy-hops must be a whole number of 1 or more, got \"${raw}\"`,\n );\n console.error(\" 1 = ddev-router/Traefik alone, 2 = a CDN in front of it\");\n process.exit(1);\n }\n\n return hops;\n}\n\nasync function runSetup(values: Record<string, unknown>): Promise<void> {\n // Load existing config to use as defaults on re-runs\n const existingConfig = loadConfig();\n const tld = (values.tld as string | undefined) ?? (existingConfig.tld || undefined);\n\n if (!tld) {\n console.error(\"Error: --tld is required for setup\");\n console.error(\"Example: trafic-agent setup --tld=previews.example.com\");\n process.exit(1);\n }\n\n await setup({\n tld,\n trustedProxyHops: parseTrustedProxyHops(values[\"trusted-proxy-hops\"] as string | undefined),\n email: values.email as string | undefined,\n noHardening: values[\"no-hardening\"] as boolean | undefined,\n noRootSsh: values[\"no-root-ssh\"] as boolean | undefined,\n noDocker: values[\"no-docker\"] as boolean | undefined,\n noDdev: values[\"no-ddev\"] as boolean | undefined,\n sshUsers: values[\"ssh-users\"]\n ? (values[\"ssh-users\"] as string).split(\",\").map((s) => s.trim())\n : undefined,\n dryRun: values[\"dry-run\"] as boolean | undefined,\n });\n}\n\nasync function main(): Promise<void> {\n const { values, positionals } = parseArgs({\n allowPositionals: true,\n options: {\n // Common\n help: { type: \"boolean\", short: \"h\" },\n\n // Start options\n config: { type: \"string\", short: \"c\" },\n port: { type: \"string\", short: \"p\" },\n\n // Setup options\n tld: { type: \"string\" },\n email: { type: \"string\" },\n \"no-hardening\": { type: \"boolean\" },\n \"no-root-ssh\": { type: \"boolean\" },\n \"no-docker\": { type: \"boolean\" },\n \"no-ddev\": { type: \"boolean\" },\n \"ssh-users\": { type: \"string\" },\n \"trusted-proxy-hops\": { type: \"string\" },\n \"dry-run\": { type: \"boolean\" },\n\n // Upgrade options\n list: { type: \"boolean\" },\n },\n });\n\n const command = positionals[0] ?? \"help\";\n\n if (values.help) {\n printHelp();\n process.exit(0);\n }\n\n switch (command) {\n case \"help\":\n printHelp();\n break;\n\n case \"version\":\n printVersion();\n break;\n\n case \"start\":\n await runStart(values);\n break;\n\n case \"setup\":\n await runSetup(values);\n break;\n\n case \"upgrade\":\n case \"update\":\n handleUpgrade(values);\n break;\n\n case \"audit\":\n await audit();\n break;\n\n default:\n console.error(`Unknown command: ${command}`);\n printHelp();\n process.exit(1);\n }\n}\n\nmain().catch((error) => {\n console.error(\"Fatal error:\", error);\n process.exit(1);\n});\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export type { AgentConfig, AuthConfig, AuthRule, AuthRequest, AuthResult, DdevPr
2
2
  export { loadConfig, validateConfig, parseDuration } from "./utils/config.js";
3
3
  export { checkAuth, matchIp, matchHostname, parseBasicAuth, parseBearerToken, } from "./utils/auth.js";
4
4
  export { loadProjectList, buildHostnameIndex, getProjectFromHostname, watchProjectList, getProjectInfo, startProject, stopProject, listProjects, } from "./utils/ddev.js";
5
+ export { buildDockerFirewallScript, buildDockerFirewallUnit, configureDockerFirewall, DOCKER_FIREWALL_SCRIPT, DOCKER_FIREWALL_UNIT, } from "./setup/docker-firewall.js";
5
6
  export { initDb, getDb, closeDb, updateProjectAccess, getProject, setProjectStatus, getIdleProjects, logAccess, getAccessLogs, cleanOldLogs, } from "./utils/db.js";
6
7
  export { loadProjectConfig, shouldNeverStop, getIdleTimeoutMs, } from "./utils/project-config.js";
7
8
  export type { ProjectConfig } from "./utils/project-config.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,WAAW,EACX,UAAU,EACV,QAAQ,EACR,WAAW,EACX,UAAU,EACV,WAAW,EACX,aAAa,EACb,SAAS,GACV,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAG9E,OAAO,EACL,SAAS,EACT,OAAO,EACP,aAAa,EACb,cAAc,EACd,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,sBAAsB,EACtB,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,WAAW,EACX,YAAY,GACb,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,MAAM,EACN,KAAK,EACL,OAAO,EACP,mBAAmB,EACnB,UAAU,EACV,gBAAgB,EAChB,eAAe,EACf,SAAS,EACT,aAAa,EACb,YAAY,GACb,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,gBAAgB,GACjB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAG/D,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAG5E,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG1C,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC7E,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,WAAW,EACX,UAAU,EACV,QAAQ,EACR,WAAW,EACX,UAAU,EACV,WAAW,EACX,aAAa,EACb,SAAS,GACV,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAG9E,OAAO,EACL,SAAS,EACT,OAAO,EACP,aAAa,EACb,cAAc,EACd,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,sBAAsB,EACtB,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,WAAW,EACX,YAAY,GACb,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,yBAAyB,EACzB,uBAAuB,EACvB,uBAAuB,EACvB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,EACL,MAAM,EACN,KAAK,EACL,OAAO,EACP,mBAAmB,EACnB,UAAU,EACV,gBAAgB,EAChB,eAAe,EACf,SAAS,EACT,aAAa,EACb,YAAY,GACb,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,gBAAgB,GACjB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAG/D,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAG5E,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG1C,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC7E,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { a as printAuditResults, c as stopIdleProjects, d as validateConfig, l as loadConfig, n as setup, o as runAudit, s as startIdleScheduler, t as audit, u as parseDuration } from "./setup-DHhQF7wW.js";
2
- import { A as initDb, B as stopProject, C as shouldNeverStop, D as getDb, E as getAccessLogs, F as getProjectFromHostname, G as parseBasicAuth, H as checkAuth, I as getProjectInfo, K as parseBearerToken, L as listProjects, M as setProjectStatus, N as updateProjectAccess, O as getIdleProjects, P as buildHostnameIndex, R as loadProjectList, S as loadProjectConfig, T as closeDb, U as matchHostname, V as watchProjectList, W as matchIp, j as logAccess, k as getProject, n as startServer, w as cleanOldLogs, x as getIdleTimeoutMs, z as startProject } from "./server-CLrB1zpR.js";
3
- export { audit, buildHostnameIndex, checkAuth, cleanOldLogs, closeDb, getAccessLogs, getDb, getIdleProjects, getIdleTimeoutMs, getProject, getProjectFromHostname, getProjectInfo, initDb, listProjects, loadConfig, loadProjectConfig, loadProjectList, logAccess, matchHostname, matchIp, parseBasicAuth, parseBearerToken, parseDuration, printAuditResults, runAudit, setProjectStatus, setup, shouldNeverStop, startIdleScheduler, startProject, startServer, stopIdleProjects, stopProject, updateProjectAccess, validateConfig, watchProjectList };
1
+ import { a as printAuditResults, c as stopIdleProjects, d as buildDockerFirewallScript, f as buildDockerFirewallUnit, g as validateConfig, h as parseDuration, l as DOCKER_FIREWALL_SCRIPT, m as loadConfig, n as setup, o as runAudit, p as configureDockerFirewall, s as startIdleScheduler, t as audit, u as DOCKER_FIREWALL_UNIT } from "./setup-DxoFhMfT.js";
2
+ import { B as buildHostnameIndex, C as getIdleProjects, D as setProjectStatus, E as logAccess, G as startProject, H as getProjectInfo, J as checkAuth, K as stopProject, O as updateProjectAccess, Q as parseBearerToken, S as getDb, T as initDb, U as listProjects, V as getProjectFromHostname, W as loadProjectList, X as matchIp, Y as matchHostname, Z as parseBasicAuth, _ as loadProjectConfig, b as closeDb, g as getIdleTimeoutMs, q as watchProjectList, s as startServer, v as shouldNeverStop, w as getProject, x as getAccessLogs, y as cleanOldLogs } from "./server-BAjHxgqh.js";
3
+ export { DOCKER_FIREWALL_SCRIPT, DOCKER_FIREWALL_UNIT, audit, buildDockerFirewallScript, buildDockerFirewallUnit, buildHostnameIndex, checkAuth, cleanOldLogs, closeDb, configureDockerFirewall, getAccessLogs, getDb, getIdleProjects, getIdleTimeoutMs, getProject, getProjectFromHostname, getProjectInfo, initDb, listProjects, loadConfig, loadProjectConfig, loadProjectList, logAccess, matchHostname, matchIp, parseBasicAuth, parseBearerToken, parseDuration, printAuditResults, runAudit, setProjectStatus, setup, shouldNeverStop, startIdleScheduler, startProject, startServer, stopIdleProjects, stopProject, updateProjectAccess, validateConfig, watchProjectList };