@studiometa/trafic-agent 0.1.45 → 0.1.47

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 (37) hide show
  1. package/README.md +129 -20
  2. package/dist/cli.js +49 -4
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.js +2 -2
  5. package/dist/{server-BAjHxgqh.js → server-EPU5dtPx.js} +355 -21
  6. package/dist/server-EPU5dtPx.js.map +1 -0
  7. package/dist/server.d.ts.map +1 -1
  8. package/dist/server.js +1 -1
  9. package/dist/setup/agent.d.ts +2 -0
  10. package/dist/setup/agent.d.ts.map +1 -1
  11. package/dist/setup/audit.d.ts +39 -1
  12. package/dist/setup/audit.d.ts.map +1 -1
  13. package/dist/setup/ddev.d.ts +102 -3
  14. package/dist/setup/ddev.d.ts.map +1 -1
  15. package/dist/setup/docker.d.ts +23 -0
  16. package/dist/setup/docker.d.ts.map +1 -1
  17. package/dist/setup/index.d.ts.map +1 -1
  18. package/dist/setup/migrations/0015__catchall_priority.d.ts +40 -0
  19. package/dist/setup/migrations/0015__catchall_priority.d.ts.map +1 -0
  20. package/dist/setup/migrations/0016__wildcard_dns_challenge.d.ts +63 -0
  21. package/dist/setup/migrations/0016__wildcard_dns_challenge.d.ts.map +1 -0
  22. package/dist/setup/migrations/0018__docker_address_pools.d.ts +29 -0
  23. package/dist/setup/migrations/0018__docker_address_pools.d.ts.map +1 -0
  24. package/dist/setup/migrations/index.d.ts.map +1 -1
  25. package/dist/setup/types.d.ts +11 -0
  26. package/dist/setup/types.d.ts.map +1 -1
  27. package/dist/{setup-BrMbM8w4.js → setup-4zlNJqDe.js} +373 -173
  28. package/dist/setup-4zlNJqDe.js.map +1 -0
  29. package/dist/types.d.ts +21 -0
  30. package/dist/types.d.ts.map +1 -1
  31. package/dist/utils/config.d.ts.map +1 -1
  32. package/dist/utils/tls.d.ts +10 -0
  33. package/dist/utils/tls.d.ts.map +1 -0
  34. package/package.json +1 -1
  35. package/templates/wait.html +1 -0
  36. package/dist/server-BAjHxgqh.js.map +0 -1
  37. package/dist/setup-BrMbM8w4.js.map +0 -1
package/README.md CHANGED
@@ -120,6 +120,14 @@ policy = "allow"
120
120
  [[auth.rules]]
121
121
  match = "admin.*"
122
122
  policy = "basic"
123
+
124
+ # Optional: one wildcard certificate instead of one per preview.
125
+ # See the TLS section below.
126
+ [tls]
127
+ dns_provider = "cloudflare"
128
+
129
+ [tls.dns_env]
130
+ CF_DNS_API_TOKEN = "..."
123
131
  ```
124
132
 
125
133
  ## Per-project configuration
@@ -181,26 +189,127 @@ trafic setup --host server.example.com --tld previews.example.com \
181
189
  --email admin@example.com
182
190
  ```
183
191
 
184
- With an email, DDEV sets `use_letsencrypt=true` and Traefik requests a
185
- certificate per project hostname on first use — previews included, since each
186
- one gets its own router. Without an email, projects are served with a locally
187
- trusted mkcert certificate, which is fine behind a proxy that terminates TLS
188
- itself but shows a browser warning if used directly.
189
-
190
- Two limits worth knowing before you rely on it:
191
-
192
- - Let's Encrypt allows **50 new certificates per registered domain per week**.
193
- Each preview hostname is a new certificate, so environments that churn fast
194
- can hit the ceiling. Renewals do not count against it.
195
- - Turning Let's Encrypt off does not discard certificates already issued.
196
- Traefik keeps them in `acme.json` in the `ddev-global-cache` volume and
197
- serves them again after a restart, so `configureDdev` deletes that storage
198
- when Let's Encrypt is disabled.
199
-
200
- Wildcard certificates would avoid the per-hostname limit but need a DNS-01
201
- challenge, which DDEV does not do. If you churn tens of previews a week, put a
202
- proxy in front that can (Caddy or Traefik with a DNS provider) and disable
203
- Let's Encrypt in DDEV so the two do not both try.
192
+ Without an email, projects are served with a locally trusted mkcert
193
+ certificate, which is fine behind a proxy that terminates TLS itself but shows
194
+ a browser warning if used directly.
195
+
196
+ There are two modes.
197
+
198
+ ### Per-host certificates (default)
199
+
200
+ DDEV puts `certResolver: acme-tlsChallenge` on every project router, so
201
+ Traefik asks Let's Encrypt for one certificate per project hostname on first
202
+ use — previews included.
203
+
204
+ Let's Encrypt allows **50 new certificates per registered domain per 7 days**.
205
+ Each preview hostname is a new certificate, so an environment that churns fast
206
+ hits the ceiling, and every host that misses out is served with Traefik's
207
+ self-signed default certificate until the window clears. Renewals do not count
208
+ against the limit.
209
+
210
+ ### Wildcard certificate (DNS-01)
211
+
212
+ One `*.<tld>` certificate covers every preview, so the per-hostname quota
213
+ stops being a factor. It needs a DNS-01 challenge, which needs an API token
214
+ for the DNS zone.
215
+
216
+ ```toml
217
+ [tls]
218
+ # Provider name from the Traefik/lego DNS provider list
219
+ dns_provider = "cloudflare"
220
+
221
+ # Optional: use the Let's Encrypt staging CA for a first test
222
+ # ca_server = "https://acme-staging-v02.api.letsencrypt.org/directory"
223
+
224
+ [tls.dns_env]
225
+ CF_DNS_API_TOKEN = "..."
226
+ ```
227
+
228
+ `setup` can write that section for you:
229
+
230
+ ```bash
231
+ trafic setup --host server.example.com --tld previews.example.com \
232
+ --email admin@example.com \
233
+ --dns-provider cloudflare --dns-env CF_DNS_API_TOKEN=...
234
+ ```
235
+
236
+ `--email` is required with `--dns-provider`: Let's Encrypt refuses an ACME
237
+ account without one. The credentials land in
238
+ `/home/ddev/.ddev/router-compose.trafic.yaml` (mode 600), which is what puts
239
+ them in the router container's environment where lego reads them.
240
+
241
+ **Rollout on an existing server:**
242
+
243
+ 1. Add the `[tls]` section to `/etc/trafic/config.toml`
244
+ 2. Run `sudo trafic-agent upgrade`
245
+
246
+ Migration `0016__wildcard_dns_challenge` writes the Traefik files, removes the
247
+ router container and starts one running project. DDEV regenerates the static
248
+ config on any project start, but reads `router-compose.*.yaml` only when it
249
+ recreates the router, so the removal is what gets the credentials into the
250
+ container. The router is back within seconds. With no project running, the
251
+ next deploy applies it. `trafic-agent audit` reports whether the certificate
252
+ was issued.
253
+
254
+ The first order takes about a minute after the router starts: lego waits for
255
+ the challenge record to propagate before Let's Encrypt looks for it.
256
+
257
+ **What the agent writes:**
258
+
259
+ | File | Purpose |
260
+ |------|---------|
261
+ | `~/.ddev/traefik/static_config.trafic.yaml` | The `acme-dns` resolver, its storage and the DNS challenge settings |
262
+ | `~/.ddev/traefik/custom-global-config/0-trafic-tls.yaml` | The default TLS store asking for `<tld>` and `*.<tld>` |
263
+ | `~/.ddev/router-compose.trafic.yaml` | The provider credentials on the router container |
264
+
265
+ The `0-` prefix is load-bearing. Traefik's file provider keeps the **first**
266
+ `tls.stores.default` it reads in directory order and logs "TLS store default
267
+ already configured, skipping" for every later file — and DDEV writes an empty
268
+ one in `default_config.yaml`.
269
+
270
+ `use_letsencrypt` stays on, and DDEV keeps writing a certificate resolver on
271
+ every project router. That costs nothing: Traefik skips a per-host ACME
272
+ request once the default store already holds a certificate matching the host,
273
+ wildcards included. So the quota use stops on its own as soon as the wildcard
274
+ is issued.
275
+
276
+ **Token scope, and the spare-zone pattern.** For Cloudflare the token needs
277
+ `Zone:DNS:Edit` on the zone holding the TXT record. That is a token that can
278
+ rewrite DNS for a production zone, sitting on a preview server. lego follows
279
+ CNAMEs when it looks for `_acme-challenge`, so the challenge can be delegated
280
+ to a zone nothing else uses:
281
+
282
+ ```
283
+ _acme-challenge.previews.example.com. CNAME _acme-challenge.example-acme.net.
284
+ ```
285
+
286
+ Scope the token to `example-acme.net` alone. A leak then costs a throwaway
287
+ zone rather than the production one.
288
+
289
+ **`*.<tld>` covers one label only.** `*.previews.example.com` matches
290
+ `my-app.previews.example.com` but not `a.b.previews.example.com`. A preview
291
+ name with a dot in it falls back to per-host issuance.
292
+
293
+ **A first test should use staging.** Set `ca_server` to
294
+ `https://acme-staging-v02.api.letsencrypt.org/directory`, run the rollout and
295
+ check the router log. Staging certificates are not trusted by browsers — that
296
+ is the point: a wrong token or an unreachable zone costs nothing in quota.
297
+ Switching to production afterwards is the case below.
298
+
299
+ **Changing `[tls]` later.** Editing `ca_server` or the DNS provider only
300
+ changes the config file. The Traefik files carry the old values until they are
301
+ rewritten, which today means re-running `setup` — the migration runs once per
302
+ server and does nothing on a second `upgrade`. A staging certificate also has
303
+ to be discarded before the switch to production: Traefik keeps it in
304
+ `acme-dns.json` in the `ddev-global-cache` volume and serves it until renewal,
305
+ so delete that file as well.
306
+
307
+ ### Turning Let's Encrypt off
308
+
309
+ Disabling it does not discard certificates already issued. Traefik keeps them
310
+ in `acme.json` in the `ddev-global-cache` volume and serves them again after a
311
+ restart, so `configureDdev` deletes that storage when Let's Encrypt is
312
+ disabled.
204
313
 
205
314
  ## Network exposure
206
315
 
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
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-BrMbM8w4.js";
3
- import { L as step, M as execSilent, 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";
2
+ import { H as step, I as exec, L as execSilent, R as info, T as closeDb, U as success, W as warn, it as loadConfig, ot as validateConfig, s as startServer, z as isRoot } from "./server-EPU5dtPx.js";
3
+ import { i as runPendingMigrations, n as setup, r as listMigrations, s as startIdleScheduler, t as audit } from "./setup-4zlNJqDe.js";
4
4
  import { readFileSync } from "node:fs";
5
5
  import { execSync, spawnSync } from "node:child_process";
6
6
  import { parseArgs } from "node:util";
@@ -113,7 +113,7 @@ function upgradeDdev(dryRun) {
113
113
  /** The real collaborators, used unless a caller passes their own. */
114
114
  function nodeUpgradeIo() {
115
115
  return {
116
- currentVersion: "0.1.45",
116
+ currentVersion: "0.1.47",
117
117
  alreadyReExeced: process.env["TRAFIC_UPGRADE_REEXEC"] === "1",
118
118
  isRoot,
119
119
  fetchLatestVersion,
@@ -215,6 +215,9 @@ Start options:
215
215
  Setup options:
216
216
  --tld <domain> TLD for DDEV projects (required on first run, reused from config on re-runs)
217
217
  --email <email> Email for Let's Encrypt certificates
218
+ --dns-provider <name> DNS-01 provider (lego name, e.g. cloudflare). Turns on the
219
+ wildcard *.<tld> certificate; requires --email
220
+ --dns-env KEY=VALUE Credential for that provider, repeatable
218
221
  --no-hardening Skip server hardening steps
219
222
  --no-root-ssh Disable root SSH login (recovery via rescue mode only)
220
223
  --no-docker Skip Docker installation
@@ -237,6 +240,11 @@ Examples:
237
240
  sudo trafic-agent setup --tld=previews.example.com
238
241
  sudo trafic-agent setup --tld=previews.example.com --trusted-proxy-hops=2
239
242
  sudo trafic-agent setup --tld=previews.example.com --email=admin@example.com
243
+
244
+ # Setup with one wildcard certificate instead of one per preview
245
+ sudo trafic-agent setup --tld=previews.example.com --email=admin@example.com \\
246
+ --dns-provider=cloudflare --dns-env=CF_DNS_API_TOKEN=xxx
247
+
240
248
  sudo trafic-agent setup --tld=previews.example.com --no-hardening --dry-run
241
249
 
242
250
  # Upgrade: install latest version, run migrations, restart service
@@ -252,7 +260,7 @@ function printHelp() {
252
260
  console.log(HELP.trim());
253
261
  }
254
262
  function printVersion() {
255
- console.log("0.1.45");
263
+ console.log("0.1.47");
256
264
  }
257
265
  async function runStart(values) {
258
266
  const config = loadConfig(values.config);
@@ -306,6 +314,25 @@ function parseTrustedProxyHops(raw) {
306
314
  }
307
315
  return hops;
308
316
  }
317
+ /**
318
+ * Parse repeated --dns-env KEY=VALUE pairs.
319
+ *
320
+ * A bad entry stops setup: a provider silently missing its token fails much
321
+ * later, as an ACME error in the router log.
322
+ */
323
+ function parseDnsEnv(entries) {
324
+ const env = {};
325
+ for (const entry of entries ?? []) {
326
+ const separator = entry.indexOf("=");
327
+ const key = separator === -1 ? entry : entry.slice(0, separator);
328
+ if (separator === -1 || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
329
+ console.error(`Error: --dns-env must be KEY=VALUE, got "${entry}"`);
330
+ process.exit(1);
331
+ }
332
+ env[key] = entry.slice(separator + 1);
333
+ }
334
+ return env;
335
+ }
309
336
  async function runSetup(values) {
310
337
  const existingConfig = loadConfig();
311
338
  const tld = values.tld ?? (existingConfig.tld || void 0);
@@ -314,8 +341,21 @@ async function runSetup(values) {
314
341
  console.error("Example: trafic-agent setup --tld=previews.example.com");
315
342
  process.exit(1);
316
343
  }
344
+ const dnsProvider = values["dns-provider"] ?? existingConfig.tls.dnsProvider;
345
+ const dnsEnv = {
346
+ ...existingConfig.tls.dnsEnv,
347
+ ...parseDnsEnv(values["dns-env"])
348
+ };
349
+ if (dnsProvider && !values.email) {
350
+ console.error("Error: --dns-provider requires --email");
351
+ console.error(" Let's Encrypt refuses an ACME account without one");
352
+ process.exit(1);
353
+ }
317
354
  await setup({
318
355
  tld,
356
+ dnsProvider,
357
+ dnsEnv,
358
+ caServer: existingConfig.tls.caServer,
319
359
  trustedProxyHops: parseTrustedProxyHops(values["trusted-proxy-hops"]),
320
360
  email: values.email,
321
361
  noHardening: values["no-hardening"],
@@ -349,6 +389,11 @@ async function main() {
349
389
  "no-docker": { type: "boolean" },
350
390
  "no-ddev": { type: "boolean" },
351
391
  "ssh-users": { type: "string" },
392
+ "dns-provider": { type: "string" },
393
+ "dns-env": {
394
+ type: "string",
395
+ multiple: true
396
+ },
352
397
  "trusted-proxy-hops": { type: "string" },
353
398
  "dry-run": { type: "boolean" },
354
399
  list: { type: "boolean" }
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, execSilent, 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 * The installed apt package version of DDEV, or null when it cannot be read.\n */\nexport function ddevVersion(): string | null {\n return execSilent(\"dpkg-query --show --showformat='${Version}' ddev\") || null;\n}\n\n/**\n * Upgrade DDEV from its apt repository. Throws when apt fails.\n *\n * DDEV is left out of unattended-upgrades on purpose — a major landing on its\n * own can break running previews — so it is updated here, where an operator\n * is watching. `--only-upgrade` never installs DDEV on a server that does not\n * already have it.\n */\nexport function upgradeDdev(dryRun: boolean): void {\n exec(\n \"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update\",\n { silent: !dryRun },\n );\n exec(\n \"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y --only-upgrade ddev\",\n { silent: !dryRun },\n );\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 /** The installed DDEV version, or null when DDEV is absent. */\n ddevVersion: () => string | null;\n upgradeDdev: (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 ddevVersion,\n upgradeDdev,\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. Update DDEV from its apt repository\n * 5. 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: Update DDEV ──────────────────────────────────────────────────\n step(\"Update DDEV\");\n upgradeDdevStep(dryRun, io);\n\n // ── Step 5: Restart service ───────────────────────────────────────────────\n step(\"Restart trafic-agent service\");\n io.restartAgentService(dryRun);\n if (!dryRun) {\n success(\"Service restarted\");\n }\n}\n\n/**\n * Update DDEV, reporting the version on either side of the apt call.\n *\n * A failed apt run must not cost the server its service restart, so the\n * error is warned about and the upgrade carries on.\n */\nfunction upgradeDdevStep(dryRun: boolean, io: UpgradeIo): void {\n const before = io.ddevVersion();\n\n if (!dryRun && before === null) {\n info(\"DDEV is not installed — skipping\");\n return;\n }\n\n if (before) {\n info(`Current: ${before}`);\n }\n\n try {\n io.upgradeDdev(dryRun);\n } catch (err) {\n warn(`DDEV update failed: ${err instanceof Error ? err.message : String(err)}`);\n return;\n }\n\n if (dryRun) {\n return;\n }\n\n const after = io.ddevVersion();\n if (after && after !== before) {\n success(`DDEV updated: ${after}`);\n } else {\n success(\"DDEV up to date\");\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":";;;;;;;;;;;AAWA,SAAgB,qBAAoC;CAClD,IAAI;EACF,OAAO,SAAS,6CAA6C;GAC3D,UAAU;GACV,OAAO;EACT,CAAC,CAAC,CAAC,KAAK;CACV,QAAQ;EACN,OAAO;CACT;AACF;;;;;AAMA,SAAgB,QAAQ,SAAiB,MAAuB;CAC9D,MAAM,SAAS,MAAwC;EACrD,MAAM,QAAQ,EAAE,QAAQ,MAAM,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,MAAM;EACvD,OAAO;GAAC,MAAM,MAAM;GAAG,MAAM,MAAM;GAAG,MAAM,MAAM;EAAC;CACrD;CAEA,MAAM,CAAC,MAAM,MAAM,QAAQ,MAAM,OAAO;CACxC,MAAM,CAAC,MAAM,MAAM,QAAQ,MAAM,IAAI;CAErC,IAAI,SAAS,MAAM,OAAO,OAAO;CACjC,IAAI,SAAS,MAAM,OAAO,OAAO;CACjC,OAAO,OAAO;AAChB;;;;;;AAOA,SAAgB,mBAAmB,QAAuB;CACxD,KAAK,kEAAkE,EAAE,QAAQ,CAAC,OAAO,CAAC;AAC5F;;;;;;;;;;;AAYA,SAAgB,sBAAqC;CACnD,IAAI;EAOF,MAAM,UAAU,GALG,SAAS,qCAAqC;GAC/D,UAAU;GACV,OAAO;EACT,CAAC,CAAC,CAAC,KAEgB,CAAA,CAAW,QAAQ,oBAAoB,EAAE,EAAE;EAE9D,OADY,KAAK,MAAM,aAAa,SAAS,OAAO,CAC7C,CAAA,CAAI;CACb,QAAQ;EACN,OAAO;CACT;AACF;;;;;;AAOA,SAAgB,gBAAgB,MAAuB;CACrD,MAAM,SAAS,SAAS,sBAAsB;EAAE,UAAU;EAAS,OAAO;CAAO,CAAC,CAAC,CAAC,KAAK;CACzF,MAAM,SAAS,UAAU,QAAQ,MAAM;EACrC,OAAO;EACP,KAAK;GAAE,GAAG,QAAQ;GAAK,uBAAuB;EAAI;CACpD,CAAC;CACD,QAAQ,KAAK,OAAO,UAAU,CAAC;AACjC;;;;AAKA,SAAgB,oBAAoB,QAAuB;CACzD,KAAK,kCAAkC,EAAE,QAAQ,CAAC,OAAO,CAAC;AAC5D;;;;AAKA,SAAgB,cAA6B;CAC3C,OAAO,WAAW,kDAAkD,KAAK;AAC3E;;;;;;;;;AAUA,SAAgB,YAAY,QAAuB;CACjD,KACE,oEACA,EAAE,QAAQ,CAAC,OAAO,CACpB;CACA,KACE,4FACA,EAAE,QAAQ,CAAC,OAAO,CACpB;AACF;;AA2BA,SAAgB,gBAA2B;CACzC,OAAO;EACL,gBAAA;EACA,iBAAiB,QAAQ,IAAI,6BAA6B;EAC1D;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF;;;;;;;;;;AAWA,SAAgB,WACd,SAAS,OACT,YACA,KAAgB,cAAc,GACxB;CACN,IAAI,CAAC,GAAG,OAAO,KAAK,CAAC,QAAQ;EAC3B,QAAQ,MAAM,uDAAuD;EACrE,QAAQ,IAAI,kCAAkC;EAC9C,QAAQ,KAAK,CAAC;CAChB;CAGA,MAAM,kBAAkB,GAAG;CAG3B,KAAK,mBAAmB;CAExB,MAAM,UAAU,GAAG;CACnB,KAAK,oBAAoB,SAAS;CAElC,MAAM,SAAS,GAAG,mBAAmB;CAErC,IAAI,CAAC,QACH,KAAK,uDAAuD;MACvD,IAAI,QAAQ,SAAS,MAAM,GAAG;EACnC,QAAQ,0BAA0B,QAAQ;EAG1C,KAAK,wBAAwB;EAC7B,GAAG,mBAAmB,MAAM;EAE5B,IAAI,CAAC,QAAQ;GAGX,MAAM,mBAAmB,GAAG,oBAAoB;GAEhD,IAAI,CAAC,mBAAmB,oBAAoB,QAAQ,SAAS,gBAAgB,GAAG;IAC9E,QAAQ,sCAAsC,kBAAkB;IAIhE,GAAG,gBAAgB,cAAc,CAAC,SAAS,CAAC;GAC9C,OAAO,IAAI,kBACT,QAAQ,sCAAsC,kBAAkB;EAEpE;CACF,OACE,QAAQ,uBAAuB,QAAQ,EAAE;CAI3C,KAAK,wBAAwB;CAC7B,GAAG,qBAAqB,MAAM;CAG9B,KAAK,aAAa;CAClB,gBAAgB,QAAQ,EAAE;CAG1B,KAAK,8BAA8B;CACnC,GAAG,oBAAoB,MAAM;CAC7B,IAAI,CAAC,QACH,QAAQ,mBAAmB;AAE/B;;;;;;;AAQA,SAAS,gBAAgB,QAAiB,IAAqB;CAC7D,MAAM,SAAS,GAAG,YAAY;CAE9B,IAAI,CAAC,UAAU,WAAW,MAAM;EAC9B,KAAK,kCAAkC;EACvC;CACF;CAEA,IAAI,QACF,KAAK,YAAY,QAAQ;CAG3B,IAAI;EACF,GAAG,YAAY,MAAM;CACvB,SAAS,KAAK;EACZ,KAAK,uBAAuB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,GAAG;EAC9E;CACF;CAEA,IAAI,QACF;CAGF,MAAM,QAAQ,GAAG,YAAY;CAC7B,IAAI,SAAS,UAAU,QACrB,QAAQ,iBAAiB,OAAO;MAEhC,QAAQ,iBAAiB;AAE7B;;;ACpQA,IAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDb,SAAS,YAAkB;CACzB,QAAQ,IAAI,KAAK,KAAK,CAAC;AACzB;AAEA,SAAS,eAAqB;CAC5B,QAAQ,IAAA,QAAe;AACzB;AAEA,eAAe,SAAS,QAAgD;CAEtE,MAAM,SAAS,WAAW,OAAO,MAA4B;CAG7D,IAAI,OAAO,MACT,OAAO,OAAO,SAAS,OAAO,MAAgB,EAAE;CAIlD,MAAM,SAAS,eAAe,MAAM;CACpC,IAAI,OAAO,SAAS,GAAG;EACrB,QAAQ,MAAM,uBAAuB;EACrC,KAAK,MAAM,SAAS,QAClB,QAAQ,MAAM,OAAO,OAAO;EAE9B,QAAQ,KAAK,CAAC;CAChB;CAGA,MAAM,iBAAuB;EAC3B,QAAQ,IAAI,oBAAoB;EAChC,QAAQ;EACR,QAAQ,KAAK,CAAC;CAChB;CAEA,QAAQ,GAAG,UAAU,QAAQ;CAC7B,QAAQ,GAAG,WAAW,QAAQ;CAG9B,YAAY,MAAM;CAClB,mBAAmB,MAAM;AAC3B;AAEA,SAAS,cAAc,QAAuC;CAC5D,MAAM,SAAS,OAAO;CAGtB,IAFa,OAAO,MAEV;EACR,MAAM,UAAU,eAAe;EAC/B,QAAQ,IAAI,iBAAiB;EAE7B,KAAK,MAAM,EAAE,WAAW,YAAY,SAAS;GAC3C,MAAM,OAAO,WAAW,YAAY,qBAAqB;GACzD,MAAM,QAAQ,WAAW,YAAY,6BAA6B;GAClE,QAAQ,IAAI,KAAK,KAAK,UAAU,UAAU,GAAG,OAAO,EAAE,EAAE,UAAU,UAAU,YAAY,OAAO,EAAE,EAAE,GAAG,OAAO;EAC/G;EAEA,QAAQ,IAAI,EAAE;EACd;CACF;CAEA,QAAQ,IAAI,4CAA4C;CAExD,IAAI,QACF,QAAQ,IAAI,0DAA0D;CAIxE,MAAM,aAAa,CAAC,WAAW,GAAI,SAAS,CAAC,WAAW,IAAI,CAAC,CAAE;CAC/D,WAAW,UAAU,OAAO,UAAU;CAEtC,IAAI,CAAC,QACH,QAAQ,IAAI,wCAAwC;AAExD;;;;;AAMA,SAAS,sBAAsB,KAAkC;CAC/D,IAAI,QAAQ,KAAA,GACV;CAGF,MAAM,OAAO,OAAO,GAAG;CAEvB,IAAI,CAAC,OAAO,UAAU,IAAI,KAAK,OAAO,GAAG;EACvC,QAAQ,MACN,yEAAyE,IAAI,EAC/E;EACA,QAAQ,MAAM,2DAA2D;EACzE,QAAQ,KAAK,CAAC;CAChB;CAEA,OAAO;AACT;AAEA,eAAe,SAAS,QAAgD;CAEtE,MAAM,iBAAiB,WAAW;CAClC,MAAM,MAAO,OAAO,QAA+B,eAAe,OAAO,KAAA;CAEzE,IAAI,CAAC,KAAK;EACR,QAAQ,MAAM,oCAAoC;EAClD,QAAQ,MAAM,wDAAwD;EACtE,QAAQ,KAAK,CAAC;CAChB;CAEA,MAAM,MAAM;EACV;EACA,kBAAkB,sBAAsB,OAAO,qBAA2C;EAC1F,OAAO,OAAO;EACd,aAAa,OAAO;EACpB,WAAW,OAAO;EAClB,UAAU,OAAO;EACjB,QAAQ,OAAO;EACf,UAAU,OAAO,eACZ,OAAO,YAAY,CAAY,MAAM,GAAG,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK,CAAC,IAC9D,KAAA;EACJ,QAAQ,OAAO;CACjB,CAAC;AACH;AAEA,eAAe,OAAsB;CACnC,MAAM,EAAE,QAAQ,gBAAgB,UAAU;EACxC,kBAAkB;EAClB,SAAS;GAEP,MAAM;IAAE,MAAM;IAAW,OAAO;GAAI;GAGpC,QAAQ;IAAE,MAAM;IAAU,OAAO;GAAI;GACrC,MAAM;IAAE,MAAM;IAAU,OAAO;GAAI;GAGnC,KAAK,EAAE,MAAM,SAAS;GACtB,OAAO,EAAE,MAAM,SAAS;GACxB,gBAAgB,EAAE,MAAM,UAAU;GAClC,eAAe,EAAE,MAAM,UAAU;GACjC,aAAa,EAAE,MAAM,UAAU;GAC/B,WAAW,EAAE,MAAM,UAAU;GAC7B,aAAa,EAAE,MAAM,SAAS;GAC9B,sBAAsB,EAAE,MAAM,SAAS;GACvC,WAAW,EAAE,MAAM,UAAU;GAG7B,MAAM,EAAE,MAAM,UAAU;EAC1B;CACF,CAAC;CAED,MAAM,UAAU,YAAY,MAAM;CAElC,IAAI,OAAO,MAAM;EACf,UAAU;EACV,QAAQ,KAAK,CAAC;CAChB;CAEA,QAAQ,SAAR;EACE,KAAK;GACH,UAAU;GACV;EAEF,KAAK;GACH,aAAa;GACb;EAEF,KAAK;GACH,MAAM,SAAS,MAAM;GACrB;EAEF,KAAK;GACH,MAAM,SAAS,MAAM;GACrB;EAEF,KAAK;EACL,KAAK;GACH,cAAc,MAAM;GACpB;EAEF,KAAK;GACH,MAAM,MAAM;GACZ;EAEF;GACE,QAAQ,MAAM,oBAAoB,SAAS;GAC3C,UAAU;GACV,QAAQ,KAAK,CAAC;CAClB;AACF;AAEA,KAAK,CAAC,CAAC,OAAO,UAAU;CACtB,QAAQ,MAAM,gBAAgB,KAAK;CACnC,QAAQ,KAAK,CAAC;AAChB,CAAC"}
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, execSilent, 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 * The installed apt package version of DDEV, or null when it cannot be read.\n */\nexport function ddevVersion(): string | null {\n return execSilent(\"dpkg-query --show --showformat='${Version}' ddev\") || null;\n}\n\n/**\n * Upgrade DDEV from its apt repository. Throws when apt fails.\n *\n * DDEV is left out of unattended-upgrades on purpose — a major landing on its\n * own can break running previews — so it is updated here, where an operator\n * is watching. `--only-upgrade` never installs DDEV on a server that does not\n * already have it.\n */\nexport function upgradeDdev(dryRun: boolean): void {\n exec(\n \"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update\",\n { silent: !dryRun },\n );\n exec(\n \"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y --only-upgrade ddev\",\n { silent: !dryRun },\n );\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 /** The installed DDEV version, or null when DDEV is absent. */\n ddevVersion: () => string | null;\n upgradeDdev: (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 ddevVersion,\n upgradeDdev,\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. Update DDEV from its apt repository\n * 5. 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: Update DDEV ──────────────────────────────────────────────────\n step(\"Update DDEV\");\n upgradeDdevStep(dryRun, io);\n\n // ── Step 5: Restart service ───────────────────────────────────────────────\n step(\"Restart trafic-agent service\");\n io.restartAgentService(dryRun);\n if (!dryRun) {\n success(\"Service restarted\");\n }\n}\n\n/**\n * Update DDEV, reporting the version on either side of the apt call.\n *\n * A failed apt run must not cost the server its service restart, so the\n * error is warned about and the upgrade carries on.\n */\nfunction upgradeDdevStep(dryRun: boolean, io: UpgradeIo): void {\n const before = io.ddevVersion();\n\n if (!dryRun && before === null) {\n info(\"DDEV is not installed — skipping\");\n return;\n }\n\n if (before) {\n info(`Current: ${before}`);\n }\n\n try {\n io.upgradeDdev(dryRun);\n } catch (err) {\n warn(`DDEV update failed: ${err instanceof Error ? err.message : String(err)}`);\n return;\n }\n\n if (dryRun) {\n return;\n }\n\n const after = io.ddevVersion();\n if (after && after !== before) {\n success(`DDEV updated: ${after}`);\n } else {\n success(\"DDEV up to date\");\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 --dns-provider <name> DNS-01 provider (lego name, e.g. cloudflare). Turns on the\n wildcard *.<tld> certificate; requires --email\n --dns-env KEY=VALUE Credential for that provider, repeatable\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\n # Setup with one wildcard certificate instead of one per preview\n sudo trafic-agent setup --tld=previews.example.com --email=admin@example.com \\\\\n --dns-provider=cloudflare --dns-env=CF_DNS_API_TOKEN=xxx\n\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\n/**\n * Parse repeated --dns-env KEY=VALUE pairs.\n *\n * A bad entry stops setup: a provider silently missing its token fails much\n * later, as an ACME error in the router log.\n */\nfunction parseDnsEnv(entries?: string[]): Record<string, string> {\n const env: Record<string, string> = {};\n\n for (const entry of entries ?? []) {\n const separator = entry.indexOf(\"=\");\n const key = separator === -1 ? entry : entry.slice(0, separator);\n\n if (separator === -1 || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {\n console.error(`Error: --dns-env must be KEY=VALUE, got \"${entry}\"`);\n process.exit(1);\n }\n\n env[key] = entry.slice(separator + 1);\n }\n\n return env;\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 // Re-runs keep what the config already says, so a setup run for another\n // reason cannot silently drop the wildcard certificate\n const dnsProvider =\n (values[\"dns-provider\"] as string | undefined) ?? existingConfig.tls.dnsProvider;\n const dnsEnv = {\n ...existingConfig.tls.dnsEnv,\n ...parseDnsEnv(values[\"dns-env\"] as string[] | undefined),\n };\n\n if (dnsProvider && !values.email) {\n console.error(\"Error: --dns-provider requires --email\");\n console.error(\" Let's Encrypt refuses an ACME account without one\");\n process.exit(1);\n }\n\n await setup({\n tld,\n dnsProvider,\n dnsEnv,\n // Not a flag: a CA override is a config-file decision, kept across re-runs\n caServer: existingConfig.tls.caServer,\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 \"dns-provider\": { type: \"string\" },\n \"dns-env\": { type: \"string\", multiple: true },\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.js CHANGED
@@ -1,3 +1,3 @@
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-BrMbM8w4.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";
1
+ import { $ as checkAuth, A as initDb, C as shouldNeverStop, D as getDb, E as getAccessLogs, G as buildHostnameIndex, J as listProjects, K as getProjectFromHostname, M as setProjectStatus, N as updateProjectAccess, O as getIdleProjects, Q as watchProjectList, S as loadProjectConfig, T as closeDb, X as startProject, Y as loadProjectList, Z as stopProject, at as parseDuration, et as matchHostname, it as loadConfig, j as logAccess, k as getProject, nt as parseBasicAuth, ot as validateConfig, q as getProjectInfo, rt as parseBearerToken, s as startServer, tt as matchIp, w as cleanOldLogs, x as getIdleTimeoutMs } from "./server-EPU5dtPx.js";
2
+ import { a as printAuditResults, c as stopIdleProjects, d as buildDockerFirewallScript, f as buildDockerFirewallUnit, l as DOCKER_FIREWALL_SCRIPT, n as setup, o as runAudit, p as configureDockerFirewall, s as startIdleScheduler, t as audit, u as DOCKER_FIREWALL_UNIT } from "./setup-4zlNJqDe.js";
3
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 };