@studiometa/trafic-agent 0.1.23 → 0.1.25

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 CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { _ as validateConfig, c as info, d as success, f as warn, h as loadConfig, i as runPendingMigrations, l as isRoot, n as setup, p as startIdleScheduler, r as listMigrations, s as exec, t as audit, u as step } from "./setup-BB7Laws8.js";
3
- import { o as closeDb, t as startServer } from "./server-D6zlodZz.js";
2
+ import { _ as validateConfig, c as info, d as success, f as warn, h as loadConfig, i as runPendingMigrations, l as isRoot, n as setup, p as startIdleScheduler, r as listMigrations, s as exec, t as audit, u as step } from "./setup-D61uhYiJ.js";
3
+ import { o as closeDb, t as startServer } from "./server-VlzPosuk.js";
4
4
  import { readFileSync } from "node:fs";
5
5
  import { execSync, spawnSync } from "node:child_process";
6
6
  import { parseArgs } from "node:util";
7
+ //#region src/setup/upgrade.ts
7
8
  /**
8
9
  * Fetch the latest published version of @studiometa/trafic-agent from the npm registry.
9
10
  * Returns null if the registry is unreachable.
@@ -72,10 +73,11 @@ function getInstalledVersion() {
72
73
  * This function never returns — it replaces the current process.
73
74
  */
74
75
  function reExecNewBinary(args) {
75
- const result = spawnSync(execSync("which trafic-agent", {
76
+ const binary = execSync("which trafic-agent", {
76
77
  encoding: "utf-8",
77
78
  stdio: "pipe"
78
- }).trim(), args, {
79
+ }).trim();
80
+ const result = spawnSync(binary, args, {
79
81
  stdio: "inherit",
80
82
  env: {
81
83
  ...process.env,
@@ -106,7 +108,7 @@ function runUpgrade(dryRun = false, reExecArgs) {
106
108
  }
107
109
  const alreadyReExeced = process.env["TRAFIC_UPGRADE_REEXEC"] === "1";
108
110
  step("Check for updates");
109
- const current = "0.1.23";
111
+ const current = "0.1.25";
110
112
  info(`Current version: ${current}`);
111
113
  const latest = fetchLatestVersion();
112
114
  if (!latest) warn("Could not reach npm registry — skipping version check");
@@ -128,6 +130,8 @@ function runUpgrade(dryRun = false, reExecArgs) {
128
130
  restartAgentService(dryRun);
129
131
  if (!dryRun) success("Service restarted");
130
132
  }
133
+ //#endregion
134
+ //#region src/cli.ts
131
135
  var HELP = `
132
136
  trafic-agent — Server agent for DDEV preview environments
133
137
 
@@ -183,7 +187,7 @@ function printHelp() {
183
187
  console.log(HELP.trim());
184
188
  }
185
189
  function printVersion() {
186
- console.log("0.1.23");
190
+ console.log("0.1.25");
187
191
  }
188
192
  async function runStart(values) {
189
193
  const config = loadConfig(values.config);
@@ -302,5 +306,6 @@ main().catch((error) => {
302
306
  console.error("Fatal error:", error);
303
307
  process.exit(1);
304
308
  });
309
+ //#endregion
305
310
 
306
311
  //# sourceMappingURL=cli.js.map
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-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 --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 --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\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 email: values.email as string | undefined,\n noHardening: values[\"no-hardening\"] 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-docker\": { type: \"boolean\" },\n \"no-ddev\": { type: \"boolean\" },\n \"ssh-users\": { 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 * 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-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 --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 --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\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 email: values.email as string | undefined,\n noHardening: values[\"no-hardening\"] 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-docker\": { type: \"boolean\" },\n \"no-ddev\": { type: \"boolean\" },\n \"ssh-users\": { 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;;;;;;;;;AAUA,SAAgB,WAAW,SAAS,OAAO,YAA6B;CACtE,IAAI,CAAC,OAAO,KAAK,CAAC,QAAQ;EACxB,QAAQ,MAAM,uDAAuD;EACrE,QAAQ,IAAI,kCAAkC;EAC9C,QAAQ,KAAK,CAAC;CAChB;CAGA,MAAM,kBAAkB,QAAQ,IAAI,6BAA6B;CAGjE,KAAK,mBAAmB;CAExB,MAAM,UAAA;CACN,KAAK,oBAAoB,SAAS;CAElC,MAAM,SAAS,mBAAmB;CAElC,IAAI,CAAC,QACH,KAAK,uDAAuD;MACvD,IAAI,QAAQ,SAAS,MAAM,GAAG;EACnC,QAAQ,0BAA0B,QAAQ;EAG1C,KAAK,wBAAwB;EAC7B,mBAAmB,MAAM;EAEzB,IAAI,CAAC,QAAQ;GAGX,MAAM,mBAAmB,oBAAoB;GAE7C,IAAI,CAAC,mBAAmB,oBAAoB,QAAQ,SAAS,gBAAgB,GAAG;IAC9E,QAAQ,sCAAsC,kBAAkB;IAIhE,gBAAgB,cAAc,CAAC,SAAS,CAAC;GAC3C,OAAO,IAAI,kBACT,QAAQ,sCAAsC,kBAAkB;EAEpE;CACF,OACE,QAAQ,uBAAuB,QAAQ,EAAE;CAI3C,KAAK,wBAAwB;CAC7B,qBAAqB,MAAM;CAG3B,KAAK,8BAA8B;CACnC,oBAAoB,MAAM;CAC1B,IAAI,CAAC,QACH,QAAQ,mBAAmB;AAE/B;;;ACnJA,IAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDb,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;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,OAAO,OAAO;EACd,aAAa,OAAO;EACpB,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,aAAa,EAAE,MAAM,UAAU;GAC/B,WAAW,EAAE,MAAM,UAAU;GAC7B,aAAa,EAAE,MAAM,SAAS;GAC9B,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 { _ as validateConfig, a as printAuditResults, g as parseDuration, h as loadConfig, m as stopIdleProjects, n as setup, o as runAudit, p as startIdleScheduler, t as audit } from "./setup-BB7Laws8.js";
2
- import { C as checkAuth, D as parseBearerToken, E as parseBasicAuth, S as watchProjectList, T as matchIp, _ as getProjectInfo, a as cleanOldLogs, b as startProject, c as getDb, d as initDb, f as logAccess, g as getProjectFromHostname, h as buildHostnameIndex, i as shouldNeverStop, l as getIdleProjects, m as updateProjectAccess, n as getIdleTimeoutMs, o as closeDb, p as setProjectStatus, r as loadProjectConfig, s as getAccessLogs, t as startServer, u as getProject, v as listProjects, w as matchHostname, x as stopProject, y as loadProjectList } from "./server-D6zlodZz.js";
1
+ import { _ as validateConfig, a as printAuditResults, g as parseDuration, h as loadConfig, m as stopIdleProjects, n as setup, o as runAudit, p as startIdleScheduler, t as audit } from "./setup-D61uhYiJ.js";
2
+ import { C as checkAuth, D as parseBearerToken, E as parseBasicAuth, S as watchProjectList, T as matchIp, _ as getProjectInfo, a as cleanOldLogs, b as startProject, c as getDb, d as initDb, f as logAccess, g as getProjectFromHostname, h as buildHostnameIndex, i as shouldNeverStop, l as getIdleProjects, m as updateProjectAccess, n as getIdleTimeoutMs, o as closeDb, p as setProjectStatus, r as loadProjectConfig, s as getAccessLogs, t as startServer, u as getProject, v as listProjects, w as matchHostname, x as stopProject, y as loadProjectList } from "./server-VlzPosuk.js";
3
3
  export { audit, buildHostnameIndex, checkAuth, cleanOldLogs, closeDb, getAccessLogs, getDb, getIdleProjects, getIdleTimeoutMs, getProject, getProjectFromHostname, getProjectInfo, initDb, listProjects, loadConfig, loadProjectConfig, loadProjectList, logAccess, matchHostname, matchIp, parseBasicAuth, parseBearerToken, parseDuration, printAuditResults, runAudit, setProjectStatus, setup, shouldNeverStop, startIdleScheduler, startProject, startServer, stopIdleProjects, stopProject, updateProjectAccess, validateConfig, watchProjectList };
@@ -4,6 +4,7 @@ import { DatabaseSync } from "node:sqlite";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { createServer } from "node:http";
6
6
  import { fileURLToPath } from "node:url";
7
+ //#region src/utils/auth.ts
7
8
  /**
8
9
  * Check if an IP matches a pattern (supports CIDR notation)
9
10
  */
@@ -200,6 +201,8 @@ function applyDefaultPolicy(config, ip, authorization) {
200
201
  };
201
202
  }
202
203
  }
204
+ //#endregion
205
+ //#region src/utils/ddev.ts
203
206
  function parseSimpleYaml$1(content) {
204
207
  const result = {};
205
208
  const lines = content.split("\n");
@@ -331,6 +334,8 @@ function listProjects() {
331
334
  return [];
332
335
  }
333
336
  }
337
+ //#endregion
338
+ //#region src/utils/db.ts
334
339
  var db = null;
335
340
  /**
336
341
  * Initialize the database connection and schema
@@ -443,6 +448,8 @@ function cleanOldLogs(days) {
443
448
  const result = database.prepare("DELETE FROM access_logs WHERE timestamp < ?").run(cutoff);
444
449
  return Number(result.changes);
445
450
  }
451
+ //#endregion
452
+ //#region src/utils/project-config.ts
446
453
  /**
447
454
  * Simple YAML parser for config.trafic.yaml
448
455
  * Only supports simple key: value pairs (no nesting, no arrays)
@@ -507,6 +514,8 @@ function getIdleTimeoutMs(config) {
507
514
  const ms = (hours * 3600 + minutes * 60 + seconds) * 1e3;
508
515
  return ms > 0 ? ms : void 0;
509
516
  }
517
+ //#endregion
518
+ //#region src/server.ts
510
519
  var __dirname = dirname(fileURLToPath(import.meta.url));
511
520
  var config;
512
521
  var projectList;
@@ -599,7 +608,8 @@ async function handleErrors(req, res) {
599
608
  });
600
609
  res.end(template.replace(/\{\{project\}\}/g, projectName).replace(/\{\{hostname\}\}/g, hostname));
601
610
  setTimeout(() => {
602
- setProjectStatus(projectName, startProject(projectName) ? "running" : "stopped");
611
+ const success = startProject(projectName);
612
+ setProjectStatus(projectName, success ? "running" : "stopped");
603
613
  }, 100);
604
614
  }
605
615
  /**
@@ -683,6 +693,7 @@ function startServer(agentConfig) {
683
693
  console.log(`Projects: ${projectList.size}`);
684
694
  });
685
695
  }
696
+ //#endregion
686
697
  export { checkAuth as C, parseBearerToken as D, parseBasicAuth as E, watchProjectList as S, matchIp as T, getProjectInfo as _, cleanOldLogs as a, startProject as b, getDb as c, initDb as d, logAccess as f, getProjectFromHostname as g, buildHostnameIndex as h, shouldNeverStop as i, getIdleProjects as l, updateProjectAccess as m, getIdleTimeoutMs as n, closeDb as o, setProjectStatus as p, loadProjectConfig as r, getAccessLogs as s, startServer as t, getProject as u, listProjects as v, matchHostname as w, stopProject as x, loadProjectList as y };
687
698
 
688
- //# sourceMappingURL=server-D6zlodZz.js.map
699
+ //# sourceMappingURL=server-VlzPosuk.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"server-D6zlodZz.js","names":[],"sources":["../src/utils/auth.ts","../src/utils/ddev.ts","../src/utils/db.ts","../src/utils/project-config.ts","../src/server.ts"],"sourcesContent":["import type { AuthConfig, AuthRequest, AuthResult, AuthRule } from \"../types.js\";\n\n/**\n * Check if an IP matches a pattern (supports CIDR notation)\n */\nexport function matchIp(ip: string, pattern: string): boolean {\n // Exact match\n if (ip === pattern) return true;\n\n // CIDR notation\n if (pattern.includes(\"/\")) {\n return matchCidr(ip, pattern);\n }\n\n // Wildcard (e.g., 192.168.*)\n if (pattern.includes(\"*\")) {\n const regex = new RegExp(\n \"^\" + pattern.replace(/\\./g, \"\\\\.\").replace(/\\*/g, \".*\") + \"$\",\n );\n return regex.test(ip);\n }\n\n return false;\n}\n\n/**\n * Match IP against CIDR range\n */\nfunction matchCidr(ip: string, cidr: string): boolean {\n const [range, bits] = cidr.split(\"/\");\n const mask = parseInt(bits, 10);\n\n const ipNum = ipToNumber(ip);\n const rangeNum = ipToNumber(range);\n\n if (ipNum === null || rangeNum === null) return false;\n\n const maskNum = ~((1 << (32 - mask)) - 1);\n return (ipNum & maskNum) === (rangeNum & maskNum);\n}\n\n/**\n * Convert IP string to number\n */\nfunction ipToNumber(ip: string): number | null {\n const parts = ip.split(\".\");\n if (parts.length !== 4) return null;\n\n let num = 0;\n for (const part of parts) {\n const n = parseInt(part, 10);\n if (isNaN(n) || n < 0 || n > 255) return null;\n num = (num << 8) | n;\n }\n return num >>> 0; // Convert to unsigned\n}\n\n/**\n * Match hostname against a glob pattern\n */\nexport function matchHostname(hostname: string, pattern: string): boolean {\n // Exact match\n if (hostname === pattern) return true;\n\n // Glob pattern\n const regex = new RegExp(\n \"^\" +\n pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\*/g, \".*\")\n .replace(/\\?/g, \".\") +\n \"$\",\n );\n return regex.test(hostname);\n}\n\n/**\n * Parse Basic auth header\n */\nexport function parseBasicAuth(\n header: string,\n): { username: string; password: string } | null {\n if (!header.startsWith(\"Basic \")) return null;\n\n try {\n const encoded = header.slice(6);\n const decoded = Buffer.from(encoded, \"base64\").toString(\"utf-8\");\n const colonIndex = decoded.indexOf(\":\");\n if (colonIndex === -1) return null;\n\n return {\n username: decoded.slice(0, colonIndex),\n password: decoded.slice(colonIndex + 1),\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Parse Bearer token from header\n */\nexport function parseBearerToken(header: string): string | null {\n if (!header.startsWith(\"Bearer \")) return null;\n return header.slice(7);\n}\n\n/**\n * Check authentication for a request\n */\nexport function checkAuth(request: AuthRequest, config: AuthConfig): AuthResult {\n const { hostname, ip, authorization } = request;\n\n // Get real IP (handle X-Forwarded-For)\n const realIp = request.forwardedFor?.split(\",\")[0].trim() ?? ip;\n\n // Check IP whitelist first (always allows)\n for (const pattern of config.allowedIps) {\n if (matchIp(realIp, pattern)) {\n return { allowed: true, reason: \"ip\" };\n }\n }\n\n // Check per-hostname rules\n for (const rule of config.rules) {\n if (matchHostname(hostname, rule.match)) {\n return checkRule(rule, realIp, authorization, config);\n }\n }\n\n // Apply default policy\n return applyDefaultPolicy(config, realIp, authorization);\n}\n\n/**\n * Check a specific rule\n */\nfunction checkRule(\n rule: AuthRule,\n ip: string,\n authorization: string | undefined,\n config: AuthConfig,\n): AuthResult {\n // Rule-specific IP whitelist\n if (rule.allowedIps) {\n for (const pattern of rule.allowedIps) {\n if (matchIp(ip, pattern)) {\n return { allowed: true, reason: \"rule\" };\n }\n }\n }\n\n switch (rule.policy) {\n case \"allow\":\n return { allowed: true, reason: \"rule\" };\n\n case \"deny\":\n return { allowed: false, reason: \"rule\" };\n\n case \"token\": {\n if (!authorization) return { allowed: false, reason: \"rule\" };\n const token = parseBearerToken(authorization);\n const tokens = rule.tokens ?? config.tokens;\n if (token && tokens.includes(token)) {\n return { allowed: true, reason: \"token\" };\n }\n return { allowed: false, reason: \"rule\" };\n }\n\n case \"basic\": {\n if (!authorization) return { allowed: false, reason: \"rule\" };\n const creds = parseBasicAuth(authorization);\n if (creds) {\n const credString = `${creds.username}:${creds.password}`;\n if (config.basicAuth.includes(credString)) {\n return { allowed: true, reason: \"basic\" };\n }\n }\n return { allowed: false, reason: \"rule\" };\n }\n\n default:\n return { allowed: false, reason: \"rule\" };\n }\n}\n\n/**\n * Apply default policy\n */\nfunction applyDefaultPolicy(\n config: AuthConfig,\n ip: string,\n authorization: string | undefined,\n): AuthResult {\n switch (config.defaultPolicy) {\n case \"allow\":\n return { allowed: true, reason: \"default\" };\n\n case \"deny\":\n return { allowed: false, reason: \"default\" };\n\n case \"basic\": {\n if (!authorization) return { allowed: false, reason: \"default\" };\n const creds = parseBasicAuth(authorization);\n if (creds) {\n const credString = `${creds.username}:${creds.password}`;\n if (config.basicAuth.includes(credString)) {\n return { allowed: true, reason: \"basic\" };\n }\n }\n // Also check tokens as fallback\n const token = parseBearerToken(authorization);\n if (token && config.tokens.includes(token)) {\n return { allowed: true, reason: \"token\" };\n }\n return { allowed: false, reason: \"default\" };\n }\n\n case \"token\": {\n if (!authorization) return { allowed: false, reason: \"default\" };\n const token = parseBearerToken(authorization);\n if (token && config.tokens.includes(token)) {\n return { allowed: true, reason: \"token\" };\n }\n return { allowed: false, reason: \"default\" };\n }\n\n default:\n return { allowed: false, reason: \"default\" };\n }\n}\n","import { readFileSync, existsSync, watch } from \"node:fs\";\nimport { execSync } from \"node:child_process\";\nimport type { DdevProject } from \"../types.js\";\n\n// Simple YAML parser for project_list.yaml (avoid dependencies)\n// The file format is simple: key: value pairs with quoted strings\nfunction parseSimpleYaml(content: string): Record<string, string> {\n const result: Record<string, string> = {};\n const lines = content.split(\"\\n\");\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n\n const colonIndex = trimmed.indexOf(\":\");\n if (colonIndex === -1) continue;\n\n const key = trimmed.slice(0, colonIndex).trim();\n let value = trimmed.slice(colonIndex + 1).trim();\n\n // Remove quotes\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n value = value.slice(1, -1);\n }\n\n result[key] = value;\n }\n\n return result;\n}\n\n/**\n * Load projects from DDEV's project_list.yaml\n * Returns a map of project name -> app root path\n */\nexport function loadProjectList(\n projectListPath: string,\n): Map<string, string> {\n if (!existsSync(projectListPath)) {\n return new Map();\n }\n\n const content = readFileSync(projectListPath, \"utf-8\");\n const parsed = parseSimpleYaml(content);\n\n return new Map(Object.entries(parsed));\n}\n\n/**\n * Build a hostname -> project name index\n */\nexport function buildHostnameIndex(\n projects: Map<string, string>,\n tld: string,\n): Map<string, string> {\n const index = new Map<string, string>();\n\n for (const [name] of projects) {\n // Primary hostname: project-name.tld\n index.set(`${name}.${tld}`, name);\n }\n\n return index;\n}\n\n/**\n * Get project name from hostname\n */\nexport function getProjectFromHostname(\n hostname: string,\n index: Map<string, string>,\n): string | undefined {\n return index.get(hostname);\n}\n\n/**\n * Watch project_list.yaml for changes\n */\nexport function watchProjectList(\n projectListPath: string,\n onChange: () => void,\n): void {\n if (!existsSync(projectListPath)) {\n console.warn(`Project list not found: ${projectListPath}`);\n return;\n }\n\n watch(projectListPath, (eventType) => {\n if (eventType === \"change\") {\n onChange();\n }\n });\n}\n\n/**\n * Get detailed project info using ddev describe\n */\nexport function getProjectInfo(name: string): DdevProject | undefined {\n try {\n const output = execSync(`ddev describe ${name} -j`, {\n encoding: \"utf-8\",\n timeout: 10000,\n });\n const data = JSON.parse(output);\n\n if (!data.raw) return undefined;\n\n const raw = data.raw;\n return {\n name: raw.name,\n status: raw.status,\n appRoot: raw.approot,\n httpURLs: raw.httpURLs ?? [],\n httpsURLs: raw.httpsURLs ?? [],\n type: raw.type,\n phpVersion: raw.php_version,\n dbType: raw.dbinfo?.dbType,\n };\n } catch {\n return undefined;\n }\n}\n\n/**\n * Start a DDEV project\n */\nexport function startProject(name: string): boolean {\n try {\n execSync(`ddev start ${name}`, {\n encoding: \"utf-8\",\n timeout: 120000, // 2 minutes\n stdio: \"pipe\",\n });\n return true;\n } catch (error) {\n console.error(`Failed to start project ${name}:`, error);\n return false;\n }\n}\n\n/**\n * Stop a DDEV project\n */\nexport function stopProject(name: string): boolean {\n try {\n execSync(`ddev stop ${name}`, {\n encoding: \"utf-8\",\n timeout: 60000, // 1 minute\n stdio: \"pipe\",\n });\n return true;\n } catch (error) {\n console.error(`Failed to stop project ${name}:`, error);\n return false;\n }\n}\n\n/**\n * List all DDEV projects with their status\n */\nexport function listProjects(): DdevProject[] {\n try {\n const output = execSync(\"ddev list -j\", {\n encoding: \"utf-8\",\n timeout: 30000,\n });\n const data = JSON.parse(output);\n\n if (!Array.isArray(data)) return [];\n\n return data.map((p: Record<string, unknown>) => ({\n name: p.name as string,\n status: p.status as DdevProject[\"status\"],\n appRoot: p.approot as string,\n httpURLs: (p.httpURLs as string[]) ?? [],\n httpsURLs: (p.httpsURLs as string[]) ?? [],\n type: p.type as string,\n }));\n } catch {\n return [];\n }\n}\n","import { DatabaseSync } from \"node:sqlite\";\nimport type { ProjectRecord, AccessLog } from \"../types.js\";\n\nlet db: DatabaseSync | null = null;\n\n/**\n * Initialize the database connection and schema\n */\nexport function initDb(dbPath: string): DatabaseSync {\n db = new DatabaseSync(dbPath);\n db.exec(\"PRAGMA journal_mode=WAL\");\n\n // Create tables\n db.exec(`\n CREATE TABLE IF NOT EXISTS projects (\n name TEXT PRIMARY KEY,\n last_access INTEGER NOT NULL,\n status TEXT NOT NULL DEFAULT 'stopped'\n );\n\n CREATE TABLE IF NOT EXISTS access_logs (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project TEXT NOT NULL,\n timestamp INTEGER NOT NULL,\n ip TEXT NOT NULL,\n user_agent TEXT NOT NULL,\n path TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_access_logs_project ON access_logs(project);\n CREATE INDEX IF NOT EXISTS idx_access_logs_timestamp ON access_logs(timestamp);\n `);\n\n return db;\n}\n\n/**\n * Get the database instance\n */\nexport function getDb(): DatabaseSync {\n if (!db) {\n throw new Error(\"Database not initialized. Call initDb first.\");\n }\n return db;\n}\n\n/**\n * Close the database connection\n */\nexport function closeDb(): void {\n if (db) {\n db.close();\n db = null;\n }\n}\n\n/**\n * Update project last access time\n */\nexport function updateProjectAccess(name: string): void {\n const database = getDb();\n const stmt = database.prepare(`\n INSERT INTO projects (name, last_access, status)\n VALUES (?, ?, 'running')\n ON CONFLICT(name) DO UPDATE SET last_access = excluded.last_access\n `);\n stmt.run(name, Date.now());\n}\n\n/**\n * Get project record\n */\nexport function getProject(name: string): ProjectRecord | undefined {\n const database = getDb();\n const stmt = database.prepare(\n \"SELECT name, last_access as lastAccess, status FROM projects WHERE name = ?\",\n );\n return stmt.get(name) as unknown as ProjectRecord | undefined;\n}\n\n/**\n * Set project status\n */\nexport function setProjectStatus(\n name: string,\n status: ProjectRecord[\"status\"],\n): void {\n const database = getDb();\n const stmt = database.prepare(`\n INSERT INTO projects (name, last_access, status)\n VALUES (?, ?, ?)\n ON CONFLICT(name) DO UPDATE SET status = excluded.status\n `);\n stmt.run(name, Date.now(), status);\n}\n\n/**\n * Get idle projects (last access older than threshold)\n */\nexport function getIdleProjects(thresholdMs: number): ProjectRecord[] {\n const database = getDb();\n const cutoff = Date.now() - thresholdMs;\n const stmt = database.prepare(`\n SELECT name, last_access as lastAccess, status\n FROM projects\n WHERE last_access < ? AND status = 'running'\n `);\n return stmt.all(cutoff) as unknown as ProjectRecord[];\n}\n\n/**\n * Log an access\n */\nexport function logAccess(log: Omit<AccessLog, \"id\">): void {\n const database = getDb();\n const stmt = database.prepare(`\n INSERT INTO access_logs (project, timestamp, ip, user_agent, path)\n VALUES (?, ?, ?, ?, ?)\n `);\n stmt.run(log.project, log.timestamp, log.ip, log.userAgent, log.path);\n}\n\n/**\n * Get recent access logs for a project\n */\nexport function getAccessLogs(project: string, limit = 100): AccessLog[] {\n const database = getDb();\n const stmt = database.prepare(`\n SELECT id, project, timestamp, ip, user_agent as userAgent, path\n FROM access_logs\n WHERE project = ?\n ORDER BY timestamp DESC\n LIMIT ?\n `);\n return stmt.all(project, limit) as unknown as AccessLog[];\n}\n\n/**\n * Clean old access logs (older than given days)\n */\nexport function cleanOldLogs(days: number): number {\n const database = getDb();\n const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;\n const stmt = database.prepare(\"DELETE FROM access_logs WHERE timestamp < ?\");\n const result = stmt.run(cutoff);\n return Number(result.changes);\n}\n","import { readFileSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/**\n * Per-project Trafic configuration\n * Read from .ddev/config.trafic.yaml\n */\nexport interface ProjectConfig {\n /** Auth policy override: allow | deny | basic | token */\n auth_policy?: \"allow\" | \"deny\" | \"basic\" | \"token\";\n /** Idle timeout override: \"never\" or duration like \"30m\", \"4h\" */\n idle_timeout?: string;\n}\n\n/**\n * Simple YAML parser for config.trafic.yaml\n * Only supports simple key: value pairs (no nesting, no arrays)\n */\nfunction parseSimpleYaml(content: string): Record<string, string> {\n const result: Record<string, string> = {};\n const lines = content.split(\"\\n\");\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Skip empty lines and comments\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n\n const colonIndex = trimmed.indexOf(\":\");\n if (colonIndex === -1) continue;\n\n const key = trimmed.slice(0, colonIndex).trim();\n let value = trimmed.slice(colonIndex + 1).trim();\n\n // Remove quotes if present\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n value = value.slice(1, -1);\n }\n\n result[key] = value;\n }\n\n return result;\n}\n\n/**\n * Load project-specific Trafic config from .ddev/config.trafic.yaml\n */\nexport function loadProjectConfig(projectDir: string): ProjectConfig {\n const configPath = join(projectDir, \".ddev\", \"config.trafic.yaml\");\n\n if (!existsSync(configPath)) {\n return {};\n }\n\n try {\n const content = readFileSync(configPath, \"utf-8\");\n const parsed = parseSimpleYaml(content);\n\n const config: ProjectConfig = {};\n\n // Validate auth_policy\n if (parsed.auth_policy) {\n const policy = parsed.auth_policy.toLowerCase();\n if ([\"allow\", \"deny\", \"basic\", \"token\"].includes(policy)) {\n config.auth_policy = policy as ProjectConfig[\"auth_policy\"];\n }\n }\n\n // Validate idle_timeout\n if (parsed.idle_timeout) {\n config.idle_timeout = parsed.idle_timeout;\n }\n\n return config;\n } catch (error) {\n console.warn(`Failed to load project config from ${configPath}:`, error);\n return {};\n }\n}\n\n/**\n * Check if a project should never be stopped (idle_timeout = \"never\")\n */\nexport function shouldNeverStop(config: ProjectConfig): boolean {\n return config.idle_timeout?.toLowerCase() === \"never\";\n}\n\n/**\n * Get idle timeout in milliseconds, or undefined to use global default\n */\nexport function getIdleTimeoutMs(config: ProjectConfig): number | undefined {\n if (!config.idle_timeout) return undefined;\n if (shouldNeverStop(config)) return undefined;\n\n // Parse duration string (e.g., \"30m\", \"4h\", \"2h30m\")\n const regex = /(?:(\\d+)h)?(?:(\\d+)m)?(?:(\\d+)s)?/;\n const match = config.idle_timeout.match(regex);\n if (!match) return undefined;\n\n const hours = parseInt(match[1] ?? \"0\", 10);\n const minutes = parseInt(match[2] ?? \"0\", 10);\n const seconds = parseInt(match[3] ?? \"0\", 10);\n\n const ms = (hours * 3600 + minutes * 60 + seconds) * 1000;\n return ms > 0 ? ms : undefined;\n}\n","import { createServer, type IncomingMessage, type ServerResponse } from \"node:http\";\nimport { readFileSync } from \"node:fs\";\nimport { resolve, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { AgentConfig, AuthConfig } from \"./types.js\";\nimport { checkAuth } from \"./utils/auth.js\";\nimport {\n loadProjectList,\n buildHostnameIndex,\n watchProjectList,\n startProject,\n getProjectInfo,\n} from \"./utils/ddev.js\";\nimport {\n initDb,\n updateProjectAccess,\n logAccess,\n setProjectStatus,\n getProject,\n} from \"./utils/db.js\";\nimport { loadProjectConfig } from \"./utils/project-config.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n// State\nlet config: AgentConfig;\nlet projectList: Map<string, string>;\nlet hostnameIndex: Map<string, string>;\n// Cache of per-project configs (project name -> config)\nconst projectConfigs = new Map<string, ReturnType<typeof loadProjectConfig>>();\n\n/**\n * Load HTML template\n */\nfunction loadTemplate(name: string): string {\n const templatePath = resolve(__dirname, \"..\", \"templates\", `${name}.html`);\n try {\n return readFileSync(templatePath, \"utf-8\");\n } catch {\n return `<html><body><h1>${name}</h1></body></html>`;\n }\n}\n\n/**\n * Get effective auth config for a project (merges global + per-project)\n */\nfunction getEffectiveAuthConfig(projectName: string | undefined): AuthConfig {\n if (!projectName) return config.auth;\n\n const projectConfig = projectConfigs.get(projectName);\n if (!projectConfig?.auth_policy) return config.auth;\n\n // Override default policy with project-specific policy\n return {\n ...config.auth,\n defaultPolicy: projectConfig.auth_policy,\n };\n}\n\n/**\n * Handle forward auth requests from Traefik\n * Traefik sends the original request headers, we return 200 (allow) or 401 (deny)\n */\nfunction handleAuth(req: IncomingMessage, res: ServerResponse): void {\n const hostname = req.headers[\"x-forwarded-host\"] as string ?? \"\";\n const ip = req.headers[\"x-forwarded-for\"] as string ?? req.socket.remoteAddress ?? \"\";\n const authorization = req.headers[\"authorization\"];\n const path = req.headers[\"x-forwarded-uri\"] as string ?? \"/\";\n\n // Find project from hostname\n const projectName = hostnameIndex.get(hostname);\n\n // Get effective auth config (global + per-project overrides)\n const authConfig = getEffectiveAuthConfig(projectName);\n\n const result = checkAuth(\n {\n hostname,\n ip: ip.split(\",\")[0].trim(),\n authorization,\n forwardedFor: ip,\n },\n authConfig,\n );\n\n if (result.allowed) {\n // Log access and update last access time\n if (projectName) {\n updateProjectAccess(projectName);\n logAccess({\n project: projectName,\n timestamp: Date.now(),\n ip: ip.split(\",\")[0].trim(),\n userAgent: req.headers[\"user-agent\"] ?? \"\",\n path,\n });\n }\n\n res.writeHead(200);\n res.end();\n } else {\n // Return 401 with WWW-Authenticate header for basic auth\n res.writeHead(401, {\n \"WWW-Authenticate\": 'Basic realm=\"Trafic\"',\n });\n res.end(\"Unauthorized\");\n }\n}\n\n/**\n * Handle errors middleware requests (502 from Traefik)\n * When a project is stopped, Traefik returns 502. We show a waiting page and start the project.\n */\nasync function handleErrors(\n req: IncomingMessage,\n res: ServerResponse,\n): Promise<void> {\n const hostname = req.headers[\"x-forwarded-host\"] as string ?? req.headers.host ?? \"\";\n const projectName = hostnameIndex.get(hostname);\n\n if (!projectName) {\n // Unknown project\n const template = loadTemplate(\"error\");\n res.writeHead(404, { \"Content-Type\": \"text/html\" });\n res.end(template.replace(\"{{message}}\", \"Project not found\"));\n return;\n }\n\n // Check if project is already starting\n const record = getProject(projectName);\n if (record?.status === \"starting\") {\n // Show waiting page\n const template = loadTemplate(\"wait\");\n res.writeHead(503, {\n \"Content-Type\": \"text/html\",\n \"Retry-After\": \"5\",\n });\n res.end(\n template\n .replace(/\\{\\{project\\}\\}/g, projectName)\n .replace(/\\{\\{hostname\\}\\}/g, hostname),\n );\n return;\n }\n\n // Mark as starting\n setProjectStatus(projectName, \"starting\");\n\n // Show waiting page immediately\n const template = loadTemplate(\"wait\");\n res.writeHead(503, {\n \"Content-Type\": \"text/html\",\n \"Retry-After\": \"5\",\n });\n res.end(\n template\n .replace(/\\{\\{project\\}\\}/g, projectName)\n .replace(/\\{\\{hostname\\}\\}/g, hostname),\n );\n\n // Start project in background\n setTimeout(() => {\n const success = startProject(projectName);\n setProjectStatus(projectName, success ? \"running\" : \"stopped\");\n }, 100);\n}\n\n/**\n * Handle status polling requests\n */\nfunction handleStatus(req: IncomingMessage, res: ServerResponse): void {\n const url = new URL(req.url ?? \"/\", `http://${req.headers.host}`);\n const projectName = url.searchParams.get(\"project\");\n\n if (!projectName) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Missing project parameter\" }));\n return;\n }\n\n const info = getProjectInfo(projectName);\n const record = getProject(projectName);\n\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n name: projectName,\n status: info?.status ?? record?.status ?? \"unknown\",\n ready: info?.status === \"running\",\n }),\n );\n}\n\n/**\n * Request handler\n */\nasync function handleRequest(\n req: IncomingMessage,\n res: ServerResponse,\n): Promise<void> {\n const url = req.url ?? \"/\";\n\n try {\n // Route requests\n if (url === \"/__auth__\" || url.startsWith(\"/__auth__/\")) {\n handleAuth(req, res);\n } else if (url === \"/__status__\" || url.startsWith(\"/__status__/\")) {\n handleStatus(req, res);\n } else if (url === \"/__health__\") {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"ok\", version: \"__VERSION__\" }));\n } else {\n // Default: errors middleware\n await handleErrors(req, res);\n }\n } catch (error) {\n console.error(\"Request error:\", error);\n res.writeHead(500, { \"Content-Type\": \"text/plain\" });\n res.end(\"Internal Server Error\");\n }\n}\n\n/**\n * Reload project list and their configs\n */\nfunction reloadProjects(): void {\n projectList = loadProjectList(config.projectListPath);\n hostnameIndex = buildHostnameIndex(projectList, config.tld);\n\n // Load per-project configs\n projectConfigs.clear();\n for (const [name, projectDir] of projectList) {\n const projectConfig = loadProjectConfig(projectDir);\n if (projectConfig.auth_policy || projectConfig.idle_timeout) {\n projectConfigs.set(name, projectConfig);\n console.log(` ${name}: auth=${projectConfig.auth_policy ?? \"default\"}, idle=${projectConfig.idle_timeout ?? \"default\"}`);\n }\n }\n\n console.log(`Loaded ${projectList.size} projects (${projectConfigs.size} with custom config)`);\n}\n\n/**\n * Start the agent server\n */\nexport function startServer(agentConfig: AgentConfig): void {\n config = agentConfig;\n\n // Initialize database\n initDb(config.dbPath);\n\n // Load projects\n reloadProjects();\n\n // Watch for changes\n watchProjectList(config.projectListPath, () => {\n console.log(\"Project list changed, reloading...\");\n reloadProjects();\n });\n\n // Create HTTP server\n const server = createServer((req, res) => {\n handleRequest(req, res).catch((error) => {\n console.error(\"Unhandled error:\", error);\n if (!res.headersSent) {\n res.writeHead(500);\n res.end(\"Internal Server Error\");\n }\n });\n });\n\n server.listen(config.port, () => {\n console.log(`Trafic agent listening on port ${config.port}`);\n console.log(`TLD: ${config.tld}`);\n console.log(`Projects: ${projectList.size}`);\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"server-VlzPosuk.js","names":[],"sources":["../src/utils/auth.ts","../src/utils/ddev.ts","../src/utils/db.ts","../src/utils/project-config.ts","../src/server.ts"],"sourcesContent":["import type { AuthConfig, AuthRequest, AuthResult, AuthRule } from \"../types.js\";\n\n/**\n * Check if an IP matches a pattern (supports CIDR notation)\n */\nexport function matchIp(ip: string, pattern: string): boolean {\n // Exact match\n if (ip === pattern) return true;\n\n // CIDR notation\n if (pattern.includes(\"/\")) {\n return matchCidr(ip, pattern);\n }\n\n // Wildcard (e.g., 192.168.*)\n if (pattern.includes(\"*\")) {\n const regex = new RegExp(\n \"^\" + pattern.replace(/\\./g, \"\\\\.\").replace(/\\*/g, \".*\") + \"$\",\n );\n return regex.test(ip);\n }\n\n return false;\n}\n\n/**\n * Match IP against CIDR range\n */\nfunction matchCidr(ip: string, cidr: string): boolean {\n const [range, bits] = cidr.split(\"/\");\n const mask = parseInt(bits, 10);\n\n const ipNum = ipToNumber(ip);\n const rangeNum = ipToNumber(range);\n\n if (ipNum === null || rangeNum === null) return false;\n\n const maskNum = ~((1 << (32 - mask)) - 1);\n return (ipNum & maskNum) === (rangeNum & maskNum);\n}\n\n/**\n * Convert IP string to number\n */\nfunction ipToNumber(ip: string): number | null {\n const parts = ip.split(\".\");\n if (parts.length !== 4) return null;\n\n let num = 0;\n for (const part of parts) {\n const n = parseInt(part, 10);\n if (isNaN(n) || n < 0 || n > 255) return null;\n num = (num << 8) | n;\n }\n return num >>> 0; // Convert to unsigned\n}\n\n/**\n * Match hostname against a glob pattern\n */\nexport function matchHostname(hostname: string, pattern: string): boolean {\n // Exact match\n if (hostname === pattern) return true;\n\n // Glob pattern\n const regex = new RegExp(\n \"^\" +\n pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\*/g, \".*\")\n .replace(/\\?/g, \".\") +\n \"$\",\n );\n return regex.test(hostname);\n}\n\n/**\n * Parse Basic auth header\n */\nexport function parseBasicAuth(\n header: string,\n): { username: string; password: string } | null {\n if (!header.startsWith(\"Basic \")) return null;\n\n try {\n const encoded = header.slice(6);\n const decoded = Buffer.from(encoded, \"base64\").toString(\"utf-8\");\n const colonIndex = decoded.indexOf(\":\");\n if (colonIndex === -1) return null;\n\n return {\n username: decoded.slice(0, colonIndex),\n password: decoded.slice(colonIndex + 1),\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Parse Bearer token from header\n */\nexport function parseBearerToken(header: string): string | null {\n if (!header.startsWith(\"Bearer \")) return null;\n return header.slice(7);\n}\n\n/**\n * Check authentication for a request\n */\nexport function checkAuth(request: AuthRequest, config: AuthConfig): AuthResult {\n const { hostname, ip, authorization } = request;\n\n // Get real IP (handle X-Forwarded-For)\n const realIp = request.forwardedFor?.split(\",\")[0].trim() ?? ip;\n\n // Check IP whitelist first (always allows)\n for (const pattern of config.allowedIps) {\n if (matchIp(realIp, pattern)) {\n return { allowed: true, reason: \"ip\" };\n }\n }\n\n // Check per-hostname rules\n for (const rule of config.rules) {\n if (matchHostname(hostname, rule.match)) {\n return checkRule(rule, realIp, authorization, config);\n }\n }\n\n // Apply default policy\n return applyDefaultPolicy(config, realIp, authorization);\n}\n\n/**\n * Check a specific rule\n */\nfunction checkRule(\n rule: AuthRule,\n ip: string,\n authorization: string | undefined,\n config: AuthConfig,\n): AuthResult {\n // Rule-specific IP whitelist\n if (rule.allowedIps) {\n for (const pattern of rule.allowedIps) {\n if (matchIp(ip, pattern)) {\n return { allowed: true, reason: \"rule\" };\n }\n }\n }\n\n switch (rule.policy) {\n case \"allow\":\n return { allowed: true, reason: \"rule\" };\n\n case \"deny\":\n return { allowed: false, reason: \"rule\" };\n\n case \"token\": {\n if (!authorization) return { allowed: false, reason: \"rule\" };\n const token = parseBearerToken(authorization);\n const tokens = rule.tokens ?? config.tokens;\n if (token && tokens.includes(token)) {\n return { allowed: true, reason: \"token\" };\n }\n return { allowed: false, reason: \"rule\" };\n }\n\n case \"basic\": {\n if (!authorization) return { allowed: false, reason: \"rule\" };\n const creds = parseBasicAuth(authorization);\n if (creds) {\n const credString = `${creds.username}:${creds.password}`;\n if (config.basicAuth.includes(credString)) {\n return { allowed: true, reason: \"basic\" };\n }\n }\n return { allowed: false, reason: \"rule\" };\n }\n\n default:\n return { allowed: false, reason: \"rule\" };\n }\n}\n\n/**\n * Apply default policy\n */\nfunction applyDefaultPolicy(\n config: AuthConfig,\n ip: string,\n authorization: string | undefined,\n): AuthResult {\n switch (config.defaultPolicy) {\n case \"allow\":\n return { allowed: true, reason: \"default\" };\n\n case \"deny\":\n return { allowed: false, reason: \"default\" };\n\n case \"basic\": {\n if (!authorization) return { allowed: false, reason: \"default\" };\n const creds = parseBasicAuth(authorization);\n if (creds) {\n const credString = `${creds.username}:${creds.password}`;\n if (config.basicAuth.includes(credString)) {\n return { allowed: true, reason: \"basic\" };\n }\n }\n // Also check tokens as fallback\n const token = parseBearerToken(authorization);\n if (token && config.tokens.includes(token)) {\n return { allowed: true, reason: \"token\" };\n }\n return { allowed: false, reason: \"default\" };\n }\n\n case \"token\": {\n if (!authorization) return { allowed: false, reason: \"default\" };\n const token = parseBearerToken(authorization);\n if (token && config.tokens.includes(token)) {\n return { allowed: true, reason: \"token\" };\n }\n return { allowed: false, reason: \"default\" };\n }\n\n default:\n return { allowed: false, reason: \"default\" };\n }\n}\n","import { readFileSync, existsSync, watch } from \"node:fs\";\nimport { execSync } from \"node:child_process\";\nimport type { DdevProject } from \"../types.js\";\n\n// Simple YAML parser for project_list.yaml (avoid dependencies)\n// The file format is simple: key: value pairs with quoted strings\nfunction parseSimpleYaml(content: string): Record<string, string> {\n const result: Record<string, string> = {};\n const lines = content.split(\"\\n\");\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n\n const colonIndex = trimmed.indexOf(\":\");\n if (colonIndex === -1) continue;\n\n const key = trimmed.slice(0, colonIndex).trim();\n let value = trimmed.slice(colonIndex + 1).trim();\n\n // Remove quotes\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n value = value.slice(1, -1);\n }\n\n result[key] = value;\n }\n\n return result;\n}\n\n/**\n * Load projects from DDEV's project_list.yaml\n * Returns a map of project name -> app root path\n */\nexport function loadProjectList(\n projectListPath: string,\n): Map<string, string> {\n if (!existsSync(projectListPath)) {\n return new Map();\n }\n\n const content = readFileSync(projectListPath, \"utf-8\");\n const parsed = parseSimpleYaml(content);\n\n return new Map(Object.entries(parsed));\n}\n\n/**\n * Build a hostname -> project name index\n */\nexport function buildHostnameIndex(\n projects: Map<string, string>,\n tld: string,\n): Map<string, string> {\n const index = new Map<string, string>();\n\n for (const [name] of projects) {\n // Primary hostname: project-name.tld\n index.set(`${name}.${tld}`, name);\n }\n\n return index;\n}\n\n/**\n * Get project name from hostname\n */\nexport function getProjectFromHostname(\n hostname: string,\n index: Map<string, string>,\n): string | undefined {\n return index.get(hostname);\n}\n\n/**\n * Watch project_list.yaml for changes\n */\nexport function watchProjectList(\n projectListPath: string,\n onChange: () => void,\n): void {\n if (!existsSync(projectListPath)) {\n console.warn(`Project list not found: ${projectListPath}`);\n return;\n }\n\n watch(projectListPath, (eventType) => {\n if (eventType === \"change\") {\n onChange();\n }\n });\n}\n\n/**\n * Get detailed project info using ddev describe\n */\nexport function getProjectInfo(name: string): DdevProject | undefined {\n try {\n const output = execSync(`ddev describe ${name} -j`, {\n encoding: \"utf-8\",\n timeout: 10000,\n });\n const data = JSON.parse(output);\n\n if (!data.raw) return undefined;\n\n const raw = data.raw;\n return {\n name: raw.name,\n status: raw.status,\n appRoot: raw.approot,\n httpURLs: raw.httpURLs ?? [],\n httpsURLs: raw.httpsURLs ?? [],\n type: raw.type,\n phpVersion: raw.php_version,\n dbType: raw.dbinfo?.dbType,\n };\n } catch {\n return undefined;\n }\n}\n\n/**\n * Start a DDEV project\n */\nexport function startProject(name: string): boolean {\n try {\n execSync(`ddev start ${name}`, {\n encoding: \"utf-8\",\n timeout: 120000, // 2 minutes\n stdio: \"pipe\",\n });\n return true;\n } catch (error) {\n console.error(`Failed to start project ${name}:`, error);\n return false;\n }\n}\n\n/**\n * Stop a DDEV project\n */\nexport function stopProject(name: string): boolean {\n try {\n execSync(`ddev stop ${name}`, {\n encoding: \"utf-8\",\n timeout: 60000, // 1 minute\n stdio: \"pipe\",\n });\n return true;\n } catch (error) {\n console.error(`Failed to stop project ${name}:`, error);\n return false;\n }\n}\n\n/**\n * List all DDEV projects with their status\n */\nexport function listProjects(): DdevProject[] {\n try {\n const output = execSync(\"ddev list -j\", {\n encoding: \"utf-8\",\n timeout: 30000,\n });\n const data = JSON.parse(output);\n\n if (!Array.isArray(data)) return [];\n\n return data.map((p: Record<string, unknown>) => ({\n name: p.name as string,\n status: p.status as DdevProject[\"status\"],\n appRoot: p.approot as string,\n httpURLs: (p.httpURLs as string[]) ?? [],\n httpsURLs: (p.httpsURLs as string[]) ?? [],\n type: p.type as string,\n }));\n } catch {\n return [];\n }\n}\n","import { DatabaseSync } from \"node:sqlite\";\nimport type { ProjectRecord, AccessLog } from \"../types.js\";\n\nlet db: DatabaseSync | null = null;\n\n/**\n * Initialize the database connection and schema\n */\nexport function initDb(dbPath: string): DatabaseSync {\n db = new DatabaseSync(dbPath);\n db.exec(\"PRAGMA journal_mode=WAL\");\n\n // Create tables\n db.exec(`\n CREATE TABLE IF NOT EXISTS projects (\n name TEXT PRIMARY KEY,\n last_access INTEGER NOT NULL,\n status TEXT NOT NULL DEFAULT 'stopped'\n );\n\n CREATE TABLE IF NOT EXISTS access_logs (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project TEXT NOT NULL,\n timestamp INTEGER NOT NULL,\n ip TEXT NOT NULL,\n user_agent TEXT NOT NULL,\n path TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_access_logs_project ON access_logs(project);\n CREATE INDEX IF NOT EXISTS idx_access_logs_timestamp ON access_logs(timestamp);\n `);\n\n return db;\n}\n\n/**\n * Get the database instance\n */\nexport function getDb(): DatabaseSync {\n if (!db) {\n throw new Error(\"Database not initialized. Call initDb first.\");\n }\n return db;\n}\n\n/**\n * Close the database connection\n */\nexport function closeDb(): void {\n if (db) {\n db.close();\n db = null;\n }\n}\n\n/**\n * Update project last access time\n */\nexport function updateProjectAccess(name: string): void {\n const database = getDb();\n const stmt = database.prepare(`\n INSERT INTO projects (name, last_access, status)\n VALUES (?, ?, 'running')\n ON CONFLICT(name) DO UPDATE SET last_access = excluded.last_access\n `);\n stmt.run(name, Date.now());\n}\n\n/**\n * Get project record\n */\nexport function getProject(name: string): ProjectRecord | undefined {\n const database = getDb();\n const stmt = database.prepare(\n \"SELECT name, last_access as lastAccess, status FROM projects WHERE name = ?\",\n );\n return stmt.get(name) as unknown as ProjectRecord | undefined;\n}\n\n/**\n * Set project status\n */\nexport function setProjectStatus(\n name: string,\n status: ProjectRecord[\"status\"],\n): void {\n const database = getDb();\n const stmt = database.prepare(`\n INSERT INTO projects (name, last_access, status)\n VALUES (?, ?, ?)\n ON CONFLICT(name) DO UPDATE SET status = excluded.status\n `);\n stmt.run(name, Date.now(), status);\n}\n\n/**\n * Get idle projects (last access older than threshold)\n */\nexport function getIdleProjects(thresholdMs: number): ProjectRecord[] {\n const database = getDb();\n const cutoff = Date.now() - thresholdMs;\n const stmt = database.prepare(`\n SELECT name, last_access as lastAccess, status\n FROM projects\n WHERE last_access < ? AND status = 'running'\n `);\n return stmt.all(cutoff) as unknown as ProjectRecord[];\n}\n\n/**\n * Log an access\n */\nexport function logAccess(log: Omit<AccessLog, \"id\">): void {\n const database = getDb();\n const stmt = database.prepare(`\n INSERT INTO access_logs (project, timestamp, ip, user_agent, path)\n VALUES (?, ?, ?, ?, ?)\n `);\n stmt.run(log.project, log.timestamp, log.ip, log.userAgent, log.path);\n}\n\n/**\n * Get recent access logs for a project\n */\nexport function getAccessLogs(project: string, limit = 100): AccessLog[] {\n const database = getDb();\n const stmt = database.prepare(`\n SELECT id, project, timestamp, ip, user_agent as userAgent, path\n FROM access_logs\n WHERE project = ?\n ORDER BY timestamp DESC\n LIMIT ?\n `);\n return stmt.all(project, limit) as unknown as AccessLog[];\n}\n\n/**\n * Clean old access logs (older than given days)\n */\nexport function cleanOldLogs(days: number): number {\n const database = getDb();\n const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;\n const stmt = database.prepare(\"DELETE FROM access_logs WHERE timestamp < ?\");\n const result = stmt.run(cutoff);\n return Number(result.changes);\n}\n","import { readFileSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/**\n * Per-project Trafic configuration\n * Read from .ddev/config.trafic.yaml\n */\nexport interface ProjectConfig {\n /** Auth policy override: allow | deny | basic | token */\n auth_policy?: \"allow\" | \"deny\" | \"basic\" | \"token\";\n /** Idle timeout override: \"never\" or duration like \"30m\", \"4h\" */\n idle_timeout?: string;\n}\n\n/**\n * Simple YAML parser for config.trafic.yaml\n * Only supports simple key: value pairs (no nesting, no arrays)\n */\nfunction parseSimpleYaml(content: string): Record<string, string> {\n const result: Record<string, string> = {};\n const lines = content.split(\"\\n\");\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Skip empty lines and comments\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n\n const colonIndex = trimmed.indexOf(\":\");\n if (colonIndex === -1) continue;\n\n const key = trimmed.slice(0, colonIndex).trim();\n let value = trimmed.slice(colonIndex + 1).trim();\n\n // Remove quotes if present\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n value = value.slice(1, -1);\n }\n\n result[key] = value;\n }\n\n return result;\n}\n\n/**\n * Load project-specific Trafic config from .ddev/config.trafic.yaml\n */\nexport function loadProjectConfig(projectDir: string): ProjectConfig {\n const configPath = join(projectDir, \".ddev\", \"config.trafic.yaml\");\n\n if (!existsSync(configPath)) {\n return {};\n }\n\n try {\n const content = readFileSync(configPath, \"utf-8\");\n const parsed = parseSimpleYaml(content);\n\n const config: ProjectConfig = {};\n\n // Validate auth_policy\n if (parsed.auth_policy) {\n const policy = parsed.auth_policy.toLowerCase();\n if ([\"allow\", \"deny\", \"basic\", \"token\"].includes(policy)) {\n config.auth_policy = policy as ProjectConfig[\"auth_policy\"];\n }\n }\n\n // Validate idle_timeout\n if (parsed.idle_timeout) {\n config.idle_timeout = parsed.idle_timeout;\n }\n\n return config;\n } catch (error) {\n console.warn(`Failed to load project config from ${configPath}:`, error);\n return {};\n }\n}\n\n/**\n * Check if a project should never be stopped (idle_timeout = \"never\")\n */\nexport function shouldNeverStop(config: ProjectConfig): boolean {\n return config.idle_timeout?.toLowerCase() === \"never\";\n}\n\n/**\n * Get idle timeout in milliseconds, or undefined to use global default\n */\nexport function getIdleTimeoutMs(config: ProjectConfig): number | undefined {\n if (!config.idle_timeout) return undefined;\n if (shouldNeverStop(config)) return undefined;\n\n // Parse duration string (e.g., \"30m\", \"4h\", \"2h30m\")\n const regex = /(?:(\\d+)h)?(?:(\\d+)m)?(?:(\\d+)s)?/;\n const match = config.idle_timeout.match(regex);\n if (!match) return undefined;\n\n const hours = parseInt(match[1] ?? \"0\", 10);\n const minutes = parseInt(match[2] ?? \"0\", 10);\n const seconds = parseInt(match[3] ?? \"0\", 10);\n\n const ms = (hours * 3600 + minutes * 60 + seconds) * 1000;\n return ms > 0 ? ms : undefined;\n}\n","import { createServer, type IncomingMessage, type ServerResponse } from \"node:http\";\nimport { readFileSync } from \"node:fs\";\nimport { resolve, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { AgentConfig, AuthConfig } from \"./types.js\";\nimport { checkAuth } from \"./utils/auth.js\";\nimport {\n loadProjectList,\n buildHostnameIndex,\n watchProjectList,\n startProject,\n getProjectInfo,\n} from \"./utils/ddev.js\";\nimport {\n initDb,\n updateProjectAccess,\n logAccess,\n setProjectStatus,\n getProject,\n} from \"./utils/db.js\";\nimport { loadProjectConfig } from \"./utils/project-config.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n// State\nlet config: AgentConfig;\nlet projectList: Map<string, string>;\nlet hostnameIndex: Map<string, string>;\n// Cache of per-project configs (project name -> config)\nconst projectConfigs = new Map<string, ReturnType<typeof loadProjectConfig>>();\n\n/**\n * Load HTML template\n */\nfunction loadTemplate(name: string): string {\n const templatePath = resolve(__dirname, \"..\", \"templates\", `${name}.html`);\n try {\n return readFileSync(templatePath, \"utf-8\");\n } catch {\n return `<html><body><h1>${name}</h1></body></html>`;\n }\n}\n\n/**\n * Get effective auth config for a project (merges global + per-project)\n */\nfunction getEffectiveAuthConfig(projectName: string | undefined): AuthConfig {\n if (!projectName) return config.auth;\n\n const projectConfig = projectConfigs.get(projectName);\n if (!projectConfig?.auth_policy) return config.auth;\n\n // Override default policy with project-specific policy\n return {\n ...config.auth,\n defaultPolicy: projectConfig.auth_policy,\n };\n}\n\n/**\n * Handle forward auth requests from Traefik\n * Traefik sends the original request headers, we return 200 (allow) or 401 (deny)\n */\nfunction handleAuth(req: IncomingMessage, res: ServerResponse): void {\n const hostname = req.headers[\"x-forwarded-host\"] as string ?? \"\";\n const ip = req.headers[\"x-forwarded-for\"] as string ?? req.socket.remoteAddress ?? \"\";\n const authorization = req.headers[\"authorization\"];\n const path = req.headers[\"x-forwarded-uri\"] as string ?? \"/\";\n\n // Find project from hostname\n const projectName = hostnameIndex.get(hostname);\n\n // Get effective auth config (global + per-project overrides)\n const authConfig = getEffectiveAuthConfig(projectName);\n\n const result = checkAuth(\n {\n hostname,\n ip: ip.split(\",\")[0].trim(),\n authorization,\n forwardedFor: ip,\n },\n authConfig,\n );\n\n if (result.allowed) {\n // Log access and update last access time\n if (projectName) {\n updateProjectAccess(projectName);\n logAccess({\n project: projectName,\n timestamp: Date.now(),\n ip: ip.split(\",\")[0].trim(),\n userAgent: req.headers[\"user-agent\"] ?? \"\",\n path,\n });\n }\n\n res.writeHead(200);\n res.end();\n } else {\n // Return 401 with WWW-Authenticate header for basic auth\n res.writeHead(401, {\n \"WWW-Authenticate\": 'Basic realm=\"Trafic\"',\n });\n res.end(\"Unauthorized\");\n }\n}\n\n/**\n * Handle errors middleware requests (502 from Traefik)\n * When a project is stopped, Traefik returns 502. We show a waiting page and start the project.\n */\nasync function handleErrors(\n req: IncomingMessage,\n res: ServerResponse,\n): Promise<void> {\n const hostname = req.headers[\"x-forwarded-host\"] as string ?? req.headers.host ?? \"\";\n const projectName = hostnameIndex.get(hostname);\n\n if (!projectName) {\n // Unknown project\n const template = loadTemplate(\"error\");\n res.writeHead(404, { \"Content-Type\": \"text/html\" });\n res.end(template.replace(\"{{message}}\", \"Project not found\"));\n return;\n }\n\n // Check if project is already starting\n const record = getProject(projectName);\n if (record?.status === \"starting\") {\n // Show waiting page\n const template = loadTemplate(\"wait\");\n res.writeHead(503, {\n \"Content-Type\": \"text/html\",\n \"Retry-After\": \"5\",\n });\n res.end(\n template\n .replace(/\\{\\{project\\}\\}/g, projectName)\n .replace(/\\{\\{hostname\\}\\}/g, hostname),\n );\n return;\n }\n\n // Mark as starting\n setProjectStatus(projectName, \"starting\");\n\n // Show waiting page immediately\n const template = loadTemplate(\"wait\");\n res.writeHead(503, {\n \"Content-Type\": \"text/html\",\n \"Retry-After\": \"5\",\n });\n res.end(\n template\n .replace(/\\{\\{project\\}\\}/g, projectName)\n .replace(/\\{\\{hostname\\}\\}/g, hostname),\n );\n\n // Start project in background\n setTimeout(() => {\n const success = startProject(projectName);\n setProjectStatus(projectName, success ? \"running\" : \"stopped\");\n }, 100);\n}\n\n/**\n * Handle status polling requests\n */\nfunction handleStatus(req: IncomingMessage, res: ServerResponse): void {\n const url = new URL(req.url ?? \"/\", `http://${req.headers.host}`);\n const projectName = url.searchParams.get(\"project\");\n\n if (!projectName) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Missing project parameter\" }));\n return;\n }\n\n const info = getProjectInfo(projectName);\n const record = getProject(projectName);\n\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n name: projectName,\n status: info?.status ?? record?.status ?? \"unknown\",\n ready: info?.status === \"running\",\n }),\n );\n}\n\n/**\n * Request handler\n */\nasync function handleRequest(\n req: IncomingMessage,\n res: ServerResponse,\n): Promise<void> {\n const url = req.url ?? \"/\";\n\n try {\n // Route requests\n if (url === \"/__auth__\" || url.startsWith(\"/__auth__/\")) {\n handleAuth(req, res);\n } else if (url === \"/__status__\" || url.startsWith(\"/__status__/\")) {\n handleStatus(req, res);\n } else if (url === \"/__health__\") {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"ok\", version: \"__VERSION__\" }));\n } else {\n // Default: errors middleware\n await handleErrors(req, res);\n }\n } catch (error) {\n console.error(\"Request error:\", error);\n res.writeHead(500, { \"Content-Type\": \"text/plain\" });\n res.end(\"Internal Server Error\");\n }\n}\n\n/**\n * Reload project list and their configs\n */\nfunction reloadProjects(): void {\n projectList = loadProjectList(config.projectListPath);\n hostnameIndex = buildHostnameIndex(projectList, config.tld);\n\n // Load per-project configs\n projectConfigs.clear();\n for (const [name, projectDir] of projectList) {\n const projectConfig = loadProjectConfig(projectDir);\n if (projectConfig.auth_policy || projectConfig.idle_timeout) {\n projectConfigs.set(name, projectConfig);\n console.log(` ${name}: auth=${projectConfig.auth_policy ?? \"default\"}, idle=${projectConfig.idle_timeout ?? \"default\"}`);\n }\n }\n\n console.log(`Loaded ${projectList.size} projects (${projectConfigs.size} with custom config)`);\n}\n\n/**\n * Start the agent server\n */\nexport function startServer(agentConfig: AgentConfig): void {\n config = agentConfig;\n\n // Initialize database\n initDb(config.dbPath);\n\n // Load projects\n reloadProjects();\n\n // Watch for changes\n watchProjectList(config.projectListPath, () => {\n console.log(\"Project list changed, reloading...\");\n reloadProjects();\n });\n\n // Create HTTP server\n const server = createServer((req, res) => {\n handleRequest(req, res).catch((error) => {\n console.error(\"Unhandled error:\", error);\n if (!res.headersSent) {\n res.writeHead(500);\n res.end(\"Internal Server Error\");\n }\n });\n });\n\n server.listen(config.port, () => {\n console.log(`Trafic agent listening on port ${config.port}`);\n console.log(`TLD: ${config.tld}`);\n console.log(`Projects: ${projectList.size}`);\n });\n}\n"],"mappings":"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"}
package/dist/server.js CHANGED
@@ -1,2 +1,2 @@
1
- import { t as startServer } from "./server-D6zlodZz.js";
1
+ import { t as startServer } from "./server-VlzPosuk.js";
2
2
  export { startServer };
@@ -1,5 +1,17 @@
1
1
  /**
2
- * Install Node.js using fnm
2
+ * Add the NodeSource apt repository and GPG key.
3
+ * Idempotent: safe to call even if the repository is already configured.
4
+ *
5
+ * nodejs.org points at NodeSource for apt installs. The Ubuntu 24.04
6
+ * `nodejs` package is 18.x, far below what the agent needs.
7
+ */
8
+ export declare function addNodeSourceAptRepo(): void;
9
+ /**
10
+ * Install Node.js from the NodeSource apt repository.
11
+ *
12
+ * apt keeps node and npm in /usr/bin — on root's PATH, so `which
13
+ * trafic-agent` resolves for the systemd unit — and unattended-upgrades
14
+ * patches them, which a version manager install never gets.
3
15
  */
4
16
  export declare function installNode(): void;
5
17
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/setup/agent.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,wBAAgB,WAAW,IAAI,IAAI,CAqClC;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,IAAI,CAOnC;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAiDpE;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAkD3C"}
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/setup/agent.ts"],"names":[],"mappings":"AASA;;;;;;GAMG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAuB3C;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,IAAI,IAAI,CAqBlC;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,IAAI,CAOnC;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAiDpE;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAkD3C"}
@@ -1 +1 @@
1
- {"version":3,"file":"ddev.d.ts","sourceRoot":"","sources":["../../src/setup/ddev.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAOxC;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,IAAI,CAWrC;AAED;;GAEG;AACH,wBAAgB,WAAW,IAAI,IAAI,CA2BlC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAsB/D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAsChD;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAc3C;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAkEvC"}
1
+ {"version":3,"file":"ddev.d.ts","sourceRoot":"","sources":["../../src/setup/ddev.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAYxC;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,IAAI,CAWrC;AAED;;GAEG;AACH,wBAAgB,WAAW,IAAI,IAAI,CA2BlC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAsB/D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAsChD;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAc3C;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAkEvC"}