@studiometa/trafic-agent 0.1.49 → 0.1.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +158 -13
  2. package/dist/cli.js +134 -14
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.d.ts +7 -2
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +3 -3
  7. package/dist/{server-DZAo8Clg.js → server-Cfk-O4DJ.js} +406 -30
  8. package/dist/server-Cfk-O4DJ.js.map +1 -0
  9. package/dist/server.d.ts +26 -2
  10. package/dist/server.d.ts.map +1 -1
  11. package/dist/server.js +2 -2
  12. package/dist/setup/agent.d.ts +11 -1
  13. package/dist/setup/agent.d.ts.map +1 -1
  14. package/dist/setup/migrations/0020__backup_dir.d.ts +23 -0
  15. package/dist/setup/migrations/0020__backup_dir.d.ts.map +1 -0
  16. package/dist/setup/migrations/index.d.ts.map +1 -1
  17. package/dist/{setup-DWgb6dAD.js → setup-CiWIirVb.js} +827 -209
  18. package/dist/setup-CiWIirVb.js.map +1 -0
  19. package/dist/tasks/access-logs.d.ts +8 -0
  20. package/dist/tasks/access-logs.d.ts.map +1 -0
  21. package/dist/tasks/backup.d.ts +48 -0
  22. package/dist/tasks/backup.d.ts.map +1 -0
  23. package/dist/tasks/destroy-project.d.ts +71 -0
  24. package/dist/tasks/destroy-project.d.ts.map +1 -0
  25. package/dist/tasks/destroy-scheduled.d.ts +41 -0
  26. package/dist/tasks/destroy-scheduled.d.ts.map +1 -0
  27. package/dist/tasks/running-limit.d.ts +54 -0
  28. package/dist/tasks/running-limit.d.ts.map +1 -0
  29. package/dist/tasks/stop-idle.d.ts +13 -2
  30. package/dist/tasks/stop-idle.d.ts.map +1 -1
  31. package/dist/types.d.ts +41 -2
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/utils/config.d.ts +6 -0
  34. package/dist/utils/config.d.ts.map +1 -1
  35. package/dist/utils/db.d.ts +76 -1
  36. package/dist/utils/db.d.ts.map +1 -1
  37. package/dist/utils/ddev.d.ts +12 -5
  38. package/dist/utils/ddev.d.ts.map +1 -1
  39. package/dist/utils/paths.d.ts +3 -0
  40. package/dist/utils/paths.d.ts.map +1 -0
  41. package/package.json +1 -1
  42. package/dist/server-DZAo8Clg.js.map +0 -1
  43. package/dist/setup-DWgb6dAD.js.map +0 -1
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":[],"sources":["../src/setup/upgrade.ts","../src/cli.ts"],"sourcesContent":["import { execSync, spawnSync } from \"node:child_process\";\nimport { readFileSync } from \"node:fs\";\nimport { step, success, info, warn, exec, execSilent, isRoot } from \"./steps.js\";\nimport { runPendingMigrations } from \"./migrations/index.js\";\n\ndeclare const __VERSION__: string;\n\n/**\n * Fetch the latest published version of @studiometa/trafic-agent from the npm registry.\n * Returns null if the registry is unreachable.\n */\nexport function fetchLatestVersion(): string | null {\n try {\n return execSync(\"npm show @studiometa/trafic-agent version\", {\n encoding: \"utf-8\",\n stdio: \"pipe\",\n }).trim();\n } catch {\n return null;\n }\n}\n\n/**\n * Compare two semver strings. Returns true if `next` is strictly greater than `current`.\n * Handles simple MAJOR.MINOR.PATCH without pre-release suffixes.\n */\nexport function isNewer(current: string, next: string): boolean {\n const parse = (v: string): [number, number, number] => {\n const parts = v.replace(/^v/, \"\").split(\".\").map(Number);\n return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];\n };\n\n const [cMaj, cMin, cPat] = parse(current);\n const [nMaj, nMin, nPat] = parse(next);\n\n if (nMaj !== cMaj) return nMaj > cMaj;\n if (nMin !== cMin) return nMin > cMin;\n return nPat > cPat;\n}\n\n/**\n * Install the latest version of @studiometa/trafic-agent globally via npm.\n * Uses --prefer-online to bypass the local npm cache and always fetch the\n * latest published tarball.\n */\nexport function installLatestAgent(dryRun: boolean): void {\n exec(\"npm install -g @studiometa/trafic-agent@latest --prefer-online\", { silent: !dryRun });\n}\n\n/**\n * Read the actual installed version of @studiometa/trafic-agent from its\n * package.json on disk — not from __VERSION__ which is baked in at build time.\n *\n * Resolves the package root by walking up from the trafic-agent binary:\n * /usr/bin/trafic-agent -> /usr/lib/node_modules/@studiometa/trafic-agent/dist/cli.js\n * -> /usr/lib/node_modules/@studiometa/trafic-agent/package.json\n *\n * Returns null if the file can't be read.\n */\nexport function getInstalledVersion(): string | null {\n try {\n // Resolve the real path of the binary (follow symlinks)\n const realBinary = execSync(\"readlink -f $(which trafic-agent)\", {\n encoding: \"utf-8\",\n stdio: \"pipe\",\n }).trim();\n // dist/cli.js -> up two dirs -> package root\n const pkgJson = `${realBinary.replace(/\\/dist\\/cli\\.js$/, \"\")}/package.json`;\n const pkg = JSON.parse(readFileSync(pkgJson, \"utf-8\")) as { version: string };\n return pkg.version;\n } catch {\n return null;\n }\n}\n\n/**\n * Re-exec the newly installed trafic-agent binary with the same arguments.\n * Used after a self-update so the new binary runs its own migrations.\n * This function never returns — it replaces the current process.\n */\nexport function reExecNewBinary(args: string[]): never {\n const binary = execSync(\"which trafic-agent\", { encoding: \"utf-8\", stdio: \"pipe\" }).trim();\n const result = spawnSync(binary, args, {\n stdio: \"inherit\",\n env: { ...process.env, TRAFIC_UPGRADE_REEXEC: \"1\" },\n });\n process.exit(result.status ?? 0);\n}\n\n/**\n * Restart the trafic-agent systemd service.\n */\nexport function restartAgentService(dryRun: boolean): void {\n exec(\"systemctl restart trafic-agent\", { silent: !dryRun });\n}\n\n/**\n * The installed apt package version of DDEV, or null when it cannot be read.\n */\nexport function ddevVersion(): string | null {\n return execSilent(\"dpkg-query --show --showformat='${Version}' ddev\") || null;\n}\n\n/**\n * Upgrade DDEV from its apt repository. Throws when apt fails.\n *\n * DDEV is left out of unattended-upgrades on purpose — a major landing on its\n * own can break running previews — so it is updated here, where an operator\n * is watching. `--only-upgrade` never installs DDEV on a server that does not\n * already have it.\n */\nexport function upgradeDdev(dryRun: boolean): void {\n exec(\n \"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update\",\n { silent: !dryRun },\n );\n exec(\n \"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y --only-upgrade ddev\",\n { silent: !dryRun },\n );\n}\n\n/**\n * What the upgrade sequence needs from the outside world.\n *\n * Injected so the orchestration can be tested without reaching npm, writing\n * to the filesystem or replacing the running process — the last of which is\n * why `reExecNewBinary` is here rather than called directly.\n */\nexport interface UpgradeIo {\n /** The version of the process running the upgrade. */\n currentVersion: string;\n /** Whether an earlier run already re-execed, guarding against a loop. */\n alreadyReExeced: boolean;\n isRoot: () => boolean;\n fetchLatestVersion: () => string | null;\n installLatestAgent: (dryRun: boolean) => void;\n getInstalledVersion: () => string | null;\n reExecNewBinary: (args: string[]) => never;\n restartAgentService: (dryRun: boolean) => void;\n runPendingMigrations: (dryRun: boolean) => void;\n /** The installed DDEV version, or null when DDEV is absent. */\n ddevVersion: () => string | null;\n upgradeDdev: (dryRun: boolean) => void;\n}\n\n/** The real collaborators, used unless a caller passes their own. */\nexport function nodeUpgradeIo(): UpgradeIo {\n return {\n currentVersion: __VERSION__,\n alreadyReExeced: process.env[\"TRAFIC_UPGRADE_REEXEC\"] === \"1\",\n isRoot,\n fetchLatestVersion,\n installLatestAgent,\n getInstalledVersion,\n reExecNewBinary,\n restartAgentService,\n runPendingMigrations,\n ddevVersion,\n upgradeDdev,\n };\n}\n\n/**\n * Full upgrade sequence:\n * 1. Check for a new version on npm\n * 2. Install it globally if one is found — then re-exec the new binary\n * so that steps 3 and 4 run with the new migration registry\n * 3. Run pending migrations\n * 4. Update DDEV from its apt repository\n * 5. Restart the systemd service\n */\nexport function runUpgrade(\n dryRun = false,\n reExecArgs?: string[],\n io: UpgradeIo = nodeUpgradeIo(),\n): void {\n if (!io.isRoot() && !dryRun) {\n console.error(\" \\x1b[31m✗\\x1b[0m This command must be run as root\");\n console.log(\" Run: sudo trafic-agent upgrade\");\n process.exit(1);\n }\n\n // Guard against infinite re-exec loops: only re-exec once per upgrade run.\n const alreadyReExeced = io.alreadyReExeced;\n\n // ── Step 1: Check for updates ─────────────────────────────────────────────\n step(\"Check for updates\");\n\n const current = io.currentVersion;\n info(`Current version: ${current}`);\n\n const latest = io.fetchLatestVersion();\n\n if (!latest) {\n warn(\"Could not reach npm registry — skipping version check\");\n } else if (isNewer(current, latest)) {\n success(`New version available: ${latest}`);\n\n // ── Step 2: Install ───────────────────────────────────────────────────\n step(\"Install latest version\");\n io.installLatestAgent(dryRun);\n\n if (!dryRun) {\n // Verify the installed version actually changed before re-execing —\n // npm can serve stale cache and leave the old binary in place.\n const installedVersion = io.getInstalledVersion();\n\n if (!alreadyReExeced && installedVersion && isNewer(current, installedVersion)) {\n success(`Installed @studiometa/trafic-agent@${installedVersion}`);\n // Re-exec the newly installed binary so steps 3 and 4 run with the\n // new migration registry — the current process only knows about\n // migrations that existed at the time it was compiled.\n io.reExecNewBinary(reExecArgs ?? [\"upgrade\"]);\n } else if (installedVersion) {\n success(`Installed @studiometa/trafic-agent@${installedVersion}`);\n }\n }\n } else {\n success(`Already up to date (${current})`);\n }\n\n // ── Step 3: Run pending migrations ───────────────────────────────────────\n step(\"Run pending migrations\");\n io.runPendingMigrations(dryRun);\n\n // ── Step 4: Update DDEV ──────────────────────────────────────────────────\n step(\"Update DDEV\");\n upgradeDdevStep(dryRun, io);\n\n // ── Step 5: Restart service ───────────────────────────────────────────────\n step(\"Restart trafic-agent service\");\n io.restartAgentService(dryRun);\n if (!dryRun) {\n success(\"Service restarted\");\n }\n}\n\n/**\n * Update DDEV, reporting the version on either side of the apt call.\n *\n * A failed apt run must not cost the server its service restart, so the\n * error is warned about and the upgrade carries on.\n */\nfunction upgradeDdevStep(dryRun: boolean, io: UpgradeIo): void {\n const before = io.ddevVersion();\n\n if (!dryRun && before === null) {\n info(\"DDEV is not installed — skipping\");\n return;\n }\n\n if (before) {\n info(`Current: ${before}`);\n }\n\n try {\n io.upgradeDdev(dryRun);\n } catch (err) {\n warn(`DDEV update failed: ${err instanceof Error ? err.message : String(err)}`);\n return;\n }\n\n if (dryRun) {\n return;\n }\n\n const after = io.ddevVersion();\n if (after && after !== before) {\n success(`DDEV updated: ${after}`);\n } else {\n success(\"DDEV up to date\");\n }\n}\n","#!/usr/bin/env node\n\nimport { parseArgs } from \"node:util\";\nimport { loadConfig, validateConfig } from \"./utils/config.js\";\nimport { startServer } from \"./server.js\";\nimport { startIdleScheduler } from \"./tasks/stop-idle.js\";\nimport { closeDb } from \"./utils/db.js\";\nimport { setup, audit } from \"./setup/index.js\";\nimport { listMigrations } from \"./setup/migrations/index.js\";\nimport { runUpgrade } from \"./setup/upgrade.js\";\n\ndeclare const __VERSION__: string;\n\nconst HELP = `\ntrafic-agent — Server agent for DDEV preview environments\n\nUsage:\n trafic-agent <command> [options]\n\nCommands:\n start Start the agent server\n setup Setup a new server (Docker, DDEV, hardening)\n upgrade Check for a new version, install it, run migrations, restart the service\n update Alias for upgrade\n audit Run security audit checks\n version Show version\n help Show this help\n\nStart options:\n -c, --config <path> Path to config file (default: /etc/trafic/config.toml)\n -p, --port <port> Override port from config\n\nSetup options:\n --tld <domain> TLD for DDEV projects (required on first run, reused from config on re-runs)\n --email <email> Email for Let's Encrypt certificates\n --dns-provider <name> DNS-01 provider (lego name, e.g. cloudflare). Turns on the\n wildcard *.<tld> certificate; requires --email\n --dns-env KEY=VALUE Credential for that provider, repeatable\n --no-hardening Skip server hardening steps\n --no-root-ssh Disable root SSH login (recovery via rescue mode only)\n --no-docker Skip Docker installation\n --no-ddev Skip DDEV installation\n --ssh-users <users> Comma-separated list of SSH users to allow (default: ddev)\n --trusted-proxy-hops <n> Proxies in front of the agent (default: 1; 2 behind a CDN).\n Written to the generated config; ignored if one exists\n --dry-run Show what would be done without making changes\n\nUpgrade/update options:\n --dry-run Show what would be done without making changes\n --list List all migrations and their status (no install or restart)\n\nExamples:\n # Start the agent\n trafic-agent start\n trafic-agent start --config /path/to/config.toml\n\n # Setup a new server\n sudo trafic-agent setup --tld=previews.example.com\n sudo trafic-agent setup --tld=previews.example.com --trusted-proxy-hops=2\n sudo trafic-agent setup --tld=previews.example.com --email=admin@example.com\n\n # Setup with one wildcard certificate instead of one per preview\n sudo trafic-agent setup --tld=previews.example.com --email=admin@example.com \\\\\n --dns-provider=cloudflare --dns-env=CF_DNS_API_TOKEN=xxx\n\n sudo trafic-agent setup --tld=previews.example.com --no-hardening --dry-run\n\n # Upgrade: install latest version, run migrations, restart service\n sudo trafic-agent upgrade\n sudo trafic-agent upgrade --dry-run\n trafic-agent upgrade --list\n sudo trafic-agent update # alias for upgrade\n\n # Run security audit\n trafic-agent audit\n`;\n\nfunction printHelp(): void {\n console.log(HELP.trim());\n}\n\nfunction printVersion(): void {\n console.log(__VERSION__);\n}\n\nasync function runStart(values: Record<string, unknown>): Promise<void> {\n // Load config\n const config = loadConfig(values.config as string | undefined);\n\n // Override port if specified\n if (values.port) {\n config.port = parseInt(values.port as string, 10);\n }\n\n // Validate config\n const errors = validateConfig(config);\n if (errors.length > 0) {\n console.error(\"Configuration errors:\");\n for (const error of errors) {\n console.error(` - ${error}`);\n }\n process.exit(1);\n }\n\n // Graceful shutdown\n const shutdown = (): void => {\n console.log(\"\\nShutting down...\");\n closeDb();\n process.exit(0);\n };\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n\n // Start server and scheduler\n startServer(config);\n startIdleScheduler(config);\n}\n\nfunction handleUpgrade(values: Record<string, unknown>): void {\n const dryRun = values[\"dry-run\"] as boolean | undefined;\n const list = values.list as boolean | undefined;\n\n if (list) {\n const entries = listMigrations();\n console.log(\"\\nMigrations:\\n\");\n\n for (const { migration, status } of entries) {\n const icon = status === \"applied\" ? \"\\x1b[32m✓\\x1b[0m\" : \"\\x1b[36m→\\x1b[0m\";\n const label = status === \"applied\" ? \"\\x1b[90m(applied)\\x1b[0m\" : \"(pending)\";\n console.log(` ${icon} \\x1b[1m${migration.id.padEnd(30)}\\x1b[0m ${migration.description.padEnd(55)} ${label}`);\n }\n\n console.log(\"\");\n return;\n }\n\n console.log(\"\\n\\x1b[1m🚦 Trafic Server Upgrade\\x1b[0m\\n\");\n\n if (dryRun) {\n console.log(\"\\x1b[33m⚠ Dry-run mode: no changes will be made\\x1b[0m\\n\");\n }\n\n // Build re-exec args so flags survive the binary swap after a self-update\n const reExecArgs = [\"upgrade\", ...(dryRun ? [\"--dry-run\"] : [])];\n runUpgrade(dryRun ?? false, reExecArgs);\n\n if (!dryRun) {\n console.log(\"\\n\\x1b[32m✓ Upgrade complete!\\x1b[0m\\n\");\n }\n}\n\n/**\n * Parse --trusted-proxy-hops, failing loudly rather than writing a config\n * that would quietly break the IP allowlist.\n */\nfunction parseTrustedProxyHops(raw?: string): number | undefined {\n if (raw === undefined) {\n return undefined;\n }\n\n const hops = Number(raw);\n\n if (!Number.isInteger(hops) || hops < 1) {\n console.error(\n `Error: --trusted-proxy-hops must be a whole number of 1 or more, got \"${raw}\"`,\n );\n console.error(\" 1 = ddev-router/Traefik alone, 2 = a CDN in front of it\");\n process.exit(1);\n }\n\n return hops;\n}\n\n/**\n * Parse repeated --dns-env KEY=VALUE pairs.\n *\n * A bad entry stops setup: a provider silently missing its token fails much\n * later, as an ACME error in the router log.\n */\nfunction parseDnsEnv(entries?: string[]): Record<string, string> {\n const env: Record<string, string> = {};\n\n for (const entry of entries ?? []) {\n const separator = entry.indexOf(\"=\");\n const key = separator === -1 ? entry : entry.slice(0, separator);\n\n if (separator === -1 || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {\n console.error(`Error: --dns-env must be KEY=VALUE, got \"${entry}\"`);\n process.exit(1);\n }\n\n env[key] = entry.slice(separator + 1);\n }\n\n return env;\n}\n\nasync function runSetup(values: Record<string, unknown>): Promise<void> {\n // Load existing config to use as defaults on re-runs\n const existingConfig = loadConfig();\n const tld = (values.tld as string | undefined) ?? (existingConfig.tld || undefined);\n\n if (!tld) {\n console.error(\"Error: --tld is required for setup\");\n console.error(\"Example: trafic-agent setup --tld=previews.example.com\");\n process.exit(1);\n }\n\n // Re-runs keep what the config already says, so a setup run for another\n // reason cannot silently drop the wildcard certificate\n const dnsProvider =\n (values[\"dns-provider\"] as string | undefined) ?? existingConfig.tls.dnsProvider;\n const dnsEnv = {\n ...existingConfig.tls.dnsEnv,\n ...parseDnsEnv(values[\"dns-env\"] as string[] | undefined),\n };\n\n if (dnsProvider && !values.email) {\n console.error(\"Error: --dns-provider requires --email\");\n console.error(\" Let's Encrypt refuses an ACME account without one\");\n process.exit(1);\n }\n\n await setup({\n tld,\n dnsProvider,\n dnsEnv,\n // Not a flag: a CA override is a config-file decision, kept across re-runs\n caServer: existingConfig.tls.caServer,\n trustedProxyHops: parseTrustedProxyHops(values[\"trusted-proxy-hops\"] as string | undefined),\n email: values.email as string | undefined,\n noHardening: values[\"no-hardening\"] as boolean | undefined,\n noRootSsh: values[\"no-root-ssh\"] as boolean | undefined,\n noDocker: values[\"no-docker\"] as boolean | undefined,\n noDdev: values[\"no-ddev\"] as boolean | undefined,\n sshUsers: values[\"ssh-users\"]\n ? (values[\"ssh-users\"] as string).split(\",\").map((s) => s.trim())\n : undefined,\n dryRun: values[\"dry-run\"] as boolean | undefined,\n });\n}\n\nasync function main(): Promise<void> {\n const { values, positionals } = parseArgs({\n allowPositionals: true,\n options: {\n // Common\n help: { type: \"boolean\", short: \"h\" },\n\n // Start options\n config: { type: \"string\", short: \"c\" },\n port: { type: \"string\", short: \"p\" },\n\n // Setup options\n tld: { type: \"string\" },\n email: { type: \"string\" },\n \"no-hardening\": { type: \"boolean\" },\n \"no-root-ssh\": { type: \"boolean\" },\n \"no-docker\": { type: \"boolean\" },\n \"no-ddev\": { type: \"boolean\" },\n \"ssh-users\": { type: \"string\" },\n \"dns-provider\": { type: \"string\" },\n \"dns-env\": { type: \"string\", multiple: true },\n \"trusted-proxy-hops\": { type: \"string\" },\n \"dry-run\": { type: \"boolean\" },\n\n // Upgrade options\n list: { type: \"boolean\" },\n },\n });\n\n const command = positionals[0] ?? \"help\";\n\n if (values.help) {\n printHelp();\n process.exit(0);\n }\n\n switch (command) {\n case \"help\":\n printHelp();\n break;\n\n case \"version\":\n printVersion();\n break;\n\n case \"start\":\n await runStart(values);\n break;\n\n case \"setup\":\n await runSetup(values);\n break;\n\n case \"upgrade\":\n case \"update\":\n handleUpgrade(values);\n break;\n\n case \"audit\":\n await audit();\n break;\n\n default:\n console.error(`Unknown command: ${command}`);\n printHelp();\n process.exit(1);\n }\n}\n\nmain().catch((error) => {\n console.error(\"Fatal error:\", error);\n process.exit(1);\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/setup/upgrade.ts","../src/cli.ts"],"sourcesContent":["import { execSync, spawnSync } from \"node:child_process\";\nimport { readFileSync } from \"node:fs\";\nimport { step, success, info, warn, exec, execSilent, isRoot } from \"./steps.js\";\nimport { runPendingMigrations } from \"./migrations/index.js\";\n\ndeclare const __VERSION__: string;\n\n/**\n * Fetch the latest published version of @studiometa/trafic-agent from the npm registry.\n * Returns null if the registry is unreachable.\n */\nexport function fetchLatestVersion(): string | null {\n try {\n return execSync(\"npm show @studiometa/trafic-agent version\", {\n encoding: \"utf-8\",\n stdio: \"pipe\",\n }).trim();\n } catch {\n return null;\n }\n}\n\n/**\n * Compare two semver strings. Returns true if `next` is strictly greater than `current`.\n * Handles simple MAJOR.MINOR.PATCH without pre-release suffixes.\n */\nexport function isNewer(current: string, next: string): boolean {\n const parse = (v: string): [number, number, number] => {\n const parts = v.replace(/^v/, \"\").split(\".\").map(Number);\n return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];\n };\n\n const [cMaj, cMin, cPat] = parse(current);\n const [nMaj, nMin, nPat] = parse(next);\n\n if (nMaj !== cMaj) return nMaj > cMaj;\n if (nMin !== cMin) return nMin > cMin;\n return nPat > cPat;\n}\n\n/**\n * Install the latest version of @studiometa/trafic-agent globally via npm.\n * Uses --prefer-online to bypass the local npm cache and always fetch the\n * latest published tarball.\n */\nexport function installLatestAgent(dryRun: boolean): void {\n exec(\"npm install -g @studiometa/trafic-agent@latest --prefer-online\", { silent: !dryRun });\n}\n\n/**\n * Read the actual installed version of @studiometa/trafic-agent from its\n * package.json on disk — not from __VERSION__ which is baked in at build time.\n *\n * Resolves the package root by walking up from the trafic-agent binary:\n * /usr/bin/trafic-agent -> /usr/lib/node_modules/@studiometa/trafic-agent/dist/cli.js\n * -> /usr/lib/node_modules/@studiometa/trafic-agent/package.json\n *\n * Returns null if the file can't be read.\n */\nexport function getInstalledVersion(): string | null {\n try {\n // Resolve the real path of the binary (follow symlinks)\n const realBinary = execSync(\"readlink -f $(which trafic-agent)\", {\n encoding: \"utf-8\",\n stdio: \"pipe\",\n }).trim();\n // dist/cli.js -> up two dirs -> package root\n const pkgJson = `${realBinary.replace(/\\/dist\\/cli\\.js$/, \"\")}/package.json`;\n const pkg = JSON.parse(readFileSync(pkgJson, \"utf-8\")) as { version: string };\n return pkg.version;\n } catch {\n return null;\n }\n}\n\n/**\n * Re-exec the newly installed trafic-agent binary with the same arguments.\n * Used after a self-update so the new binary runs its own migrations.\n * This function never returns — it replaces the current process.\n */\nexport function reExecNewBinary(args: string[]): never {\n const binary = execSync(\"which trafic-agent\", { encoding: \"utf-8\", stdio: \"pipe\" }).trim();\n const result = spawnSync(binary, args, {\n stdio: \"inherit\",\n env: { ...process.env, TRAFIC_UPGRADE_REEXEC: \"1\" },\n });\n process.exit(result.status ?? 0);\n}\n\n/**\n * Restart the trafic-agent systemd service.\n */\nexport function restartAgentService(dryRun: boolean): void {\n exec(\"systemctl restart trafic-agent\", { silent: !dryRun });\n}\n\n/**\n * The installed apt package version of DDEV, or null when it cannot be read.\n */\nexport function ddevVersion(): string | null {\n return execSilent(\"dpkg-query --show --showformat='${Version}' ddev\") || null;\n}\n\n/**\n * Upgrade DDEV from its apt repository. Throws when apt fails.\n *\n * DDEV is left out of unattended-upgrades on purpose — a major landing on its\n * own can break running previews — so it is updated here, where an operator\n * is watching. `--only-upgrade` never installs DDEV on a server that does not\n * already have it.\n */\nexport function upgradeDdev(dryRun: boolean): void {\n exec(\n \"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update\",\n { silent: !dryRun },\n );\n exec(\n \"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y --only-upgrade ddev\",\n { silent: !dryRun },\n );\n}\n\n/**\n * What the upgrade sequence needs from the outside world.\n *\n * Injected so the orchestration can be tested without reaching npm, writing\n * to the filesystem or replacing the running process — the last of which is\n * why `reExecNewBinary` is here rather than called directly.\n */\nexport interface UpgradeIo {\n /** The version of the process running the upgrade. */\n currentVersion: string;\n /** Whether an earlier run already re-execed, guarding against a loop. */\n alreadyReExeced: boolean;\n isRoot: () => boolean;\n fetchLatestVersion: () => string | null;\n installLatestAgent: (dryRun: boolean) => void;\n getInstalledVersion: () => string | null;\n reExecNewBinary: (args: string[]) => never;\n restartAgentService: (dryRun: boolean) => void;\n runPendingMigrations: (dryRun: boolean) => void;\n /** The installed DDEV version, or null when DDEV is absent. */\n ddevVersion: () => string | null;\n upgradeDdev: (dryRun: boolean) => void;\n}\n\n/** The real collaborators, used unless a caller passes their own. */\nexport function nodeUpgradeIo(): UpgradeIo {\n return {\n currentVersion: __VERSION__,\n alreadyReExeced: process.env[\"TRAFIC_UPGRADE_REEXEC\"] === \"1\",\n isRoot,\n fetchLatestVersion,\n installLatestAgent,\n getInstalledVersion,\n reExecNewBinary,\n restartAgentService,\n runPendingMigrations,\n ddevVersion,\n upgradeDdev,\n };\n}\n\n/**\n * Full upgrade sequence:\n * 1. Check for a new version on npm\n * 2. Install it globally if one is found — then re-exec the new binary\n * so that steps 3 and 4 run with the new migration registry\n * 3. Run pending migrations\n * 4. Update DDEV from its apt repository\n * 5. Restart the systemd service\n */\nexport function runUpgrade(\n dryRun = false,\n reExecArgs?: string[],\n io: UpgradeIo = nodeUpgradeIo(),\n): void {\n if (!io.isRoot() && !dryRun) {\n console.error(\" \\x1b[31m✗\\x1b[0m This command must be run as root\");\n console.log(\" Run: sudo trafic-agent upgrade\");\n process.exit(1);\n }\n\n // Guard against infinite re-exec loops: only re-exec once per upgrade run.\n const alreadyReExeced = io.alreadyReExeced;\n\n // ── Step 1: Check for updates ─────────────────────────────────────────────\n step(\"Check for updates\");\n\n const current = io.currentVersion;\n info(`Current version: ${current}`);\n\n const latest = io.fetchLatestVersion();\n\n if (!latest) {\n warn(\"Could not reach npm registry — skipping version check\");\n } else if (isNewer(current, latest)) {\n success(`New version available: ${latest}`);\n\n // ── Step 2: Install ───────────────────────────────────────────────────\n step(\"Install latest version\");\n io.installLatestAgent(dryRun);\n\n if (!dryRun) {\n // Verify the installed version actually changed before re-execing —\n // npm can serve stale cache and leave the old binary in place.\n const installedVersion = io.getInstalledVersion();\n\n if (!alreadyReExeced && installedVersion && isNewer(current, installedVersion)) {\n success(`Installed @studiometa/trafic-agent@${installedVersion}`);\n // Re-exec the newly installed binary so steps 3 and 4 run with the\n // new migration registry — the current process only knows about\n // migrations that existed at the time it was compiled.\n io.reExecNewBinary(reExecArgs ?? [\"upgrade\"]);\n } else if (installedVersion) {\n success(`Installed @studiometa/trafic-agent@${installedVersion}`);\n }\n }\n } else {\n success(`Already up to date (${current})`);\n }\n\n // ── Step 3: Run pending migrations ───────────────────────────────────────\n step(\"Run pending migrations\");\n io.runPendingMigrations(dryRun);\n\n // ── Step 4: Update DDEV ──────────────────────────────────────────────────\n step(\"Update DDEV\");\n upgradeDdevStep(dryRun, io);\n\n // ── Step 5: Restart service ───────────────────────────────────────────────\n step(\"Restart trafic-agent service\");\n io.restartAgentService(dryRun);\n if (!dryRun) {\n success(\"Service restarted\");\n }\n}\n\n/**\n * Update DDEV, reporting the version on either side of the apt call.\n *\n * A failed apt run must not cost the server its service restart, so the\n * error is warned about and the upgrade carries on.\n */\nfunction upgradeDdevStep(dryRun: boolean, io: UpgradeIo): void {\n const before = io.ddevVersion();\n\n if (!dryRun && before === null) {\n info(\"DDEV is not installed — skipping\");\n return;\n }\n\n if (before) {\n info(`Current: ${before}`);\n }\n\n try {\n io.upgradeDdev(dryRun);\n } catch (err) {\n warn(`DDEV update failed: ${err instanceof Error ? err.message : String(err)}`);\n return;\n }\n\n if (dryRun) {\n return;\n }\n\n const after = io.ddevVersion();\n if (after && after !== before) {\n success(`DDEV updated: ${after}`);\n } else {\n success(\"DDEV up to date\");\n }\n}\n","#!/usr/bin/env node\n\nimport { parseArgs } from \"node:util\";\nimport { loadConfig, parseTimeout, validateConfig } from \"./utils/config.js\";\nimport { startServer } from \"./server.js\";\nimport { startIdleScheduler } from \"./tasks/stop-idle.js\";\nimport { cancelScheduledDestroy, closeDb, getScheduledDestroys } from \"./utils/db.js\";\nimport { destroyProject, loadDestroyConfig, openDestroyDb } from \"./tasks/destroy-project.js\";\nimport {\n scheduleProjectDestroy,\n startScheduledDestroyScheduler,\n} from \"./tasks/destroy-scheduled.js\";\nimport { startBackupSweepScheduler } from \"./tasks/backup.js\";\nimport { startAccessLogSweepScheduler } from \"./tasks/access-logs.js\";\nimport { setup, audit } from \"./setup/index.js\";\nimport { isRoot } from \"./setup/steps.js\";\nimport { listMigrations } from \"./setup/migrations/index.js\";\nimport { runUpgrade } from \"./setup/upgrade.js\";\nimport type { AgentConfig } from \"./types.js\";\nimport SKILL from \"../../../skills/trafic-agent/SKILL.md?raw\";\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 Install the latest agent, run migrations, upgrade DDEV, restart the service\n update Alias for upgrade\n audit Run security audit checks\n destroy <name> Remove a project: DDEV project, directory, database records,\n now or after a delay\n skill Print the agent skill (SKILL.md) of this version\n version Show version (also -v, --version)\n help Show this help (also -h, --help)\n\nStart options:\n -c, --config <path> Path to config file (default: first of /etc/trafic/config.toml,\n ./config.toml, ./trafic.toml)\n -p, --port <port> Override port from config\n\nSetup options:\n --tld <domain> TLD for DDEV projects (required on first run, reused from config on re-runs)\n --email <email> Email for Let's Encrypt certificates\n --dns-provider <name> DNS-01 provider (lego name, e.g. cloudflare). Turns on the\n wildcard *.<tld> certificate; requires --email\n --dns-env KEY=VALUE Credential for that provider, repeatable\n --no-hardening Skip server hardening steps\n --no-root-ssh Disable root SSH login (recovery via rescue mode only)\n --no-docker Skip Docker installation\n --no-ddev Skip DDEV installation\n --ssh-users <users> Comma-separated list of SSH users to allow (default: ddev)\n --trusted-proxy-hops <n> Proxies in front of the agent (default: 1; 2 behind a CDN).\n Written to the generated config; ignored if one exists\n --dry-run Show what would be done without making changes\n\nDestroy options:\n -c, --config <path> Path to config file (default: first of /etc/trafic/config.toml,\n ./config.toml, ./trafic.toml)\n --backup Back up the database and upload directories first, to\n [backup] dir. If the backup fails, nothing is removed\n --timeout <duration> Time limit of the backup, the database dump and the files\n archive together: 10m, 90s, 1h; a bare number is minutes\n (default: 40m)\n --delay <duration> Remove nothing now: schedule the removal after this delay\n (hours, minutes, seconds only: 48h, 1h30m). The agent\n removes the project when it is due\n --cancel Cancel the scheduled removal of the project\n --list List the scheduled removals\n\nUpgrade/update options:\n --dry-run Show what would be done without making changes\n --list List all migrations and their status (no install or restart)\n\nExamples:\n # Start the agent\n trafic-agent start\n trafic-agent start --config /path/to/config.toml\n\n # Setup a new server\n sudo trafic-agent setup --tld=previews.example.com\n sudo trafic-agent setup --tld=previews.example.com --trusted-proxy-hops=2\n sudo trafic-agent setup --tld=previews.example.com --email=admin@example.com\n\n # Setup with one wildcard certificate instead of one per preview\n sudo trafic-agent setup --tld=previews.example.com --email=admin@example.com \\\\\n --dns-provider=cloudflare --dns-env=CF_DNS_API_TOKEN=xxx\n\n sudo trafic-agent setup --tld=previews.example.com --no-hardening --dry-run\n\n # Upgrade: install the latest agent, run migrations, upgrade DDEV, restart the 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 sudo trafic-agent audit\n\n # Remove a project (as the ddev user, never as root)\n trafic-agent destroy preview-42--my-app\n trafic-agent destroy preview-42--my-app --backup\n trafic-agent destroy preview-42--my-app --backup --timeout 1h\n trafic-agent destroy preview-42--my-app --delay 48h --backup\n trafic-agent destroy preview-42--my-app --cancel\n trafic-agent destroy --list\n`;\n\nfunction printHelp(): void {\n console.log(HELP.trim());\n}\n\nfunction printVersion(): void {\n console.log(__VERSION__);\n}\n\n/** Print the configuration errors and exit with code 1, if there are any. */\nfunction exitOnConfigErrors(config: AgentConfig): void {\n const errors = validateConfig(config);\n\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\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 exitOnConfigErrors(config);\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 schedulers\n startServer(config);\n startIdleScheduler(config);\n startBackupSweepScheduler(config);\n startAccessLogSweepScheduler();\n startScheduledDestroyScheduler(config);\n}\n\n/**\n * Remove a project. Runs as the ddev user, next to the running service: both\n * open the same database, so the config is read the same way `start` reads it.\n */\nasync function runDestroy(\n values: Record<string, unknown>,\n name: string | undefined,\n): Promise<void> {\n // DDEV refuses to run as root, so the directory would go and leave the\n // containers behind, and root could leave files in the database directory\n // that the service cannot write to\n if (isRoot()) {\n console.error(\"Error: run trafic-agent destroy as the ddev user, not root\");\n process.exit(1);\n }\n\n if (!name && !values.list) {\n console.error(\"Error: a project name is required\");\n console.error(\"Example: trafic-agent destroy preview-42--my-app\");\n process.exit(1);\n }\n\n // Reject a bad duration rather than quietly using the default: the flag\n // exists to raise the limit for a large backup\n const timeout = values.timeout as string | undefined;\n const timeoutMs = timeout === undefined ? undefined : parseTimeout(timeout);\n\n if (timeout !== undefined && timeoutMs === undefined) {\n console.error(`Error: invalid --timeout \"${timeout}\". Expected a duration such as 10m, 90s or 1h`);\n process.exit(1);\n }\n\n const backup = values.backup as boolean | undefined;\n const delay = values.delay as string | undefined;\n\n try {\n const config = loadDestroyConfig(values.config as string | undefined);\n // A relative or empty backup.dir would put the backup in the working\n // directory, which start refuses\n exitOnConfigErrors(config);\n openDestroyDb(config.dbPath);\n\n if (values.list) {\n printScheduledDestroys();\n } else if (values.cancel) {\n cancelDestroy(name!);\n } else if (delay !== undefined) {\n const destroyAt = scheduleProjectDestroy(name!, delay, config, { backup, timeoutMs });\n console.log(\n `Scheduled the removal of ${name} at ${destroyAt.toISOString()}${backup ? \", after a backup\" : \"\"}`,\n );\n } else {\n await destroyProject(name!, config, { backup, timeoutMs });\n }\n } catch (error) {\n console.error(`Error: ${(error as Error).message}`);\n process.exitCode = 1;\n } finally {\n closeDb();\n }\n}\n\n/**\n * Cancel the scheduled removal of a project. Fails when the sweep already\n * removes the project, so a deploy that cancels first stops there.\n */\nfunction cancelDestroy(name: string): void {\n const result = cancelScheduledDestroy(name);\n\n if (result === \"removing\") {\n throw new Error(`the removal of ${name} is in progress, it is too late to cancel it`);\n }\n\n console.log(\n result === \"cancelled\" ? `Cancelled the scheduled removal of ${name}` : `No removal scheduled for ${name}`,\n );\n}\n\n/** Print the scheduled removals, soonest first. */\nfunction printScheduledDestroys(): void {\n const scheduled = getScheduledDestroys();\n\n if (scheduled.length === 0) {\n console.log(\"No removal scheduled\");\n return;\n }\n\n for (const { name, destroyAt, backup, removing } of scheduled) {\n const notes = [backup && \"backup\", removing && \"in progress\"].filter(Boolean);\n console.log(`${new Date(destroyAt).toISOString()} ${name}${notes.length ? ` (${notes.join(\", \")})` : \"\"}`);\n }\n}\n\nfunction handleUpgrade(values: Record<string, unknown>): void {\n const dryRun = values[\"dry-run\"] as boolean | undefined;\n const list = values.list as boolean | undefined;\n\n if (list) {\n const entries = listMigrations();\n console.log(\"\\nMigrations:\\n\");\n\n for (const { migration, status } of entries) {\n const icon = status === \"applied\" ? \"\\x1b[32m✓\\x1b[0m\" : \"\\x1b[36m→\\x1b[0m\";\n const label = status === \"applied\" ? \"\\x1b[90m(applied)\\x1b[0m\" : \"(pending)\";\n console.log(` ${icon} \\x1b[1m${migration.id.padEnd(30)}\\x1b[0m ${migration.description.padEnd(55)} ${label}`);\n }\n\n console.log(\"\");\n return;\n }\n\n console.log(\"\\n\\x1b[1m🚦 Trafic Server Upgrade\\x1b[0m\\n\");\n\n if (dryRun) {\n console.log(\"\\x1b[33m⚠ Dry-run mode: no changes will be made\\x1b[0m\\n\");\n }\n\n // Build re-exec args so flags survive the binary swap after a self-update\n const reExecArgs = [\"upgrade\", ...(dryRun ? [\"--dry-run\"] : [])];\n runUpgrade(dryRun ?? false, reExecArgs);\n\n if (!dryRun) {\n console.log(\"\\n\\x1b[32m✓ Upgrade complete!\\x1b[0m\\n\");\n }\n}\n\n/**\n * Parse --trusted-proxy-hops, failing loudly rather than writing a config\n * that would quietly break the IP allowlist.\n */\nfunction parseTrustedProxyHops(raw?: string): number | undefined {\n if (raw === undefined) {\n return undefined;\n }\n\n const hops = Number(raw);\n\n if (!Number.isInteger(hops) || hops < 1) {\n console.error(\n `Error: --trusted-proxy-hops must be a whole number of 1 or more, got \"${raw}\"`,\n );\n console.error(\" 1 = ddev-router/Traefik alone, 2 = a CDN in front of it\");\n process.exit(1);\n }\n\n return hops;\n}\n\n/**\n * Parse repeated --dns-env KEY=VALUE pairs.\n *\n * A bad entry stops setup: a provider silently missing its token fails much\n * later, as an ACME error in the router log.\n */\nfunction parseDnsEnv(entries?: string[]): Record<string, string> {\n const env: Record<string, string> = {};\n\n for (const entry of entries ?? []) {\n const separator = entry.indexOf(\"=\");\n const key = separator === -1 ? entry : entry.slice(0, separator);\n\n if (separator === -1 || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {\n console.error(`Error: --dns-env must be KEY=VALUE, got \"${entry}\"`);\n process.exit(1);\n }\n\n env[key] = entry.slice(separator + 1);\n }\n\n return env;\n}\n\nasync function runSetup(values: Record<string, unknown>): Promise<void> {\n // Load existing config to use as defaults on re-runs\n const existingConfig = loadConfig();\n const tld = (values.tld as string | undefined) ?? (existingConfig.tld || undefined);\n\n if (!tld) {\n console.error(\"Error: --tld is required for setup\");\n console.error(\"Example: trafic-agent setup --tld=previews.example.com\");\n process.exit(1);\n }\n\n // Re-runs keep what the config already says, so a setup run for another\n // reason cannot silently drop the wildcard certificate\n const dnsProvider =\n (values[\"dns-provider\"] as string | undefined) ?? existingConfig.tls.dnsProvider;\n const dnsEnv = {\n ...existingConfig.tls.dnsEnv,\n ...parseDnsEnv(values[\"dns-env\"] as string[] | undefined),\n };\n\n if (dnsProvider && !values.email) {\n console.error(\"Error: --dns-provider requires --email\");\n console.error(\" Let's Encrypt refuses an ACME account without one\");\n process.exit(1);\n }\n\n await setup({\n tld,\n dnsProvider,\n dnsEnv,\n // Not a flag: a CA override is a config-file decision, kept across re-runs\n caServer: existingConfig.tls.caServer,\n trustedProxyHops: parseTrustedProxyHops(values[\"trusted-proxy-hops\"] as string | undefined),\n email: values.email as string | undefined,\n noHardening: values[\"no-hardening\"] as boolean | undefined,\n noRootSsh: values[\"no-root-ssh\"] as boolean | undefined,\n noDocker: values[\"no-docker\"] as boolean | undefined,\n noDdev: values[\"no-ddev\"] as boolean | undefined,\n sshUsers: values[\"ssh-users\"]\n ? (values[\"ssh-users\"] as string).split(\",\").map((s) => s.trim())\n : undefined,\n dryRun: values[\"dry-run\"] as boolean | undefined,\n });\n}\n\nasync function main(): Promise<void> {\n const { values, positionals } = parseArgs({\n allowPositionals: true,\n options: {\n // Common\n help: { type: \"boolean\", short: \"h\" },\n version: { type: \"boolean\", short: \"v\" },\n\n // Start options\n config: { type: \"string\", short: \"c\" },\n port: { type: \"string\", short: \"p\" },\n\n // Setup options\n tld: { type: \"string\" },\n email: { type: \"string\" },\n \"no-hardening\": { type: \"boolean\" },\n \"no-root-ssh\": { type: \"boolean\" },\n \"no-docker\": { type: \"boolean\" },\n \"no-ddev\": { type: \"boolean\" },\n \"ssh-users\": { type: \"string\" },\n \"dns-provider\": { type: \"string\" },\n \"dns-env\": { type: \"string\", multiple: true },\n \"trusted-proxy-hops\": { type: \"string\" },\n \"dry-run\": { type: \"boolean\" },\n\n // Upgrade and destroy options\n list: { type: \"boolean\" },\n\n // Destroy options\n backup: { type: \"boolean\" },\n timeout: { type: \"string\" },\n delay: { type: \"string\" },\n cancel: { 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 if (values.version) {\n printVersion();\n return;\n }\n\n switch (command) {\n case \"help\":\n printHelp();\n break;\n\n case \"version\":\n printVersion();\n break;\n\n case \"skill\":\n // The skill bundled at build time, so it matches this version\n process.stdout.write(SKILL);\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 case \"destroy\":\n await runDestroy(values, positionals[1]);\n break;\n\n default:\n console.error(`Unknown command: ${command}`);\n printHelp();\n process.exit(1);\n }\n}\n\nmain().catch((error) => {\n console.error(\"Fatal error:\", error);\n process.exit(1);\n});\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,12 +1,17 @@
1
- export type { AgentConfig, AuthConfig, AuthRule, AuthRequest, AuthResult, DdevProject, ProjectRecord, AccessLog, } from "./types.js";
1
+ export type { AgentConfig, AuthConfig, BackupConfig, AuthRule, AuthRequest, AuthResult, DdevProject, ProjectRecord, AccessLog, ScheduledDestroy, } from "./types.js";
2
2
  export { loadConfig, validateConfig, parseDuration } from "./utils/config.js";
3
3
  export { checkAuth, matchIp, matchHostname, parseBasicAuth, parseBearerToken, } from "./utils/auth.js";
4
4
  export { loadProjectList, buildHostnameIndex, getProjectFromHostname, watchProjectList, getProjectInfo, startProject, stopProject, listProjects, } from "./utils/ddev.js";
5
5
  export { buildDockerFirewallScript, buildDockerFirewallUnit, configureDockerFirewall, DOCKER_FIREWALL_SCRIPT, DOCKER_FIREWALL_UNIT, } from "./setup/docker-firewall.js";
6
- export { initDb, getDb, closeDb, updateProjectAccess, getProject, setProjectStatus, getIdleProjects, logAccess, getAccessLogs, cleanOldLogs, } from "./utils/db.js";
6
+ export { initDb, getDb, closeDb, updateProjectAccess, getProject, setProjectStatus, deleteProject, scheduleDestroy, cancelScheduledDestroy, startScheduledDestroy, getScheduledDestroys, getIdleProjects, releaseStaleStatuses, deleteUnlistedProjects, isProcessAlive, logAccess, getAccessLogs, cleanOldLogs, } from "./utils/db.js";
7
7
  export { loadProjectConfig, shouldNeverStop, getIdleTimeoutMs, } from "./utils/project-config.js";
8
8
  export type { ProjectConfig } from "./utils/project-config.js";
9
9
  export { stopIdleProjects, startIdleScheduler } from "./tasks/stop-idle.js";
10
+ export { destroyProject } from "./tasks/destroy-project.js";
11
+ export { scheduleProjectDestroy, destroyScheduledProjects, startScheduledDestroyScheduler, } from "./tasks/destroy-scheduled.js";
12
+ export { backupProject, removeOldBackups, startBackupSweepScheduler, } from "./tasks/backup.js";
13
+ export { enforceRunningLimit } from "./tasks/running-limit.js";
14
+ export { startAccessLogSweepScheduler } from "./tasks/access-logs.js";
10
15
  export { startServer } from "./server.js";
11
16
  export { setup, audit, runAudit, printAuditResults } from "./setup/index.js";
12
17
  export type { SetupOptions, StepResult, AuditCheck } from "./setup/types.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,WAAW,EACX,UAAU,EACV,QAAQ,EACR,WAAW,EACX,UAAU,EACV,WAAW,EACX,aAAa,EACb,SAAS,GACV,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAG9E,OAAO,EACL,SAAS,EACT,OAAO,EACP,aAAa,EACb,cAAc,EACd,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,sBAAsB,EACtB,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,WAAW,EACX,YAAY,GACb,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,yBAAyB,EACzB,uBAAuB,EACvB,uBAAuB,EACvB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,EACL,MAAM,EACN,KAAK,EACL,OAAO,EACP,mBAAmB,EACnB,UAAU,EACV,gBAAgB,EAChB,eAAe,EACf,SAAS,EACT,aAAa,EACb,YAAY,GACb,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,gBAAgB,GACjB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAG/D,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAG5E,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG1C,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC7E,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,WAAW,EACX,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,WAAW,EACX,UAAU,EACV,WAAW,EACX,aAAa,EACb,SAAS,EACT,gBAAgB,GACjB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAG9E,OAAO,EACL,SAAS,EACT,OAAO,EACP,aAAa,EACb,cAAc,EACd,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,sBAAsB,EACtB,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,WAAW,EACX,YAAY,GACb,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,yBAAyB,EACzB,uBAAuB,EACvB,uBAAuB,EACvB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,EACL,MAAM,EACN,KAAK,EACL,OAAO,EACP,mBAAmB,EACnB,UAAU,EACV,gBAAgB,EAChB,aAAa,EACb,eAAe,EACf,sBAAsB,EACtB,qBAAqB,EACrB,oBAAoB,EACpB,eAAe,EACf,oBAAoB,EACpB,sBAAsB,EACtB,cAAc,EACd,SAAS,EACT,aAAa,EACb,YAAY,GACb,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,gBAAgB,GACjB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAG/D,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC5E,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EACL,sBAAsB,EACtB,wBAAwB,EACxB,8BAA8B,GAC/B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,yBAAyB,GAC1B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,4BAA4B,EAAE,MAAM,wBAAwB,CAAC;AAGtE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG1C,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC7E,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { $ as stopProject, A as getIdleProjects, C as getIdleTimeoutMs, D as closeDb, E as cleanOldLogs, F as updateProjectAccess, J as getProjectFromHostname, M as initDb, N as logAccess, O as getAccessLogs, P as setProjectStatus, Q as startProject, T as shouldNeverStop, X as listProjects, Y as getProjectInfo, Z as loadProjectList, at as parseBearerToken, ct as validateConfig, et as watchProjectList, it as parseBasicAuth, j as getProject, k as getDb, nt as matchHostname, ot as loadConfig, q as buildHostnameIndex, rt as matchIp, s as startServer, st as parseDuration, tt as checkAuth, w as loadProjectConfig } from "./server-DZAo8Clg.js";
2
- import { a as printAuditResults, c as stopIdleProjects, d as buildDockerFirewallScript, f as buildDockerFirewallUnit, l as DOCKER_FIREWALL_SCRIPT, n as setup, o as runAudit, p as configureDockerFirewall, s as startIdleScheduler, t as audit, u as DOCKER_FIREWALL_UNIT } from "./setup-DWgb6dAD.js";
3
- export { DOCKER_FIREWALL_SCRIPT, DOCKER_FIREWALL_UNIT, audit, buildDockerFirewallScript, buildDockerFirewallUnit, buildHostnameIndex, checkAuth, cleanOldLogs, closeDb, configureDockerFirewall, getAccessLogs, getDb, getIdleProjects, getIdleTimeoutMs, getProject, getProjectFromHostname, getProjectInfo, initDb, listProjects, loadConfig, loadProjectConfig, loadProjectList, logAccess, matchHostname, matchIp, parseBasicAuth, parseBearerToken, parseDuration, printAuditResults, runAudit, setProjectStatus, setup, shouldNeverStop, startIdleScheduler, startProject, startServer, stopIdleProjects, stopProject, updateProjectAccess, validateConfig, watchProjectList };
1
+ import { A as shouldNeverStop, Ct as parseDuration, D as enforceRunningLimit, F as deleteUnlistedProjects, G as releaseStaleStatuses, H as initDb, I as getAccessLogs, J as startScheduledDestroy, K as scheduleDestroy, L as getDb, M as cleanOldLogs, N as closeDb, O as getIdleTimeoutMs, P as deleteProject, R as getIdleProjects, St as loadConfig, Tt as validateConfig, U as isProcessAlive, V as getScheduledDestroys, W as logAccess, X as updateProjectAccess, _t as checkAuth, bt as parseBasicAuth, ct as buildHostnameIndex, dt as listProjects, ft as loadProjectList, gt as watchProjectList, ht as stopProject, j as cancelScheduledDestroy, k as loadProjectConfig, lt as getProjectFromHostname, mt as startProject, q as setProjectStatus, u as startServer, ut as getProjectInfo, vt as matchHostname, xt as parseBearerToken, yt as matchIp, z as getProject } from "./server-Cfk-O4DJ.js";
2
+ import { C as configureDockerFirewall, S as buildDockerFirewallUnit, _ as startIdleScheduler, a as printAuditResults, b as DOCKER_FIREWALL_UNIT, c as destroyScheduledProjects, d as destroyProject, g as startBackupSweepScheduler, h as removeOldBackups, l as scheduleProjectDestroy, m as backupProject, n as setup, o as runAudit, s as startAccessLogSweepScheduler, t as audit, u as startScheduledDestroyScheduler, v as stopIdleProjects, x as buildDockerFirewallScript, y as DOCKER_FIREWALL_SCRIPT } from "./setup-CiWIirVb.js";
3
+ export { DOCKER_FIREWALL_SCRIPT, DOCKER_FIREWALL_UNIT, audit, backupProject, buildDockerFirewallScript, buildDockerFirewallUnit, buildHostnameIndex, cancelScheduledDestroy, checkAuth, cleanOldLogs, closeDb, configureDockerFirewall, deleteProject, deleteUnlistedProjects, destroyProject, destroyScheduledProjects, enforceRunningLimit, getAccessLogs, getDb, getIdleProjects, getIdleTimeoutMs, getProject, getProjectFromHostname, getProjectInfo, getScheduledDestroys, initDb, isProcessAlive, listProjects, loadConfig, loadProjectConfig, loadProjectList, logAccess, matchHostname, matchIp, parseBasicAuth, parseBearerToken, parseDuration, printAuditResults, releaseStaleStatuses, removeOldBackups, runAudit, scheduleDestroy, scheduleProjectDestroy, setProjectStatus, setup, shouldNeverStop, startAccessLogSweepScheduler, startBackupSweepScheduler, startIdleScheduler, startProject, startScheduledDestroy, startScheduledDestroyScheduler, startServer, stopIdleProjects, stopProject, updateProjectAccess, validateConfig, watchProjectList };