@studiometa/trafic-agent 0.1.38 → 0.1.40
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/dist/cli.js +27 -13
- package/dist/cli.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/{server-CvSztHW2.js → server-BAjHxgqh.js} +73 -70
- package/dist/server-BAjHxgqh.js.map +1 -0
- package/dist/server.d.ts +48 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -2
- package/dist/setup/upgrade.d.ts +23 -1
- package/dist/setup/upgrade.d.ts.map +1 -1
- package/dist/{setup-BFU-DlX3.js → setup-DxoFhMfT.js} +2 -2
- package/dist/{setup-BFU-DlX3.js.map → setup-DxoFhMfT.js.map} +1 -1
- package/dist/utils/db.d.ts +13 -4
- package/dist/utils/db.d.ts.map +1 -1
- package/dist/utils/ddev.d.ts +24 -3
- package/dist/utils/ddev.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/server-CvSztHW2.js.map +0 -1
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-
|
|
3
|
-
import {
|
|
2
|
+
import { g as validateConfig, i as runPendingMigrations, m as loadConfig, n as setup, r as listMigrations, s as startIdleScheduler, t as audit } from "./setup-DxoFhMfT.js";
|
|
3
|
+
import { L as step, N as info, P as isRoot, R as success, b as closeDb, j as exec, s as startServer, z as warn } from "./server-BAjHxgqh.js";
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
5
|
import { execSync, spawnSync } from "node:child_process";
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
@@ -92,6 +92,20 @@ function reExecNewBinary(args) {
|
|
|
92
92
|
function restartAgentService(dryRun) {
|
|
93
93
|
exec("systemctl restart trafic-agent", { silent: !dryRun });
|
|
94
94
|
}
|
|
95
|
+
/** The real collaborators, used unless a caller passes their own. */
|
|
96
|
+
function nodeUpgradeIo() {
|
|
97
|
+
return {
|
|
98
|
+
currentVersion: "0.1.40",
|
|
99
|
+
alreadyReExeced: process.env["TRAFIC_UPGRADE_REEXEC"] === "1",
|
|
100
|
+
isRoot,
|
|
101
|
+
fetchLatestVersion,
|
|
102
|
+
installLatestAgent,
|
|
103
|
+
getInstalledVersion,
|
|
104
|
+
reExecNewBinary,
|
|
105
|
+
restartAgentService,
|
|
106
|
+
runPendingMigrations
|
|
107
|
+
};
|
|
108
|
+
}
|
|
95
109
|
/**
|
|
96
110
|
* Full upgrade sequence:
|
|
97
111
|
* 1. Check for a new version on npm
|
|
@@ -100,34 +114,34 @@ function restartAgentService(dryRun) {
|
|
|
100
114
|
* 3. Run pending migrations
|
|
101
115
|
* 4. Restart the systemd service
|
|
102
116
|
*/
|
|
103
|
-
function runUpgrade(dryRun = false, reExecArgs) {
|
|
104
|
-
if (!isRoot() && !dryRun) {
|
|
117
|
+
function runUpgrade(dryRun = false, reExecArgs, io = nodeUpgradeIo()) {
|
|
118
|
+
if (!io.isRoot() && !dryRun) {
|
|
105
119
|
console.error(" \x1B[31m✗\x1B[0m This command must be run as root");
|
|
106
120
|
console.log(" Run: sudo trafic-agent upgrade");
|
|
107
121
|
process.exit(1);
|
|
108
122
|
}
|
|
109
|
-
const alreadyReExeced =
|
|
123
|
+
const alreadyReExeced = io.alreadyReExeced;
|
|
110
124
|
step("Check for updates");
|
|
111
|
-
const current =
|
|
125
|
+
const current = io.currentVersion;
|
|
112
126
|
info(`Current version: ${current}`);
|
|
113
|
-
const latest = fetchLatestVersion();
|
|
127
|
+
const latest = io.fetchLatestVersion();
|
|
114
128
|
if (!latest) warn("Could not reach npm registry — skipping version check");
|
|
115
129
|
else if (isNewer(current, latest)) {
|
|
116
130
|
success(`New version available: ${latest}`);
|
|
117
131
|
step("Install latest version");
|
|
118
|
-
installLatestAgent(dryRun);
|
|
132
|
+
io.installLatestAgent(dryRun);
|
|
119
133
|
if (!dryRun) {
|
|
120
|
-
const installedVersion = getInstalledVersion();
|
|
134
|
+
const installedVersion = io.getInstalledVersion();
|
|
121
135
|
if (!alreadyReExeced && installedVersion && isNewer(current, installedVersion)) {
|
|
122
136
|
success(`Installed @studiometa/trafic-agent@${installedVersion}`);
|
|
123
|
-
reExecNewBinary(reExecArgs ?? ["upgrade"]);
|
|
137
|
+
io.reExecNewBinary(reExecArgs ?? ["upgrade"]);
|
|
124
138
|
} else if (installedVersion) success(`Installed @studiometa/trafic-agent@${installedVersion}`);
|
|
125
139
|
}
|
|
126
140
|
} else success(`Already up to date (${current})`);
|
|
127
141
|
step("Run pending migrations");
|
|
128
|
-
runPendingMigrations(dryRun);
|
|
142
|
+
io.runPendingMigrations(dryRun);
|
|
129
143
|
step("Restart trafic-agent service");
|
|
130
|
-
restartAgentService(dryRun);
|
|
144
|
+
io.restartAgentService(dryRun);
|
|
131
145
|
if (!dryRun) success("Service restarted");
|
|
132
146
|
}
|
|
133
147
|
//#endregion
|
|
@@ -191,7 +205,7 @@ function printHelp() {
|
|
|
191
205
|
console.log(HELP.trim());
|
|
192
206
|
}
|
|
193
207
|
function printVersion() {
|
|
194
|
-
console.log("0.1.
|
|
208
|
+
console.log("0.1.40");
|
|
195
209
|
}
|
|
196
210
|
async function runStart(values) {
|
|
197
211
|
const config = loadConfig(values.config);
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","names":[],"sources":["../src/setup/upgrade.ts","../src/cli.ts"],"sourcesContent":["import { execSync, spawnSync } from \"node:child_process\";\nimport { readFileSync } from \"node:fs\";\nimport { step, success, info, warn, exec, isRoot } from \"./steps.js\";\nimport { runPendingMigrations } from \"./migrations/index.js\";\n\ndeclare const __VERSION__: string;\n\n/**\n * Fetch the latest published version of @studiometa/trafic-agent from the npm registry.\n * Returns null if the registry is unreachable.\n */\nexport function fetchLatestVersion(): string | null {\n try {\n return execSync(\"npm show @studiometa/trafic-agent version\", {\n encoding: \"utf-8\",\n stdio: \"pipe\",\n }).trim();\n } catch {\n return null;\n }\n}\n\n/**\n * Compare two semver strings. Returns true if `next` is strictly greater than `current`.\n * Handles simple MAJOR.MINOR.PATCH without pre-release suffixes.\n */\nexport function isNewer(current: string, next: string): boolean {\n const parse = (v: string): [number, number, number] => {\n const parts = v.replace(/^v/, \"\").split(\".\").map(Number);\n return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];\n };\n\n const [cMaj, cMin, cPat] = parse(current);\n const [nMaj, nMin, nPat] = parse(next);\n\n if (nMaj !== cMaj) return nMaj > cMaj;\n if (nMin !== cMin) return nMin > cMin;\n return nPat > cPat;\n}\n\n/**\n * Install the latest version of @studiometa/trafic-agent globally via npm.\n * Uses --prefer-online to bypass the local npm cache and always fetch the\n * latest published tarball.\n */\nexport function installLatestAgent(dryRun: boolean): void {\n exec(\"npm install -g @studiometa/trafic-agent@latest --prefer-online\", { silent: !dryRun });\n}\n\n/**\n * Read the actual installed version of @studiometa/trafic-agent from its\n * package.json on disk — not from __VERSION__ which is baked in at build time.\n *\n * Resolves the package root by walking up from the trafic-agent binary:\n * /usr/bin/trafic-agent -> /usr/lib/node_modules/@studiometa/trafic-agent/dist/cli.js\n * -> /usr/lib/node_modules/@studiometa/trafic-agent/package.json\n *\n * Returns null if the file can't be read.\n */\nexport function getInstalledVersion(): string | null {\n try {\n // Resolve the real path of the binary (follow symlinks)\n const realBinary = execSync(\"readlink -f $(which trafic-agent)\", {\n encoding: \"utf-8\",\n stdio: \"pipe\",\n }).trim();\n // dist/cli.js -> up two dirs -> package root\n const pkgJson = `${realBinary.replace(/\\/dist\\/cli\\.js$/, \"\")}/package.json`;\n const pkg = JSON.parse(readFileSync(pkgJson, \"utf-8\")) as { version: string };\n return pkg.version;\n } catch {\n return null;\n }\n}\n\n/**\n * Re-exec the newly installed trafic-agent binary with the same arguments.\n * Used after a self-update so the new binary runs its own migrations.\n * This function never returns — it replaces the current process.\n */\nexport function reExecNewBinary(args: string[]): never {\n const binary = execSync(\"which trafic-agent\", { encoding: \"utf-8\", stdio: \"pipe\" }).trim();\n const result = spawnSync(binary, args, {\n stdio: \"inherit\",\n env: { ...process.env, TRAFIC_UPGRADE_REEXEC: \"1\" },\n });\n process.exit(result.status ?? 0);\n}\n\n/**\n * Restart the trafic-agent systemd service.\n */\nexport function restartAgentService(dryRun: boolean): void {\n exec(\"systemctl restart trafic-agent\", { silent: !dryRun });\n}\n\n/**\n * Full upgrade sequence:\n * 1. Check for a new version on npm\n * 2. Install it globally if one is found — then re-exec the new binary\n * so that steps 3 and 4 run with the new migration registry\n * 3. Run pending migrations\n * 4. Restart the systemd service\n */\nexport function runUpgrade(dryRun = false, reExecArgs?: string[]): void {\n if (!isRoot() && !dryRun) {\n console.error(\" \\x1b[31m✗\\x1b[0m This command must be run as root\");\n console.log(\" Run: sudo trafic-agent upgrade\");\n process.exit(1);\n }\n\n // Guard against infinite re-exec loops: only re-exec once per upgrade run.\n const alreadyReExeced = process.env[\"TRAFIC_UPGRADE_REEXEC\"] === \"1\";\n\n // ── Step 1: Check for updates ─────────────────────────────────────────────\n step(\"Check for updates\");\n\n const current = __VERSION__;\n info(`Current version: ${current}`);\n\n const latest = fetchLatestVersion();\n\n if (!latest) {\n warn(\"Could not reach npm registry — skipping version check\");\n } else if (isNewer(current, latest)) {\n success(`New version available: ${latest}`);\n\n // ── Step 2: Install ───────────────────────────────────────────────────\n step(\"Install latest version\");\n installLatestAgent(dryRun);\n\n if (!dryRun) {\n // Verify the installed version actually changed before re-execing —\n // npm can serve stale cache and leave the old binary in place.\n const installedVersion = getInstalledVersion();\n\n if (!alreadyReExeced && installedVersion && isNewer(current, installedVersion)) {\n success(`Installed @studiometa/trafic-agent@${installedVersion}`);\n // Re-exec the newly installed binary so steps 3 and 4 run with the\n // new migration registry — the current process only knows about\n // migrations that existed at the time it was compiled.\n reExecNewBinary(reExecArgs ?? [\"upgrade\"]);\n } else if (installedVersion) {\n success(`Installed @studiometa/trafic-agent@${installedVersion}`);\n }\n }\n } else {\n success(`Already up to date (${current})`);\n }\n\n // ── Step 3: Run pending migrations ───────────────────────────────────────\n step(\"Run pending migrations\");\n runPendingMigrations(dryRun);\n\n // ── Step 4: Restart service ───────────────────────────────────────────────\n step(\"Restart trafic-agent service\");\n restartAgentService(dryRun);\n if (!dryRun) {\n success(\"Service restarted\");\n }\n}\n","#!/usr/bin/env node\n\nimport { parseArgs } from \"node:util\";\nimport { loadConfig, validateConfig } from \"./utils/config.js\";\nimport { startServer } from \"./server.js\";\nimport { startIdleScheduler } from \"./tasks/stop-idle.js\";\nimport { closeDb } from \"./utils/db.js\";\nimport { setup, audit } from \"./setup/index.js\";\nimport { listMigrations } from \"./setup/migrations/index.js\";\nimport { runUpgrade } from \"./setup/upgrade.js\";\n\ndeclare const __VERSION__: string;\n\nconst HELP = `\ntrafic-agent — Server agent for DDEV preview environments\n\nUsage:\n trafic-agent <command> [options]\n\nCommands:\n start Start the agent server\n setup Setup a new server (Docker, DDEV, hardening)\n upgrade Check for a new version, install it, run migrations, restart the service\n update Alias for upgrade\n audit Run security audit checks\n version Show version\n help Show this help\n\nStart options:\n -c, --config <path> Path to config file (default: /etc/trafic/config.toml)\n -p, --port <port> Override port from config\n\nSetup options:\n --tld <domain> TLD for DDEV projects (required on first run, reused from config on re-runs)\n --email <email> Email for Let's Encrypt certificates\n --no-hardening Skip server hardening steps\n --no-root-ssh Disable root SSH login (recovery via rescue mode only)\n --no-docker Skip Docker installation\n --no-ddev Skip DDEV installation\n --ssh-users <users> Comma-separated list of SSH users to allow (default: ddev)\n --trusted-proxy-hops <n> Proxies in front of the agent (default: 1; 2 behind a CDN).\n Written to the generated config; ignored if one exists\n --dry-run Show what would be done without making changes\n\nUpgrade/update options:\n --dry-run Show what would be done without making changes\n --list List all migrations and their status (no install or restart)\n\nExamples:\n # Start the agent\n trafic-agent start\n trafic-agent start --config /path/to/config.toml\n\n # Setup a new server\n sudo trafic-agent setup --tld=previews.example.com\n sudo trafic-agent setup --tld=previews.example.com --trusted-proxy-hops=2\n sudo trafic-agent setup --tld=previews.example.com --email=admin@example.com\n sudo trafic-agent setup --tld=previews.example.com --no-hardening --dry-run\n\n # Upgrade: install latest version, run migrations, restart service\n sudo trafic-agent upgrade\n sudo trafic-agent upgrade --dry-run\n trafic-agent upgrade --list\n sudo trafic-agent update # alias for upgrade\n\n # Run security audit\n trafic-agent audit\n`;\n\nfunction printHelp(): void {\n console.log(HELP.trim());\n}\n\nfunction printVersion(): void {\n console.log(__VERSION__);\n}\n\nasync function runStart(values: Record<string, unknown>): Promise<void> {\n // Load config\n const config = loadConfig(values.config as string | undefined);\n\n // Override port if specified\n if (values.port) {\n config.port = parseInt(values.port as string, 10);\n }\n\n // Validate config\n const errors = validateConfig(config);\n if (errors.length > 0) {\n console.error(\"Configuration errors:\");\n for (const error of errors) {\n console.error(` - ${error}`);\n }\n process.exit(1);\n }\n\n // Graceful shutdown\n const shutdown = (): void => {\n console.log(\"\\nShutting down...\");\n closeDb();\n process.exit(0);\n };\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n\n // Start server and scheduler\n startServer(config);\n startIdleScheduler(config);\n}\n\nfunction handleUpgrade(values: Record<string, unknown>): void {\n const dryRun = values[\"dry-run\"] as boolean | undefined;\n const list = values.list as boolean | undefined;\n\n if (list) {\n const entries = listMigrations();\n console.log(\"\\nMigrations:\\n\");\n\n for (const { migration, status } of entries) {\n const icon = status === \"applied\" ? \"\\x1b[32m✓\\x1b[0m\" : \"\\x1b[36m→\\x1b[0m\";\n const label = status === \"applied\" ? \"\\x1b[90m(applied)\\x1b[0m\" : \"(pending)\";\n console.log(` ${icon} \\x1b[1m${migration.id.padEnd(30)}\\x1b[0m ${migration.description.padEnd(55)} ${label}`);\n }\n\n console.log(\"\");\n return;\n }\n\n console.log(\"\\n\\x1b[1m🚦 Trafic Server Upgrade\\x1b[0m\\n\");\n\n if (dryRun) {\n console.log(\"\\x1b[33m⚠ Dry-run mode: no changes will be made\\x1b[0m\\n\");\n }\n\n // Build re-exec args so flags survive the binary swap after a self-update\n const reExecArgs = [\"upgrade\", ...(dryRun ? [\"--dry-run\"] : [])];\n runUpgrade(dryRun ?? false, reExecArgs);\n\n if (!dryRun) {\n console.log(\"\\n\\x1b[32m✓ Upgrade complete!\\x1b[0m\\n\");\n }\n}\n\n/**\n * Parse --trusted-proxy-hops, failing loudly rather than writing a config\n * that would quietly break the IP allowlist.\n */\nfunction parseTrustedProxyHops(raw?: string): number | undefined {\n if (raw === undefined) {\n return undefined;\n }\n\n const hops = Number(raw);\n\n if (!Number.isInteger(hops) || hops < 1) {\n console.error(\n `Error: --trusted-proxy-hops must be a whole number of 1 or more, got \"${raw}\"`,\n );\n console.error(\" 1 = ddev-router/Traefik alone, 2 = a CDN in front of it\");\n process.exit(1);\n }\n\n return hops;\n}\n\nasync function runSetup(values: Record<string, unknown>): Promise<void> {\n // Load existing config to use as defaults on re-runs\n const existingConfig = loadConfig();\n const tld = (values.tld as string | undefined) ?? (existingConfig.tld || undefined);\n\n if (!tld) {\n console.error(\"Error: --tld is required for setup\");\n console.error(\"Example: trafic-agent setup --tld=previews.example.com\");\n process.exit(1);\n }\n\n await setup({\n tld,\n trustedProxyHops: parseTrustedProxyHops(values[\"trusted-proxy-hops\"] as string | undefined),\n email: values.email as string | undefined,\n noHardening: values[\"no-hardening\"] as boolean | undefined,\n noRootSsh: values[\"no-root-ssh\"] as boolean | undefined,\n noDocker: values[\"no-docker\"] as boolean | undefined,\n noDdev: values[\"no-ddev\"] as boolean | undefined,\n sshUsers: values[\"ssh-users\"]\n ? (values[\"ssh-users\"] as string).split(\",\").map((s) => s.trim())\n : undefined,\n dryRun: values[\"dry-run\"] as boolean | undefined,\n });\n}\n\nasync function main(): Promise<void> {\n const { values, positionals } = parseArgs({\n allowPositionals: true,\n options: {\n // Common\n help: { type: \"boolean\", short: \"h\" },\n\n // Start options\n config: { type: \"string\", short: \"c\" },\n port: { type: \"string\", short: \"p\" },\n\n // Setup options\n tld: { type: \"string\" },\n email: { type: \"string\" },\n \"no-hardening\": { type: \"boolean\" },\n \"no-root-ssh\": { type: \"boolean\" },\n \"no-docker\": { type: \"boolean\" },\n \"no-ddev\": { type: \"boolean\" },\n \"ssh-users\": { type: \"string\" },\n \"trusted-proxy-hops\": { type: \"string\" },\n \"dry-run\": { type: \"boolean\" },\n\n // Upgrade options\n list: { type: \"boolean\" },\n },\n });\n\n const command = positionals[0] ?? \"help\";\n\n if (values.help) {\n printHelp();\n process.exit(0);\n }\n\n switch (command) {\n case \"help\":\n printHelp();\n break;\n\n case \"version\":\n printVersion();\n break;\n\n case \"start\":\n await runStart(values);\n break;\n\n case \"setup\":\n await runSetup(values);\n break;\n\n case \"upgrade\":\n case \"update\":\n handleUpgrade(values);\n break;\n\n case \"audit\":\n await audit();\n break;\n\n default:\n console.error(`Unknown command: ${command}`);\n printHelp();\n process.exit(1);\n }\n}\n\nmain().catch((error) => {\n console.error(\"Fatal error:\", error);\n process.exit(1);\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../src/setup/upgrade.ts","../src/cli.ts"],"sourcesContent":["import { execSync, spawnSync } from \"node:child_process\";\nimport { readFileSync } from \"node:fs\";\nimport { step, success, info, warn, exec, isRoot } from \"./steps.js\";\nimport { runPendingMigrations } from \"./migrations/index.js\";\n\ndeclare const __VERSION__: string;\n\n/**\n * Fetch the latest published version of @studiometa/trafic-agent from the npm registry.\n * Returns null if the registry is unreachable.\n */\nexport function fetchLatestVersion(): string | null {\n try {\n return execSync(\"npm show @studiometa/trafic-agent version\", {\n encoding: \"utf-8\",\n stdio: \"pipe\",\n }).trim();\n } catch {\n return null;\n }\n}\n\n/**\n * Compare two semver strings. Returns true if `next` is strictly greater than `current`.\n * Handles simple MAJOR.MINOR.PATCH without pre-release suffixes.\n */\nexport function isNewer(current: string, next: string): boolean {\n const parse = (v: string): [number, number, number] => {\n const parts = v.replace(/^v/, \"\").split(\".\").map(Number);\n return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];\n };\n\n const [cMaj, cMin, cPat] = parse(current);\n const [nMaj, nMin, nPat] = parse(next);\n\n if (nMaj !== cMaj) return nMaj > cMaj;\n if (nMin !== cMin) return nMin > cMin;\n return nPat > cPat;\n}\n\n/**\n * Install the latest version of @studiometa/trafic-agent globally via npm.\n * Uses --prefer-online to bypass the local npm cache and always fetch the\n * latest published tarball.\n */\nexport function installLatestAgent(dryRun: boolean): void {\n exec(\"npm install -g @studiometa/trafic-agent@latest --prefer-online\", { silent: !dryRun });\n}\n\n/**\n * Read the actual installed version of @studiometa/trafic-agent from its\n * package.json on disk — not from __VERSION__ which is baked in at build time.\n *\n * Resolves the package root by walking up from the trafic-agent binary:\n * /usr/bin/trafic-agent -> /usr/lib/node_modules/@studiometa/trafic-agent/dist/cli.js\n * -> /usr/lib/node_modules/@studiometa/trafic-agent/package.json\n *\n * Returns null if the file can't be read.\n */\nexport function getInstalledVersion(): string | null {\n try {\n // Resolve the real path of the binary (follow symlinks)\n const realBinary = execSync(\"readlink -f $(which trafic-agent)\", {\n encoding: \"utf-8\",\n stdio: \"pipe\",\n }).trim();\n // dist/cli.js -> up two dirs -> package root\n const pkgJson = `${realBinary.replace(/\\/dist\\/cli\\.js$/, \"\")}/package.json`;\n const pkg = JSON.parse(readFileSync(pkgJson, \"utf-8\")) as { version: string };\n return pkg.version;\n } catch {\n return null;\n }\n}\n\n/**\n * Re-exec the newly installed trafic-agent binary with the same arguments.\n * Used after a self-update so the new binary runs its own migrations.\n * This function never returns — it replaces the current process.\n */\nexport function reExecNewBinary(args: string[]): never {\n const binary = execSync(\"which trafic-agent\", { encoding: \"utf-8\", stdio: \"pipe\" }).trim();\n const result = spawnSync(binary, args, {\n stdio: \"inherit\",\n env: { ...process.env, TRAFIC_UPGRADE_REEXEC: \"1\" },\n });\n process.exit(result.status ?? 0);\n}\n\n/**\n * Restart the trafic-agent systemd service.\n */\nexport function restartAgentService(dryRun: boolean): void {\n exec(\"systemctl restart trafic-agent\", { silent: !dryRun });\n}\n\n/**\n * What the upgrade sequence needs from the outside world.\n *\n * Injected so the orchestration can be tested without reaching npm, writing\n * to the filesystem or replacing the running process — the last of which is\n * why `reExecNewBinary` is here rather than called directly.\n */\nexport interface UpgradeIo {\n /** The version of the process running the upgrade. */\n currentVersion: string;\n /** Whether an earlier run already re-execed, guarding against a loop. */\n alreadyReExeced: boolean;\n isRoot: () => boolean;\n fetchLatestVersion: () => string | null;\n installLatestAgent: (dryRun: boolean) => void;\n getInstalledVersion: () => string | null;\n reExecNewBinary: (args: string[]) => never;\n restartAgentService: (dryRun: boolean) => void;\n runPendingMigrations: (dryRun: boolean) => void;\n}\n\n/** The real collaborators, used unless a caller passes their own. */\nexport function nodeUpgradeIo(): UpgradeIo {\n return {\n currentVersion: __VERSION__,\n alreadyReExeced: process.env[\"TRAFIC_UPGRADE_REEXEC\"] === \"1\",\n isRoot,\n fetchLatestVersion,\n installLatestAgent,\n getInstalledVersion,\n reExecNewBinary,\n restartAgentService,\n runPendingMigrations,\n };\n}\n\n/**\n * Full upgrade sequence:\n * 1. Check for a new version on npm\n * 2. Install it globally if one is found — then re-exec the new binary\n * so that steps 3 and 4 run with the new migration registry\n * 3. Run pending migrations\n * 4. Restart the systemd service\n */\nexport function runUpgrade(\n dryRun = false,\n reExecArgs?: string[],\n io: UpgradeIo = nodeUpgradeIo(),\n): void {\n if (!io.isRoot() && !dryRun) {\n console.error(\" \\x1b[31m✗\\x1b[0m This command must be run as root\");\n console.log(\" Run: sudo trafic-agent upgrade\");\n process.exit(1);\n }\n\n // Guard against infinite re-exec loops: only re-exec once per upgrade run.\n const alreadyReExeced = io.alreadyReExeced;\n\n // ── Step 1: Check for updates ─────────────────────────────────────────────\n step(\"Check for updates\");\n\n const current = io.currentVersion;\n info(`Current version: ${current}`);\n\n const latest = io.fetchLatestVersion();\n\n if (!latest) {\n warn(\"Could not reach npm registry — skipping version check\");\n } else if (isNewer(current, latest)) {\n success(`New version available: ${latest}`);\n\n // ── Step 2: Install ───────────────────────────────────────────────────\n step(\"Install latest version\");\n io.installLatestAgent(dryRun);\n\n if (!dryRun) {\n // Verify the installed version actually changed before re-execing —\n // npm can serve stale cache and leave the old binary in place.\n const installedVersion = io.getInstalledVersion();\n\n if (!alreadyReExeced && installedVersion && isNewer(current, installedVersion)) {\n success(`Installed @studiometa/trafic-agent@${installedVersion}`);\n // Re-exec the newly installed binary so steps 3 and 4 run with the\n // new migration registry — the current process only knows about\n // migrations that existed at the time it was compiled.\n io.reExecNewBinary(reExecArgs ?? [\"upgrade\"]);\n } else if (installedVersion) {\n success(`Installed @studiometa/trafic-agent@${installedVersion}`);\n }\n }\n } else {\n success(`Already up to date (${current})`);\n }\n\n // ── Step 3: Run pending migrations ───────────────────────────────────────\n step(\"Run pending migrations\");\n io.runPendingMigrations(dryRun);\n\n // ── Step 4: Restart service ───────────────────────────────────────────────\n step(\"Restart trafic-agent service\");\n io.restartAgentService(dryRun);\n if (!dryRun) {\n success(\"Service restarted\");\n }\n}\n","#!/usr/bin/env node\n\nimport { parseArgs } from \"node:util\";\nimport { loadConfig, validateConfig } from \"./utils/config.js\";\nimport { startServer } from \"./server.js\";\nimport { startIdleScheduler } from \"./tasks/stop-idle.js\";\nimport { closeDb } from \"./utils/db.js\";\nimport { setup, audit } from \"./setup/index.js\";\nimport { listMigrations } from \"./setup/migrations/index.js\";\nimport { runUpgrade } from \"./setup/upgrade.js\";\n\ndeclare const __VERSION__: string;\n\nconst HELP = `\ntrafic-agent — Server agent for DDEV preview environments\n\nUsage:\n trafic-agent <command> [options]\n\nCommands:\n start Start the agent server\n setup Setup a new server (Docker, DDEV, hardening)\n upgrade Check for a new version, install it, run migrations, restart the service\n update Alias for upgrade\n audit Run security audit checks\n version Show version\n help Show this help\n\nStart options:\n -c, --config <path> Path to config file (default: /etc/trafic/config.toml)\n -p, --port <port> Override port from config\n\nSetup options:\n --tld <domain> TLD for DDEV projects (required on first run, reused from config on re-runs)\n --email <email> Email for Let's Encrypt certificates\n --no-hardening Skip server hardening steps\n --no-root-ssh Disable root SSH login (recovery via rescue mode only)\n --no-docker Skip Docker installation\n --no-ddev Skip DDEV installation\n --ssh-users <users> Comma-separated list of SSH users to allow (default: ddev)\n --trusted-proxy-hops <n> Proxies in front of the agent (default: 1; 2 behind a CDN).\n Written to the generated config; ignored if one exists\n --dry-run Show what would be done without making changes\n\nUpgrade/update options:\n --dry-run Show what would be done without making changes\n --list List all migrations and their status (no install or restart)\n\nExamples:\n # Start the agent\n trafic-agent start\n trafic-agent start --config /path/to/config.toml\n\n # Setup a new server\n sudo trafic-agent setup --tld=previews.example.com\n sudo trafic-agent setup --tld=previews.example.com --trusted-proxy-hops=2\n sudo trafic-agent setup --tld=previews.example.com --email=admin@example.com\n sudo trafic-agent setup --tld=previews.example.com --no-hardening --dry-run\n\n # Upgrade: install latest version, run migrations, restart service\n sudo trafic-agent upgrade\n sudo trafic-agent upgrade --dry-run\n trafic-agent upgrade --list\n sudo trafic-agent update # alias for upgrade\n\n # Run security audit\n trafic-agent audit\n`;\n\nfunction printHelp(): void {\n console.log(HELP.trim());\n}\n\nfunction printVersion(): void {\n console.log(__VERSION__);\n}\n\nasync function runStart(values: Record<string, unknown>): Promise<void> {\n // Load config\n const config = loadConfig(values.config as string | undefined);\n\n // Override port if specified\n if (values.port) {\n config.port = parseInt(values.port as string, 10);\n }\n\n // Validate config\n const errors = validateConfig(config);\n if (errors.length > 0) {\n console.error(\"Configuration errors:\");\n for (const error of errors) {\n console.error(` - ${error}`);\n }\n process.exit(1);\n }\n\n // Graceful shutdown\n const shutdown = (): void => {\n console.log(\"\\nShutting down...\");\n closeDb();\n process.exit(0);\n };\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n\n // Start server and scheduler\n startServer(config);\n startIdleScheduler(config);\n}\n\nfunction handleUpgrade(values: Record<string, unknown>): void {\n const dryRun = values[\"dry-run\"] as boolean | undefined;\n const list = values.list as boolean | undefined;\n\n if (list) {\n const entries = listMigrations();\n console.log(\"\\nMigrations:\\n\");\n\n for (const { migration, status } of entries) {\n const icon = status === \"applied\" ? \"\\x1b[32m✓\\x1b[0m\" : \"\\x1b[36m→\\x1b[0m\";\n const label = status === \"applied\" ? \"\\x1b[90m(applied)\\x1b[0m\" : \"(pending)\";\n console.log(` ${icon} \\x1b[1m${migration.id.padEnd(30)}\\x1b[0m ${migration.description.padEnd(55)} ${label}`);\n }\n\n console.log(\"\");\n return;\n }\n\n console.log(\"\\n\\x1b[1m🚦 Trafic Server Upgrade\\x1b[0m\\n\");\n\n if (dryRun) {\n console.log(\"\\x1b[33m⚠ Dry-run mode: no changes will be made\\x1b[0m\\n\");\n }\n\n // Build re-exec args so flags survive the binary swap after a self-update\n const reExecArgs = [\"upgrade\", ...(dryRun ? [\"--dry-run\"] : [])];\n runUpgrade(dryRun ?? false, reExecArgs);\n\n if (!dryRun) {\n console.log(\"\\n\\x1b[32m✓ Upgrade complete!\\x1b[0m\\n\");\n }\n}\n\n/**\n * Parse --trusted-proxy-hops, failing loudly rather than writing a config\n * that would quietly break the IP allowlist.\n */\nfunction parseTrustedProxyHops(raw?: string): number | undefined {\n if (raw === undefined) {\n return undefined;\n }\n\n const hops = Number(raw);\n\n if (!Number.isInteger(hops) || hops < 1) {\n console.error(\n `Error: --trusted-proxy-hops must be a whole number of 1 or more, got \"${raw}\"`,\n );\n console.error(\" 1 = ddev-router/Traefik alone, 2 = a CDN in front of it\");\n process.exit(1);\n }\n\n return hops;\n}\n\nasync function runSetup(values: Record<string, unknown>): Promise<void> {\n // Load existing config to use as defaults on re-runs\n const existingConfig = loadConfig();\n const tld = (values.tld as string | undefined) ?? (existingConfig.tld || undefined);\n\n if (!tld) {\n console.error(\"Error: --tld is required for setup\");\n console.error(\"Example: trafic-agent setup --tld=previews.example.com\");\n process.exit(1);\n }\n\n await setup({\n tld,\n trustedProxyHops: parseTrustedProxyHops(values[\"trusted-proxy-hops\"] as string | undefined),\n email: values.email as string | undefined,\n noHardening: values[\"no-hardening\"] as boolean | undefined,\n noRootSsh: values[\"no-root-ssh\"] as boolean | undefined,\n noDocker: values[\"no-docker\"] as boolean | undefined,\n noDdev: values[\"no-ddev\"] as boolean | undefined,\n sshUsers: values[\"ssh-users\"]\n ? (values[\"ssh-users\"] as string).split(\",\").map((s) => s.trim())\n : undefined,\n dryRun: values[\"dry-run\"] as boolean | undefined,\n });\n}\n\nasync function main(): Promise<void> {\n const { values, positionals } = parseArgs({\n allowPositionals: true,\n options: {\n // Common\n help: { type: \"boolean\", short: \"h\" },\n\n // Start options\n config: { type: \"string\", short: \"c\" },\n port: { type: \"string\", short: \"p\" },\n\n // Setup options\n tld: { type: \"string\" },\n email: { type: \"string\" },\n \"no-hardening\": { type: \"boolean\" },\n \"no-root-ssh\": { type: \"boolean\" },\n \"no-docker\": { type: \"boolean\" },\n \"no-ddev\": { type: \"boolean\" },\n \"ssh-users\": { type: \"string\" },\n \"trusted-proxy-hops\": { type: \"string\" },\n \"dry-run\": { type: \"boolean\" },\n\n // Upgrade options\n list: { type: \"boolean\" },\n },\n });\n\n const command = positionals[0] ?? \"help\";\n\n if (values.help) {\n printHelp();\n process.exit(0);\n }\n\n switch (command) {\n case \"help\":\n printHelp();\n break;\n\n case \"version\":\n printVersion();\n break;\n\n case \"start\":\n await runStart(values);\n break;\n\n case \"setup\":\n await runSetup(values);\n break;\n\n case \"upgrade\":\n case \"update\":\n handleUpgrade(values);\n break;\n\n case \"audit\":\n await audit();\n break;\n\n default:\n console.error(`Unknown command: ${command}`);\n printHelp();\n process.exit(1);\n }\n}\n\nmain().catch((error) => {\n console.error(\"Fatal error:\", error);\n process.exit(1);\n});\n"],"mappings":";;;;;;;;;;;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;;AAwBA,SAAgB,gBAA2B;CACzC,OAAO;EACL,gBAAA;EACA,iBAAiB,QAAQ,IAAI,6BAA6B;EAC1D;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF;;;;;;;;;AAUA,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,8BAA8B;CACnC,GAAG,oBAAoB,MAAM;CAC7B,IAAI,CAAC,QACH,QAAQ,mBAAmB;AAE/B;;;AC3LA,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"}
|
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-
|
|
2
|
-
import { B as
|
|
1
|
+
import { a as printAuditResults, c as stopIdleProjects, d as buildDockerFirewallScript, f as buildDockerFirewallUnit, g as validateConfig, h as parseDuration, l as DOCKER_FIREWALL_SCRIPT, m as loadConfig, n as setup, o as runAudit, p as configureDockerFirewall, s as startIdleScheduler, t as audit, u as DOCKER_FIREWALL_UNIT } from "./setup-DxoFhMfT.js";
|
|
2
|
+
import { B as buildHostnameIndex, C as getIdleProjects, D as setProjectStatus, E as logAccess, G as startProject, H as getProjectInfo, J as checkAuth, K as stopProject, O as updateProjectAccess, Q as parseBearerToken, S as getDb, T as initDb, U as listProjects, V as getProjectFromHostname, W as loadProjectList, X as matchIp, Y as matchHostname, Z as parseBasicAuth, _ as loadProjectConfig, b as closeDb, g as getIdleTimeoutMs, q as watchProjectList, s as startServer, v as shouldNeverStop, w as getProject, x as getAccessLogs, y as cleanOldLogs } from "./server-BAjHxgqh.js";
|
|
3
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 };
|
|
@@ -294,42 +294,31 @@ function watchProjectList(projectListPath, onChange) {
|
|
|
294
294
|
if (eventType === "change") onChange();
|
|
295
295
|
});
|
|
296
296
|
}
|
|
297
|
-
/**
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
* the same thread.
|
|
306
|
-
*/
|
|
307
|
-
function ddev(args, timeoutMs) {
|
|
308
|
-
return new Promise((resolve) => {
|
|
309
|
-
execFile("ddev", args, {
|
|
310
|
-
encoding: "utf-8",
|
|
311
|
-
timeout: timeoutMs
|
|
312
|
-
}, (error, stdout) => {
|
|
313
|
-
if (error) {
|
|
314
|
-
console.error(`ddev ${args.join(" ")} failed:`, error.message);
|
|
315
|
-
resolve({
|
|
316
|
-
ok: false,
|
|
317
|
-
stdout: stdout ?? ""
|
|
318
|
-
});
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
297
|
+
/** The real runner, used unless a caller passes its own. */
|
|
298
|
+
var nodeDdevRunner = (args, timeoutMs) => new Promise((resolve) => {
|
|
299
|
+
execFile("ddev", args, {
|
|
300
|
+
encoding: "utf-8",
|
|
301
|
+
timeout: timeoutMs
|
|
302
|
+
}, (error, stdout) => {
|
|
303
|
+
if (error) {
|
|
304
|
+
console.error(`ddev ${args.join(" ")} failed:`, error.message);
|
|
321
305
|
resolve({
|
|
322
|
-
ok:
|
|
306
|
+
ok: false,
|
|
323
307
|
stdout: stdout ?? ""
|
|
324
308
|
});
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
resolve({
|
|
312
|
+
ok: true,
|
|
313
|
+
stdout: stdout ?? ""
|
|
325
314
|
});
|
|
326
315
|
});
|
|
327
|
-
}
|
|
316
|
+
});
|
|
328
317
|
/**
|
|
329
318
|
* Get detailed project info using ddev describe
|
|
330
319
|
*/
|
|
331
|
-
async function getProjectInfo(name) {
|
|
332
|
-
const { ok, stdout } = await
|
|
320
|
+
async function getProjectInfo(name, run = nodeDdevRunner) {
|
|
321
|
+
const { ok, stdout } = await run([
|
|
333
322
|
"describe",
|
|
334
323
|
name,
|
|
335
324
|
"-j"
|
|
@@ -359,15 +348,15 @@ async function getProjectInfo(name) {
|
|
|
359
348
|
* Resolves when the project is up. Callers in the request path must not await
|
|
360
349
|
* it — respond first, then let this settle and record the outcome.
|
|
361
350
|
*/
|
|
362
|
-
async function startProject(name) {
|
|
363
|
-
const { ok } = await
|
|
351
|
+
async function startProject(name, run = nodeDdevRunner) {
|
|
352
|
+
const { ok } = await run(["start", name], 12e4);
|
|
364
353
|
return ok;
|
|
365
354
|
}
|
|
366
355
|
/**
|
|
367
356
|
* Stop a DDEV project
|
|
368
357
|
*/
|
|
369
|
-
async function stopProject(name) {
|
|
370
|
-
const { ok } = await
|
|
358
|
+
async function stopProject(name, run = nodeDdevRunner) {
|
|
359
|
+
const { ok } = await run(["stop", name], 6e4);
|
|
371
360
|
return ok;
|
|
372
361
|
}
|
|
373
362
|
/**
|
|
@@ -520,6 +509,8 @@ var nodeIo = {
|
|
|
520
509
|
};
|
|
521
510
|
//#endregion
|
|
522
511
|
//#region src/utils/db.ts
|
|
512
|
+
/** The real clock, used unless a caller passes its own. */
|
|
513
|
+
var systemClock = () => Date.now();
|
|
523
514
|
var db = null;
|
|
524
515
|
/**
|
|
525
516
|
* Initialize the database connection and schema
|
|
@@ -567,12 +558,12 @@ function closeDb() {
|
|
|
567
558
|
/**
|
|
568
559
|
* Update project last access time
|
|
569
560
|
*/
|
|
570
|
-
function updateProjectAccess(name) {
|
|
561
|
+
function updateProjectAccess(name, now = systemClock) {
|
|
571
562
|
getDb().prepare(`
|
|
572
563
|
INSERT INTO projects (name, last_access, status)
|
|
573
564
|
VALUES (?, ?, 'running')
|
|
574
565
|
ON CONFLICT(name) DO UPDATE SET last_access = excluded.last_access
|
|
575
|
-
`).run(name,
|
|
566
|
+
`).run(name, now());
|
|
576
567
|
}
|
|
577
568
|
/**
|
|
578
569
|
* Get project record
|
|
@@ -583,19 +574,19 @@ function getProject(name) {
|
|
|
583
574
|
/**
|
|
584
575
|
* Set project status
|
|
585
576
|
*/
|
|
586
|
-
function setProjectStatus(name, status) {
|
|
577
|
+
function setProjectStatus(name, status, now = systemClock) {
|
|
587
578
|
getDb().prepare(`
|
|
588
579
|
INSERT INTO projects (name, last_access, status)
|
|
589
580
|
VALUES (?, ?, ?)
|
|
590
581
|
ON CONFLICT(name) DO UPDATE SET status = excluded.status
|
|
591
|
-
`).run(name,
|
|
582
|
+
`).run(name, now(), status);
|
|
592
583
|
}
|
|
593
584
|
/**
|
|
594
585
|
* Get idle projects (last access older than threshold)
|
|
595
586
|
*/
|
|
596
|
-
function getIdleProjects(thresholdMs) {
|
|
587
|
+
function getIdleProjects(thresholdMs, now = systemClock) {
|
|
597
588
|
const database = getDb();
|
|
598
|
-
const cutoff =
|
|
589
|
+
const cutoff = now() - thresholdMs;
|
|
599
590
|
return database.prepare(`
|
|
600
591
|
SELECT name, last_access as lastAccess, status
|
|
601
592
|
FROM projects
|
|
@@ -626,9 +617,9 @@ function getAccessLogs(project, limit = 100) {
|
|
|
626
617
|
/**
|
|
627
618
|
* Clean old access logs (older than given days)
|
|
628
619
|
*/
|
|
629
|
-
function cleanOldLogs(days) {
|
|
620
|
+
function cleanOldLogs(days, now = systemClock) {
|
|
630
621
|
const database = getDb();
|
|
631
|
-
const cutoff =
|
|
622
|
+
const cutoff = now() - days * 24 * 60 * 60 * 1e3;
|
|
632
623
|
const result = database.prepare("DELETE FROM access_logs WHERE timestamp < ?").run(cutoff);
|
|
633
624
|
return Number(result.changes);
|
|
634
625
|
}
|
|
@@ -1028,12 +1019,12 @@ function loadTemplate(name) {
|
|
|
1028
1019
|
/**
|
|
1029
1020
|
* Get effective auth config for a project (merges global + per-project)
|
|
1030
1021
|
*/
|
|
1031
|
-
function getEffectiveAuthConfig(projectName) {
|
|
1032
|
-
if (!projectName) return
|
|
1033
|
-
const projectConfig = projectConfigs.get(projectName);
|
|
1034
|
-
if (!projectConfig?.auth_policy) return
|
|
1022
|
+
function getEffectiveAuthConfig(projectName, deps) {
|
|
1023
|
+
if (!projectName) return deps.auth;
|
|
1024
|
+
const projectConfig = deps.projectConfigs.get(projectName);
|
|
1025
|
+
if (!projectConfig?.auth_policy) return deps.auth;
|
|
1035
1026
|
return {
|
|
1036
|
-
...
|
|
1027
|
+
...deps.auth,
|
|
1037
1028
|
defaultPolicy: projectConfig.auth_policy
|
|
1038
1029
|
};
|
|
1039
1030
|
}
|
|
@@ -1041,14 +1032,14 @@ function getEffectiveAuthConfig(projectName) {
|
|
|
1041
1032
|
* Handle forward auth requests from Traefik
|
|
1042
1033
|
* Traefik sends the original request headers, we return 200 (allow) or 401 (deny)
|
|
1043
1034
|
*/
|
|
1044
|
-
function handleAuth(req, res) {
|
|
1035
|
+
function handleAuth(req, res, deps) {
|
|
1045
1036
|
const hostname = req.headers["x-forwarded-host"] ?? "";
|
|
1046
1037
|
const socketIp = req.socket.remoteAddress ?? "";
|
|
1047
1038
|
const forwardedFor = req.headers["x-forwarded-for"];
|
|
1048
1039
|
const authorization = req.headers["authorization"];
|
|
1049
1040
|
const path = req.headers["x-forwarded-uri"] ?? "/";
|
|
1050
|
-
const projectName = hostnameIndex.get(hostname);
|
|
1051
|
-
const authConfig = getEffectiveAuthConfig(projectName);
|
|
1041
|
+
const projectName = deps.hostnameIndex.get(hostname);
|
|
1042
|
+
const authConfig = getEffectiveAuthConfig(projectName, deps);
|
|
1052
1043
|
const clientIp = resolveClientIp(socketIp, forwardedFor, authConfig.trustedProxyHops);
|
|
1053
1044
|
if (checkAuth({
|
|
1054
1045
|
hostname,
|
|
@@ -1057,8 +1048,8 @@ function handleAuth(req, res) {
|
|
|
1057
1048
|
forwardedFor
|
|
1058
1049
|
}, authConfig).allowed) {
|
|
1059
1050
|
if (projectName) {
|
|
1060
|
-
updateProjectAccess(projectName);
|
|
1061
|
-
logAccess({
|
|
1051
|
+
deps.updateProjectAccess(projectName);
|
|
1052
|
+
deps.logAccess({
|
|
1062
1053
|
project: projectName,
|
|
1063
1054
|
timestamp: Date.now(),
|
|
1064
1055
|
ip: clientIp,
|
|
@@ -1077,17 +1068,17 @@ function handleAuth(req, res) {
|
|
|
1077
1068
|
* Handle errors middleware requests (502 from Traefik)
|
|
1078
1069
|
* When a project is stopped, Traefik returns 502. We show a waiting page and start the project.
|
|
1079
1070
|
*/
|
|
1080
|
-
async function handleErrors(req, res) {
|
|
1071
|
+
async function handleErrors(req, res, deps) {
|
|
1081
1072
|
const hostname = req.headers["x-forwarded-host"] ?? req.headers.host ?? "";
|
|
1082
|
-
const projectName = hostnameIndex.get(hostname);
|
|
1073
|
+
const projectName = deps.hostnameIndex.get(hostname);
|
|
1083
1074
|
if (!projectName) {
|
|
1084
|
-
const template = loadTemplate("error");
|
|
1075
|
+
const template = deps.loadTemplate("error");
|
|
1085
1076
|
res.writeHead(404, { "Content-Type": "text/html" });
|
|
1086
1077
|
res.end(template.replace("{{message}}", "Project not found"));
|
|
1087
1078
|
return;
|
|
1088
1079
|
}
|
|
1089
|
-
if (getProject(projectName)?.status === "starting") {
|
|
1090
|
-
const template = loadTemplate("wait");
|
|
1080
|
+
if (deps.getProject(projectName)?.status === "starting") {
|
|
1081
|
+
const template = deps.loadTemplate("wait");
|
|
1091
1082
|
res.writeHead(503, {
|
|
1092
1083
|
"Content-Type": "text/html",
|
|
1093
1084
|
"Retry-After": "5"
|
|
@@ -1095,32 +1086,32 @@ async function handleErrors(req, res) {
|
|
|
1095
1086
|
res.end(template.replace(/\{\{project\}\}/g, projectName).replace(/\{\{hostname\}\}/g, hostname));
|
|
1096
1087
|
return;
|
|
1097
1088
|
}
|
|
1098
|
-
setProjectStatus(projectName, "starting");
|
|
1099
|
-
const template = loadTemplate("wait");
|
|
1089
|
+
deps.setProjectStatus(projectName, "starting");
|
|
1090
|
+
const template = deps.loadTemplate("wait");
|
|
1100
1091
|
res.writeHead(503, {
|
|
1101
1092
|
"Content-Type": "text/html",
|
|
1102
1093
|
"Retry-After": "5"
|
|
1103
1094
|
});
|
|
1104
1095
|
res.end(template.replace(/\{\{project\}\}/g, projectName).replace(/\{\{hostname\}\}/g, hostname));
|
|
1105
|
-
startProject(projectName).then((success) => {
|
|
1106
|
-
setProjectStatus(projectName, success ? "running" : "stopped");
|
|
1096
|
+
deps.startProject(projectName).then((success) => {
|
|
1097
|
+
deps.setProjectStatus(projectName, success ? "running" : "stopped");
|
|
1107
1098
|
}).catch((error) => {
|
|
1108
|
-
setProjectStatus(projectName, "stopped");
|
|
1099
|
+
deps.setProjectStatus(projectName, "stopped");
|
|
1109
1100
|
console.error(`Could not start ${projectName}:`, error);
|
|
1110
1101
|
});
|
|
1111
1102
|
}
|
|
1112
1103
|
/**
|
|
1113
1104
|
* Handle status polling requests
|
|
1114
1105
|
*/
|
|
1115
|
-
async function handleStatus(req, res) {
|
|
1106
|
+
async function handleStatus(req, res, deps) {
|
|
1116
1107
|
const projectName = new URL(req.url ?? "/", `http://${req.headers.host}`).searchParams.get("project");
|
|
1117
1108
|
if (!projectName) {
|
|
1118
1109
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1119
1110
|
res.end(JSON.stringify({ error: "Missing project parameter" }));
|
|
1120
1111
|
return;
|
|
1121
1112
|
}
|
|
1122
|
-
const info = await getProjectInfo(projectName);
|
|
1123
|
-
const record = getProject(projectName);
|
|
1113
|
+
const info = await deps.getProjectInfo(projectName);
|
|
1114
|
+
const record = deps.getProject(projectName);
|
|
1124
1115
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1125
1116
|
res.end(JSON.stringify({
|
|
1126
1117
|
name: projectName,
|
|
@@ -1148,18 +1139,18 @@ function routePath(target) {
|
|
|
1148
1139
|
/**
|
|
1149
1140
|
* Request handler
|
|
1150
1141
|
*/
|
|
1151
|
-
async function handleRequest(req, res) {
|
|
1142
|
+
async function handleRequest(req, res, deps) {
|
|
1152
1143
|
const path = routePath(req.url);
|
|
1153
1144
|
try {
|
|
1154
|
-
if (path === "/__auth__" || path.startsWith("/__auth__/")) handleAuth(req, res);
|
|
1155
|
-
else if (path === "/__status__" || path.startsWith("/__status__/")) await handleStatus(req, res);
|
|
1145
|
+
if (path === "/__auth__" || path.startsWith("/__auth__/")) handleAuth(req, res, deps);
|
|
1146
|
+
else if (path === "/__status__" || path.startsWith("/__status__/")) await handleStatus(req, res, deps);
|
|
1156
1147
|
else if (path === "/__health__") {
|
|
1157
1148
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1158
1149
|
res.end(JSON.stringify({
|
|
1159
1150
|
status: "ok",
|
|
1160
1151
|
version: "__VERSION__"
|
|
1161
1152
|
}));
|
|
1162
|
-
} else await handleErrors(req, res);
|
|
1153
|
+
} else await handleErrors(req, res, deps);
|
|
1163
1154
|
} catch (error) {
|
|
1164
1155
|
console.error("Request error:", error);
|
|
1165
1156
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
@@ -1194,8 +1185,20 @@ function startServer(agentConfig) {
|
|
|
1194
1185
|
console.log("Project list changed, reloading...");
|
|
1195
1186
|
reloadProjects();
|
|
1196
1187
|
});
|
|
1188
|
+
const deps = () => ({
|
|
1189
|
+
auth: config.auth,
|
|
1190
|
+
hostnameIndex,
|
|
1191
|
+
projectConfigs,
|
|
1192
|
+
loadTemplate,
|
|
1193
|
+
updateProjectAccess,
|
|
1194
|
+
logAccess,
|
|
1195
|
+
getProject,
|
|
1196
|
+
setProjectStatus,
|
|
1197
|
+
startProject,
|
|
1198
|
+
getProjectInfo
|
|
1199
|
+
});
|
|
1197
1200
|
createServer((req, res) => {
|
|
1198
|
-
handleRequest(req, res).catch((error) => {
|
|
1201
|
+
handleRequest(req, res, deps()).catch((error) => {
|
|
1199
1202
|
console.error("Unhandled error:", error);
|
|
1200
1203
|
if (!res.headersSent) {
|
|
1201
1204
|
res.writeHead(500);
|
|
@@ -1209,6 +1212,6 @@ function startServer(agentConfig) {
|
|
|
1209
1212
|
});
|
|
1210
1213
|
}
|
|
1211
1214
|
//#endregion
|
|
1212
|
-
export {
|
|
1215
|
+
export { error as A, buildHostnameIndex as B, getIdleProjects as C, setProjectStatus as D, logAccess as E, resetSteps as F, startProject as G, getProjectInfo as H, setDryRun as I, checkAuth as J, stopProject as K, step as L, execSilent as M, info as N, updateProjectAccess as O, isRoot as P, parseBearerToken as Q, success as R, getDb as S, initDb as T, listProjects as U, getProjectFromHostname as V, loadProjectList as W, matchIp as X, matchHostname as Y, parseBasicAuth as Z, loadProjectConfig as _, handleStatus as a, closeDb as b, addDdevAptRepo as c, getDockerGatewayIp as d, installDdev as f, getIdleTimeoutMs as g, readToolPorts as h, handleRequest as i, exec as j, nodeIo as k, configureDdev as l, purgeTraefikAcmeStorage as m, handleAuth as n, routePath as o, installSystemDeps as p, watchProjectList as q, handleErrors as r, startServer as s, getEffectiveAuthConfig as t, configureTraefik as u, shouldNeverStop as v, getProject as w, getAccessLogs as x, cleanOldLogs as y, warn as z };
|
|
1213
1216
|
|
|
1214
|
-
//# sourceMappingURL=server-
|
|
1217
|
+
//# sourceMappingURL=server-BAjHxgqh.js.map
|