@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.
- package/README.md +129 -20
- package/dist/cli.js +49 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/{server-BAjHxgqh.js → server-EPU5dtPx.js} +355 -21
- package/dist/server-EPU5dtPx.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -1
- package/dist/setup/agent.d.ts +2 -0
- package/dist/setup/agent.d.ts.map +1 -1
- package/dist/setup/audit.d.ts +39 -1
- package/dist/setup/audit.d.ts.map +1 -1
- package/dist/setup/ddev.d.ts +102 -3
- package/dist/setup/ddev.d.ts.map +1 -1
- package/dist/setup/docker.d.ts +23 -0
- package/dist/setup/docker.d.ts.map +1 -1
- package/dist/setup/index.d.ts.map +1 -1
- package/dist/setup/migrations/0015__catchall_priority.d.ts +40 -0
- package/dist/setup/migrations/0015__catchall_priority.d.ts.map +1 -0
- package/dist/setup/migrations/0016__wildcard_dns_challenge.d.ts +63 -0
- package/dist/setup/migrations/0016__wildcard_dns_challenge.d.ts.map +1 -0
- package/dist/setup/migrations/0018__docker_address_pools.d.ts +29 -0
- package/dist/setup/migrations/0018__docker_address_pools.d.ts.map +1 -0
- package/dist/setup/migrations/index.d.ts.map +1 -1
- package/dist/setup/types.d.ts +11 -0
- package/dist/setup/types.d.ts.map +1 -1
- package/dist/{setup-BrMbM8w4.js → setup-4zlNJqDe.js} +373 -173
- package/dist/setup-4zlNJqDe.js.map +1 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/tls.d.ts +10 -0
- package/dist/utils/tls.d.ts.map +1 -0
- package/package.json +1 -1
- package/templates/wait.html +1 -0
- package/dist/server-BAjHxgqh.js.map +0 -1
- 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
|
-
|
|
185
|
-
certificate
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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 {
|
|
3
|
-
import {
|
|
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.
|
|
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.
|
|
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":"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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, 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":";;;;;;;;;;;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgEb,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;;;;;;;AAQA,SAAS,YAAY,SAA4C;CAC/D,MAAM,MAA8B,CAAC;CAErC,KAAK,MAAM,SAAS,WAAW,CAAC,GAAG;EACjC,MAAM,YAAY,MAAM,QAAQ,GAAG;EACnC,MAAM,MAAM,cAAc,KAAK,QAAQ,MAAM,MAAM,GAAG,SAAS;EAE/D,IAAI,cAAc,MAAM,CAAC,2BAA2B,KAAK,GAAG,GAAG;GAC7D,QAAQ,MAAM,4CAA4C,MAAM,EAAE;GAClE,QAAQ,KAAK,CAAC;EAChB;EAEA,IAAI,OAAO,MAAM,MAAM,YAAY,CAAC;CACtC;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;CAIA,MAAM,cACH,OAAO,mBAA0C,eAAe,IAAI;CACvE,MAAM,SAAS;EACb,GAAG,eAAe,IAAI;EACtB,GAAG,YAAY,OAAO,UAAkC;CAC1D;CAEA,IAAI,eAAe,CAAC,OAAO,OAAO;EAChC,QAAQ,MAAM,wCAAwC;EACtD,QAAQ,MAAM,qDAAqD;EACnE,QAAQ,KAAK,CAAC;CAChB;CAEA,MAAM,MAAM;EACV;EACA;EACA;EAEA,UAAU,eAAe,IAAI;EAC7B,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,gBAAgB,EAAE,MAAM,SAAS;GACjC,WAAW;IAAE,MAAM;IAAU,UAAU;GAAK;GAC5C,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"}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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 };
|