@studiometa/trafic-agent 0.1.10 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/cli.js +36 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/{server-D0N_6y4c.js → server-D6zlodZz.js} +6 -5
- package/dist/server-D6zlodZz.js.map +1 -0
- package/dist/server.js +1 -1
- package/dist/setup/ddev.d.ts +5 -0
- package/dist/setup/ddev.d.ts.map +1 -1
- package/dist/setup/index.d.ts.map +1 -1
- package/dist/setup/migrations/0001__ddev_apt_repo.d.ts +13 -0
- package/dist/setup/migrations/0001__ddev_apt_repo.d.ts.map +1 -0
- package/dist/setup/migrations/index.d.ts +35 -0
- package/dist/setup/migrations/index.d.ts.map +1 -0
- package/dist/setup/types.d.ts +25 -0
- package/dist/setup/types.d.ts.map +1 -1
- package/dist/{setup-BfiuQiwa.js → setup-CKwhVY6g.js} +110 -8
- package/dist/setup-CKwhVY6g.js.map +1 -0
- package/dist/utils/db.d.ts +3 -3
- package/dist/utils/db.d.ts.map +1 -1
- package/package.json +1 -5
- package/dist/server-D0N_6y4c.js.map +0 -1
- package/dist/setup-BfiuQiwa.js.map +0 -1
package/README.md
CHANGED
|
@@ -42,6 +42,32 @@ trafic-agent setup --tld previews.example.com
|
|
|
42
42
|
- Root access (for initial setup)
|
|
43
43
|
- Wildcard DNS pointing to the server
|
|
44
44
|
|
|
45
|
+
### `trafic-agent upgrade`
|
|
46
|
+
|
|
47
|
+
Run pending server migrations to bring an existing server up-to-date after a `trafic-agent` update. Migrations are forward-only and idempotent — safe to run multiple times.
|
|
48
|
+
|
|
49
|
+
Fresh servers set up with `trafic-agent setup` have all migrations automatically marked as applied, so `upgrade` is only relevant for existing deployments.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Preview what would be done
|
|
53
|
+
sudo trafic-agent upgrade --dry-run
|
|
54
|
+
|
|
55
|
+
# Apply all pending migrations
|
|
56
|
+
sudo trafic-agent upgrade
|
|
57
|
+
|
|
58
|
+
# List all migrations and their status
|
|
59
|
+
trafic-agent upgrade --list
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Example `--list` output:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
✓ 0001__ddev_apt_repo Migrate DDEV from manual tarball to apt repository (applied)
|
|
66
|
+
→ 0002__... ... (pending)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
State is stored in `/etc/trafic/.migrations.json` and is updated after each individual migration, so a partial failure leaves the state consistent.
|
|
70
|
+
|
|
45
71
|
## Configuration
|
|
46
72
|
|
|
47
73
|
Create `/etc/trafic/config.toml`:
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { o as closeDb, t as startServer } from "./server-
|
|
2
|
+
import { d as validateConfig, i as runPendingMigrations, l as loadConfig, n as setup, r as listMigrations, s as startIdleScheduler, t as audit } from "./setup-CKwhVY6g.js";
|
|
3
|
+
import { o as closeDb, t as startServer } from "./server-D6zlodZz.js";
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
5
|
var HELP = `
|
|
6
6
|
trafic-agent — Server agent for DDEV preview environments
|
|
@@ -11,6 +11,7 @@ Usage:
|
|
|
11
11
|
Commands:
|
|
12
12
|
start Start the agent server
|
|
13
13
|
setup Setup a new server (Docker, DDEV, hardening)
|
|
14
|
+
upgrade Run pending server migrations
|
|
14
15
|
audit Run security audit checks
|
|
15
16
|
version Show version
|
|
16
17
|
help Show this help
|
|
@@ -28,6 +29,10 @@ Setup options:
|
|
|
28
29
|
--ssh-users <users> Comma-separated list of SSH users to allow (default: ddev)
|
|
29
30
|
--dry-run Show what would be done without making changes
|
|
30
31
|
|
|
32
|
+
Upgrade options:
|
|
33
|
+
--dry-run Show what would be done without making changes
|
|
34
|
+
--list List all migrations and their status
|
|
35
|
+
|
|
31
36
|
Examples:
|
|
32
37
|
# Start the agent
|
|
33
38
|
trafic-agent start
|
|
@@ -38,6 +43,11 @@ Examples:
|
|
|
38
43
|
sudo trafic-agent setup --tld=previews.example.com --email=admin@example.com
|
|
39
44
|
sudo trafic-agent setup --tld=previews.example.com --no-hardening --dry-run
|
|
40
45
|
|
|
46
|
+
# Run pending server migrations
|
|
47
|
+
sudo trafic-agent upgrade
|
|
48
|
+
sudo trafic-agent upgrade --dry-run
|
|
49
|
+
trafic-agent upgrade --list
|
|
50
|
+
|
|
41
51
|
# Run security audit
|
|
42
52
|
trafic-agent audit
|
|
43
53
|
`;
|
|
@@ -45,7 +55,7 @@ function printHelp() {
|
|
|
45
55
|
console.log(HELP.trim());
|
|
46
56
|
}
|
|
47
57
|
function printVersion() {
|
|
48
|
-
console.log("0.1.
|
|
58
|
+
console.log("0.1.11");
|
|
49
59
|
}
|
|
50
60
|
async function runStart(values) {
|
|
51
61
|
const config = loadConfig(values.config);
|
|
@@ -66,6 +76,24 @@ async function runStart(values) {
|
|
|
66
76
|
startServer(config);
|
|
67
77
|
startIdleScheduler(config);
|
|
68
78
|
}
|
|
79
|
+
function runUpgrade(values) {
|
|
80
|
+
const dryRun = values["dry-run"];
|
|
81
|
+
if (values.list) {
|
|
82
|
+
const entries = listMigrations();
|
|
83
|
+
console.log("\nMigrations:\n");
|
|
84
|
+
for (const { migration, status } of entries) {
|
|
85
|
+
const icon = status === "applied" ? "\x1B[32m✓\x1B[0m" : "\x1B[36m→\x1B[0m";
|
|
86
|
+
const label = status === "applied" ? "\x1B[90m(applied)\x1B[0m" : "(pending)";
|
|
87
|
+
console.log(` ${icon} \x1b[1m${migration.id.padEnd(30)}\x1b[0m ${migration.description.padEnd(55)} ${label}`);
|
|
88
|
+
}
|
|
89
|
+
console.log("");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
console.log("\n\x1B[1m🚦 Trafic Server Upgrade\x1B[0m\n");
|
|
93
|
+
if (dryRun) console.log("\x1B[33m⚠ Dry-run mode: no changes will be made\x1B[0m\n");
|
|
94
|
+
runPendingMigrations(dryRun ?? false);
|
|
95
|
+
if (!dryRun) console.log("\n\x1B[32m✓ Upgrade complete!\x1B[0m\n");
|
|
96
|
+
}
|
|
69
97
|
async function runSetup(values) {
|
|
70
98
|
const existingConfig = loadConfig();
|
|
71
99
|
const tld = values.tld ?? (existingConfig.tld || void 0);
|
|
@@ -106,7 +134,8 @@ async function main() {
|
|
|
106
134
|
"no-docker": { type: "boolean" },
|
|
107
135
|
"no-ddev": { type: "boolean" },
|
|
108
136
|
"ssh-users": { type: "string" },
|
|
109
|
-
"dry-run": { type: "boolean" }
|
|
137
|
+
"dry-run": { type: "boolean" },
|
|
138
|
+
list: { type: "boolean" }
|
|
110
139
|
}
|
|
111
140
|
});
|
|
112
141
|
const command = positionals[0] ?? "help";
|
|
@@ -127,6 +156,9 @@ async function main() {
|
|
|
127
156
|
case "setup":
|
|
128
157
|
await runSetup(values);
|
|
129
158
|
break;
|
|
159
|
+
case "upgrade":
|
|
160
|
+
runUpgrade(values);
|
|
161
|
+
break;
|
|
130
162
|
case "audit":
|
|
131
163
|
await audit();
|
|
132
164
|
break;
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/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\";\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 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\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 # 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\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 });\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 \"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":";;;;
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/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, runPendingMigrations } from \"./setup/migrations/index.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 Run pending server migrations\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 options:\n --dry-run Show what would be done without making changes\n --list List all migrations and their status\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 # Run pending server migrations\n sudo trafic-agent upgrade\n sudo trafic-agent upgrade --dry-run\n trafic-agent upgrade --list\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 runUpgrade(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 runPendingMigrations(dryRun ?? false);\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 runUpgrade(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":";;;;AAYA,IAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDb,SAAS,YAAkB;AACzB,SAAQ,IAAI,KAAK,MAAM,CAAC;;AAG1B,SAAS,eAAqB;AAC5B,SAAQ,IAAA,SAAgB;;AAG1B,eAAe,SAAS,QAAgD;CAEtE,MAAM,SAAS,WAAW,OAAO,OAA6B;AAG9D,KAAI,OAAO,KACT,QAAO,OAAO,SAAS,OAAO,MAAgB,GAAG;CAInD,MAAM,SAAS,eAAe,OAAO;AACrC,KAAI,OAAO,SAAS,GAAG;AACrB,UAAQ,MAAM,wBAAwB;AACtC,OAAK,MAAM,SAAS,OAClB,SAAQ,MAAM,OAAO,QAAQ;AAE/B,UAAQ,KAAK,EAAE;;CAIjB,MAAM,iBAAuB;AAC3B,UAAQ,IAAI,qBAAqB;AACjC,WAAS;AACT,UAAQ,KAAK,EAAE;;AAGjB,SAAQ,GAAG,UAAU,SAAS;AAC9B,SAAQ,GAAG,WAAW,SAAS;AAG/B,aAAY,OAAO;AACnB,oBAAmB,OAAO;;AAG5B,SAAS,WAAW,QAAuC;CACzD,MAAM,SAAS,OAAO;AAGtB,KAFa,OAAO,MAEV;EACR,MAAM,UAAU,gBAAgB;AAChC,UAAQ,IAAI,kBAAkB;AAE9B,OAAK,MAAM,EAAE,WAAW,YAAY,SAAS;GAC3C,MAAM,OAAO,WAAW,YAAY,qBAAqB;GACzD,MAAM,QAAQ,WAAW,YAAY,6BAA6B;AAClE,WAAQ,IAAI,KAAK,KAAK,UAAU,UAAU,GAAG,OAAO,GAAG,CAAC,UAAU,UAAU,YAAY,OAAO,GAAG,CAAC,GAAG,QAAQ;;AAGhH,UAAQ,IAAI,GAAG;AACf;;AAGF,SAAQ,IAAI,6CAA6C;AAEzD,KAAI,OACF,SAAQ,IAAI,2DAA2D;AAGzE,sBAAqB,UAAU,MAAM;AAErC,KAAI,CAAC,OACH,SAAQ,IAAI,yCAAyC;;AAIzD,eAAe,SAAS,QAAgD;CAEtE,MAAM,iBAAiB,YAAY;CACnC,MAAM,MAAO,OAAO,QAA+B,eAAe,OAAO,KAAA;AAEzE,KAAI,CAAC,KAAK;AACR,UAAQ,MAAM,qCAAqC;AACnD,UAAQ,MAAM,yDAAyD;AACvE,UAAQ,KAAK,EAAE;;AAGjB,OAAM,MAAM;EACV;EACA,OAAO,OAAO;EACd,aAAa,OAAO;EACpB,UAAU,OAAO;EACjB,QAAQ,OAAO;EACf,UAAU,OAAO,eACZ,OAAO,aAAwB,MAAM,IAAI,CAAC,KAAK,MAAM,EAAE,MAAM,CAAC,GAC/D,KAAA;EACJ,QAAQ,OAAO;EAChB,CAAC;;AAGJ,eAAe,OAAsB;CACnC,MAAM,EAAE,QAAQ,gBAAgB,UAAU;EACxC,kBAAkB;EAClB,SAAS;GAEP,MAAM;IAAE,MAAM;IAAW,OAAO;IAAK;GAGrC,QAAQ;IAAE,MAAM;IAAU,OAAO;IAAK;GACtC,MAAM;IAAE,MAAM;IAAU,OAAO;IAAK;GAGpC,KAAK,EAAE,MAAM,UAAU;GACvB,OAAO,EAAE,MAAM,UAAU;GACzB,gBAAgB,EAAE,MAAM,WAAW;GACnC,aAAa,EAAE,MAAM,WAAW;GAChC,WAAW,EAAE,MAAM,WAAW;GAC9B,aAAa,EAAE,MAAM,UAAU;GAC/B,WAAW,EAAE,MAAM,WAAW;GAG9B,MAAM,EAAE,MAAM,WAAW;GAC1B;EACF,CAAC;CAEF,MAAM,UAAU,YAAY,MAAM;AAElC,KAAI,OAAO,MAAM;AACf,aAAW;AACX,UAAQ,KAAK,EAAE;;AAGjB,SAAQ,SAAR;EACE,KAAK;AACH,cAAW;AACX;EAEF,KAAK;AACH,iBAAc;AACd;EAEF,KAAK;AACH,SAAM,SAAS,OAAO;AACtB;EAEF,KAAK;AACH,SAAM,SAAS,OAAO;AACtB;EAEF,KAAK;AACH,cAAW,OAAO;AAClB;EAEF,KAAK;AACH,SAAM,OAAO;AACb;EAEF;AACE,WAAQ,MAAM,oBAAoB,UAAU;AAC5C,cAAW;AACX,WAAQ,KAAK,EAAE;;;AAIrB,MAAM,CAAC,OAAO,UAAU;AACtB,SAAQ,MAAM,gBAAgB,MAAM;AACpC,SAAQ,KAAK,EAAE;EACf"}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as
|
|
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-
|
|
1
|
+
import { a as printAuditResults, c as stopIdleProjects, d as validateConfig, l as loadConfig, n as setup, o as runAudit, s as startIdleScheduler, t as audit, u as parseDuration } from "./setup-CKwhVY6g.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";
|
|
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 };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, watch } from "node:fs";
|
|
2
2
|
import { execSync } from "node:child_process";
|
|
3
|
-
import
|
|
3
|
+
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";
|
|
@@ -336,8 +336,8 @@ var db = null;
|
|
|
336
336
|
* Initialize the database connection and schema
|
|
337
337
|
*/
|
|
338
338
|
function initDb(dbPath) {
|
|
339
|
-
db = new
|
|
340
|
-
db.
|
|
339
|
+
db = new DatabaseSync(dbPath);
|
|
340
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
341
341
|
db.exec(`
|
|
342
342
|
CREATE TABLE IF NOT EXISTS projects (
|
|
343
343
|
name TEXT PRIMARY KEY,
|
|
@@ -440,7 +440,8 @@ function getAccessLogs(project, limit = 100) {
|
|
|
440
440
|
function cleanOldLogs(days) {
|
|
441
441
|
const database = getDb();
|
|
442
442
|
const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
443
|
-
|
|
443
|
+
const result = database.prepare("DELETE FROM access_logs WHERE timestamp < ?").run(cutoff);
|
|
444
|
+
return Number(result.changes);
|
|
444
445
|
}
|
|
445
446
|
/**
|
|
446
447
|
* Simple YAML parser for config.trafic.yaml
|
|
@@ -684,4 +685,4 @@ function startServer(agentConfig) {
|
|
|
684
685
|
}
|
|
685
686
|
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 };
|
|
686
687
|
|
|
687
|
-
//# sourceMappingURL=server-
|
|
688
|
+
//# sourceMappingURL=server-D6zlodZz.js.map
|
|
@@ -0,0 +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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"}
|
package/dist/server.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as startServer } from "./server-
|
|
1
|
+
import { t as startServer } from "./server-D6zlodZz.js";
|
|
2
2
|
export { startServer };
|
package/dist/setup/ddev.d.ts
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* Install system dependencies required by Trafic
|
|
3
3
|
*/
|
|
4
4
|
export declare function installSystemDeps(): void;
|
|
5
|
+
/**
|
|
6
|
+
* Add the official DDEV apt repository and GPG key.
|
|
7
|
+
* Idempotent: safe to call even if the repository is already configured.
|
|
8
|
+
*/
|
|
9
|
+
export declare function addDdevAptRepo(): void;
|
|
5
10
|
/**
|
|
6
11
|
* Install DDEV
|
|
7
12
|
*/
|
package/dist/setup/ddev.d.ts.map
CHANGED
|
@@ -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;;GAEG;AACH,wBAAgB,WAAW,IAAI,IAAI,
|
|
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,CAuBlC;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;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAqDvC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/setup/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/setup/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAU/C,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAE3B;;GAEG;AACH,wBAAsB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAoFhE;AAED;;GAEG;AACH,wBAAsB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAQ3C"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Migration } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Migration 0001: Migrate DDEV from manual tarball to apt repository.
|
|
4
|
+
*
|
|
5
|
+
* Old installs downloaded a tarball from GitHub releases. This migration
|
|
6
|
+
* removes the manually-installed binaries and reinstalls DDEV via the
|
|
7
|
+
* official apt repository so future updates are handled by apt.
|
|
8
|
+
*
|
|
9
|
+
* Condition: skip if /etc/apt/sources.list.d/ddev.list already exists,
|
|
10
|
+
* which means the apt repository is already configured.
|
|
11
|
+
*/
|
|
12
|
+
export declare const migration0001DdevAptRepo: Migration;
|
|
13
|
+
//# sourceMappingURL=0001__ddev_apt_repo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"0001__ddev_apt_repo.d.ts","sourceRoot":"","sources":["../../../src/setup/migrations/0001__ddev_apt_repo.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB,EAAE,SA2BtC,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Migration, MigrationState, MigrationListEntry } from "../types.js";
|
|
2
|
+
/** Path to the persisted migration state file */
|
|
3
|
+
export declare const MIGRATIONS_STATE_FILE = "/etc/trafic/.migrations.json";
|
|
4
|
+
/** Ordered registry of all known migrations */
|
|
5
|
+
export declare const ALL_MIGRATIONS: Migration[];
|
|
6
|
+
/**
|
|
7
|
+
* Load the current migration state from disk.
|
|
8
|
+
* Returns an empty state if the file does not exist yet.
|
|
9
|
+
*/
|
|
10
|
+
export declare function loadMigrationState(): MigrationState;
|
|
11
|
+
/**
|
|
12
|
+
* Persist the migration state to disk.
|
|
13
|
+
* Creates /etc/trafic/ if it does not exist.
|
|
14
|
+
*/
|
|
15
|
+
export declare function saveMigrationState(state: MigrationState): void;
|
|
16
|
+
/**
|
|
17
|
+
* Return all migrations with their current status.
|
|
18
|
+
*/
|
|
19
|
+
export declare function listMigrations(): MigrationListEntry[];
|
|
20
|
+
/**
|
|
21
|
+
* Run all pending migrations and persist state after each one.
|
|
22
|
+
*
|
|
23
|
+
* When `dryRun` is true the runner prints what it would do without
|
|
24
|
+
* executing anything or touching the state file.
|
|
25
|
+
*/
|
|
26
|
+
export declare function runPendingMigrations(dryRun?: boolean): void;
|
|
27
|
+
/**
|
|
28
|
+
* Mark all known migrations as applied without running them.
|
|
29
|
+
*
|
|
30
|
+
* Called at the end of a fresh `setup()` run: a brand-new server has
|
|
31
|
+
* already been configured correctly, so migrations that fix older
|
|
32
|
+
* installations are not needed.
|
|
33
|
+
*/
|
|
34
|
+
export declare function markAllMigrationsApplied(): void;
|
|
35
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/setup/migrations/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAGjF,iDAAiD;AACjD,eAAO,MAAM,qBAAqB,iCAAiC,CAAC;AAEpE,+CAA+C;AAC/C,eAAO,MAAM,cAAc,EAAE,SAAS,EAA+B,CAAC;AAMtE;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,cAAc,CAqBnD;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI,CAG9D;AAMD;;GAEG;AACH,wBAAgB,cAAc,IAAI,kBAAkB,EAAE,CAMrD;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,UAAQ,GAAG,IAAI,CAyBzD;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,IAAI,IAAI,CAU/C"}
|
package/dist/setup/types.d.ts
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A versioned server migration
|
|
3
|
+
*/
|
|
4
|
+
export interface Migration {
|
|
5
|
+
/** Unique identifier, e.g. "0001__ddev_apt_repo" */
|
|
6
|
+
id: string;
|
|
7
|
+
/** Human-readable description shown in --list output and on run */
|
|
8
|
+
description: string;
|
|
9
|
+
/** Run the migration. Must be idempotent and non-interactive. */
|
|
10
|
+
run(): void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Persisted migration state stored in /etc/trafic/.migrations.json
|
|
14
|
+
*/
|
|
15
|
+
export interface MigrationState {
|
|
16
|
+
/** Ordered list of applied migration IDs */
|
|
17
|
+
applied: string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Entry returned by listMigrations()
|
|
21
|
+
*/
|
|
22
|
+
export interface MigrationListEntry {
|
|
23
|
+
migration: Migration;
|
|
24
|
+
status: "applied" | "pending";
|
|
25
|
+
}
|
|
1
26
|
/**
|
|
2
27
|
* Setup command options
|
|
3
28
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/setup/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,2CAA2C;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2BAA2B;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iDAAiD;IACjD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,oCAAoC;IACpC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,oDAAoD;IACpD,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/setup/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,oDAAoD;IACpD,EAAE,EAAE,MAAM,CAAC;IACX,mEAAmE;IACnE,WAAW,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,GAAG,IAAI,IAAI,CAAC;CACb;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,4CAA4C;IAC5C,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,SAAS,CAAC;IACrB,MAAM,EAAE,SAAS,GAAG,SAAS,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,2CAA2C;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2BAA2B;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iDAAiD;IACjD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,oCAAoC;IACpC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,oDAAoD;IACpD,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { _ as getProjectInfo, i as shouldNeverStop, l as getIdleProjects, n as getIdleTimeoutMs, p as setProjectStatus, r as loadProjectConfig, x as stopProject, y as loadProjectList } from "./server-
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { _ as getProjectInfo, i as shouldNeverStop, l as getIdleProjects, n as getIdleTimeoutMs, p as setProjectStatus, r as loadProjectConfig, x as stopProject, y as loadProjectList } from "./server-D6zlodZz.js";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { parse } from "smol-toml";
|
|
4
4
|
import { execSync } from "node:child_process";
|
|
5
5
|
var DEFAULT_CONFIG_PATHS = [
|
|
@@ -308,6 +308,16 @@ function installSystemDeps() {
|
|
|
308
308
|
success("System dependencies installed: jq, curl, rsync");
|
|
309
309
|
}
|
|
310
310
|
/**
|
|
311
|
+
* Add the official DDEV apt repository and GPG key.
|
|
312
|
+
* Idempotent: safe to call even if the repository is already configured.
|
|
313
|
+
*/
|
|
314
|
+
function addDdevAptRepo() {
|
|
315
|
+
exec("install -m 0755 -d /etc/apt/keyrings", { silent: true });
|
|
316
|
+
exec("curl -fsSL https://pkg.ddev.com/apt/gpg.key | gpg --dearmor | tee /etc/apt/keyrings/ddev.gpg > /dev/null", { silent: true });
|
|
317
|
+
exec("chmod a+r /etc/apt/keyrings/ddev.gpg", { silent: true });
|
|
318
|
+
exec("echo \"deb [signed-by=/etc/apt/keyrings/ddev.gpg] https://pkg.ddev.com/apt/ * *\" | tee /etc/apt/sources.list.d/ddev.list > /dev/null", { silent: true });
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
311
321
|
* Install DDEV
|
|
312
322
|
*/
|
|
313
323
|
function installDdev() {
|
|
@@ -317,10 +327,7 @@ function installDdev() {
|
|
|
317
327
|
return;
|
|
318
328
|
}
|
|
319
329
|
info("Adding DDEV apt repository...");
|
|
320
|
-
|
|
321
|
-
exec("curl -fsSL https://pkg.ddev.com/apt/gpg.key | gpg --dearmor | tee /etc/apt/keyrings/ddev.gpg > /dev/null", { silent: true });
|
|
322
|
-
exec("chmod a+r /etc/apt/keyrings/ddev.gpg", { silent: true });
|
|
323
|
-
exec("echo \"deb [signed-by=/etc/apt/keyrings/ddev.gpg] https://pkg.ddev.com/apt/ * *\" | tee /etc/apt/sources.list.d/ddev.list > /dev/null", { silent: true });
|
|
330
|
+
addDdevAptRepo();
|
|
324
331
|
info("Installing DDEV...");
|
|
325
332
|
exec("apt-get update -qq", { silent: true });
|
|
326
333
|
exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ddev", { silent: true });
|
|
@@ -865,6 +872,100 @@ function printAuditResults(checks) {
|
|
|
865
872
|
console.log(`\n\x1b[1mSummary:\x1b[0m ${passed} passed, ${warnings} warnings, ${failed} failed`);
|
|
866
873
|
}
|
|
867
874
|
/**
|
|
875
|
+
* Migration 0001: Migrate DDEV from manual tarball to apt repository.
|
|
876
|
+
*
|
|
877
|
+
* Old installs downloaded a tarball from GitHub releases. This migration
|
|
878
|
+
* removes the manually-installed binaries and reinstalls DDEV via the
|
|
879
|
+
* official apt repository so future updates are handled by apt.
|
|
880
|
+
*
|
|
881
|
+
* Condition: skip if /etc/apt/sources.list.d/ddev.list already exists,
|
|
882
|
+
* which means the apt repository is already configured.
|
|
883
|
+
*/
|
|
884
|
+
const migration0001DdevAptRepo = {
|
|
885
|
+
id: "0001__ddev_apt_repo",
|
|
886
|
+
description: "Migrate DDEV from manual tarball to apt repository",
|
|
887
|
+
run() {
|
|
888
|
+
if (existsSync("/etc/apt/sources.list.d/ddev.list")) return;
|
|
889
|
+
exec("rm -f /usr/local/bin/ddev /usr/local/bin/ddev-hostname /usr/local/bin/mkcert", { silent: true });
|
|
890
|
+
addDdevAptRepo();
|
|
891
|
+
exec("apt-get update -qq", { silent: true });
|
|
892
|
+
exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ddev", { silent: true });
|
|
893
|
+
exec("mkcert -install", { silent: true });
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
/** Path to the persisted migration state file */
|
|
897
|
+
const MIGRATIONS_STATE_FILE = "/etc/trafic/.migrations.json";
|
|
898
|
+
/** Ordered registry of all known migrations */
|
|
899
|
+
const ALL_MIGRATIONS = [migration0001DdevAptRepo];
|
|
900
|
+
/**
|
|
901
|
+
* Load the current migration state from disk.
|
|
902
|
+
* Returns an empty state if the file does not exist yet.
|
|
903
|
+
*/
|
|
904
|
+
function loadMigrationState() {
|
|
905
|
+
if (!existsSync("/etc/trafic/.migrations.json")) return { applied: [] };
|
|
906
|
+
try {
|
|
907
|
+
const raw = readFileSync(MIGRATIONS_STATE_FILE, "utf-8");
|
|
908
|
+
const parsed = JSON.parse(raw);
|
|
909
|
+
if (parsed !== null && typeof parsed === "object" && "applied" in parsed && Array.isArray(parsed.applied)) return parsed;
|
|
910
|
+
} catch {}
|
|
911
|
+
return { applied: [] };
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Persist the migration state to disk.
|
|
915
|
+
* Creates /etc/trafic/ if it does not exist.
|
|
916
|
+
*/
|
|
917
|
+
function saveMigrationState(state) {
|
|
918
|
+
mkdirSync("/etc/trafic", { recursive: true });
|
|
919
|
+
writeFileSync(MIGRATIONS_STATE_FILE, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Return all migrations with their current status.
|
|
923
|
+
*/
|
|
924
|
+
function listMigrations() {
|
|
925
|
+
const state = loadMigrationState();
|
|
926
|
+
return ALL_MIGRATIONS.map((migration) => ({
|
|
927
|
+
migration,
|
|
928
|
+
status: state.applied.includes(migration.id) ? "applied" : "pending"
|
|
929
|
+
}));
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Run all pending migrations and persist state after each one.
|
|
933
|
+
*
|
|
934
|
+
* When `dryRun` is true the runner prints what it would do without
|
|
935
|
+
* executing anything or touching the state file.
|
|
936
|
+
*/
|
|
937
|
+
function runPendingMigrations(dryRun = false) {
|
|
938
|
+
const state = loadMigrationState();
|
|
939
|
+
const pending = ALL_MIGRATIONS.filter((m) => !state.applied.includes(m.id));
|
|
940
|
+
if (pending.length === 0) {
|
|
941
|
+
console.log(" No pending migrations.");
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
for (const migration of pending) {
|
|
945
|
+
console.log(`\n → \x1b[1m${migration.id}\x1b[0m ${migration.description}`);
|
|
946
|
+
if (dryRun) {
|
|
947
|
+
console.log(` \x1b[90m(dry-run: skipped)\x1b[0m`);
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
migration.run();
|
|
951
|
+
state.applied.push(migration.id);
|
|
952
|
+
saveMigrationState(state);
|
|
953
|
+
console.log(` \x1b[32m✓ Applied\x1b[0m`);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Mark all known migrations as applied without running them.
|
|
958
|
+
*
|
|
959
|
+
* Called at the end of a fresh `setup()` run: a brand-new server has
|
|
960
|
+
* already been configured correctly, so migrations that fix older
|
|
961
|
+
* installations are not needed.
|
|
962
|
+
*/
|
|
963
|
+
function markAllMigrationsApplied() {
|
|
964
|
+
const state = loadMigrationState();
|
|
965
|
+
for (const migration of ALL_MIGRATIONS) if (!state.applied.includes(migration.id)) state.applied.push(migration.id);
|
|
966
|
+
saveMigrationState(state);
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
868
969
|
* Run the full server setup
|
|
869
970
|
*/
|
|
870
971
|
async function setup(options) {
|
|
@@ -908,6 +1009,7 @@ async function setup(options) {
|
|
|
908
1009
|
step("Server hardening");
|
|
909
1010
|
info("Skipped (--no-hardening)");
|
|
910
1011
|
}
|
|
1012
|
+
markAllMigrationsApplied();
|
|
911
1013
|
console.log("\n\x1B[32m✓ Setup complete!\x1B[0m\n");
|
|
912
1014
|
console.log("Next steps:");
|
|
913
1015
|
console.log(` 1. Edit /etc/trafic/config.toml to configure authentication`);
|
|
@@ -928,6 +1030,6 @@ async function audit() {
|
|
|
928
1030
|
printAuditResults(checks);
|
|
929
1031
|
if (checks.filter((c) => c.status === "fail").length > 0) process.exit(1);
|
|
930
1032
|
}
|
|
931
|
-
export {
|
|
1033
|
+
export { printAuditResults as a, stopIdleProjects as c, validateConfig as d, runPendingMigrations as i, loadConfig as l, setup as n, runAudit as o, listMigrations as r, startIdleScheduler as s, audit as t, parseDuration as u };
|
|
932
1034
|
|
|
933
|
-
//# sourceMappingURL=setup-
|
|
1035
|
+
//# sourceMappingURL=setup-CKwhVY6g.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup-CKwhVY6g.js","names":[],"sources":["../src/utils/config.ts","../src/tasks/stop-idle.ts","../src/setup/steps.ts","../src/setup/user.ts","../src/setup/docker.ts","../src/setup/ddev.ts","../src/setup/agent.ts","../src/setup/hardening.ts","../src/setup/audit.ts","../src/setup/migrations/0001__ddev_apt_repo.ts","../src/setup/migrations/index.ts","../src/setup/index.ts"],"sourcesContent":["import { readFileSync, existsSync } from \"node:fs\";\nimport { parse } from \"smol-toml\";\nimport type { AgentConfig, AuthConfig, AuthRule } from \"../types.js\";\n\nconst DEFAULT_CONFIG_PATHS = [\n \"/etc/trafic/config.toml\",\n \"./config.toml\",\n \"./trafic.toml\",\n];\n\nconst DEFAULT_CONFIG: AgentConfig = {\n tld: \"\",\n port: 9876,\n dbPath: \"/var/lib/trafic/db.sqlite\",\n projectListPath: \"/home/ddev/.ddev/project_list.yaml\",\n projectsDir: \"/home/ddev/www\",\n idleTimeout: \"30m\",\n idleCheckInterval: \"5m\",\n auth: {\n defaultPolicy: \"basic\",\n allowedIps: [],\n tokens: [],\n basicAuth: [],\n rules: [],\n },\n};\n\n/**\n * Parse a duration string (e.g., \"30m\", \"1h\", \"2h30m\") to milliseconds\n */\nexport function parseDuration(duration: string): number {\n const regex = /(?:(\\d+)h)?(?:(\\d+)m)?(?:(\\d+)s)?/;\n const match = duration.match(regex);\n if (!match) return 0;\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 return (hours * 3600 + minutes * 60 + seconds) * 1000;\n}\n\n/**\n * Load configuration from TOML file\n */\nexport function loadConfig(configPath?: string): AgentConfig {\n // Find config file\n let path = configPath;\n if (!path) {\n path = DEFAULT_CONFIG_PATHS.find((p) => existsSync(p));\n }\n\n if (!path || !existsSync(path)) {\n console.warn(\"No config file found, using defaults\");\n return DEFAULT_CONFIG;\n }\n\n const content = readFileSync(path, \"utf-8\");\n const parsed = parse(content) as Record<string, unknown>;\n\n // Merge with defaults\n const auth = mergeAuthConfig(parsed.auth as Record<string, unknown>);\n\n return {\n tld: (parsed.tld as string) ?? DEFAULT_CONFIG.tld,\n port: (parsed.port as number) ?? DEFAULT_CONFIG.port,\n dbPath: (parsed.db_path as string) ?? DEFAULT_CONFIG.dbPath,\n projectListPath:\n (parsed.project_list_path as string) ?? DEFAULT_CONFIG.projectListPath,\n projectsDir: (parsed.projects_dir as string) ?? DEFAULT_CONFIG.projectsDir,\n idleTimeout:\n (parsed.idle_timeout as string) ?? DEFAULT_CONFIG.idleTimeout,\n idleCheckInterval:\n (parsed.idle_check_interval as string) ??\n DEFAULT_CONFIG.idleCheckInterval,\n auth,\n };\n}\n\nfunction mergeAuthConfig(raw?: Record<string, unknown>): AuthConfig {\n if (!raw) return DEFAULT_CONFIG.auth;\n\n const rules = (raw.rules as Array<Record<string, unknown>>) ?? [];\n\n return {\n defaultPolicy:\n (raw.default_policy as AuthConfig[\"defaultPolicy\"]) ??\n DEFAULT_CONFIG.auth.defaultPolicy,\n allowedIps:\n (raw.allowed_ips as string[]) ?? DEFAULT_CONFIG.auth.allowedIps,\n tokens: (raw.tokens as string[]) ?? DEFAULT_CONFIG.auth.tokens,\n basicAuth: (raw.basic_auth as string[]) ?? DEFAULT_CONFIG.auth.basicAuth,\n rules: rules.map(\n (r): AuthRule => ({\n match: r.match as string,\n policy: r.policy as AuthRule[\"policy\"],\n tokens: r.tokens as string[] | undefined,\n allowedIps: r.allowed_ips as string[] | undefined,\n }),\n ),\n };\n}\n\n/**\n * Validate configuration\n */\nexport function validateConfig(config: AgentConfig): string[] {\n const errors: string[] = [];\n\n if (!config.tld) {\n errors.push(\"tld is required\");\n }\n\n if (config.port < 1 || config.port > 65535) {\n errors.push(\"port must be between 1 and 65535\");\n }\n\n if (\n config.auth.defaultPolicy !== \"allow\" &&\n config.auth.defaultPolicy !== \"deny\" &&\n config.auth.defaultPolicy !== \"basic\" &&\n config.auth.defaultPolicy !== \"token\"\n ) {\n errors.push('auth.default_policy must be \"allow\", \"deny\", \"basic\", or \"token\"');\n }\n\n return errors;\n}\n","import { getIdleProjects, setProjectStatus } from \"../utils/db.js\";\nimport { stopProject, getProjectInfo, loadProjectList } from \"../utils/ddev.js\";\nimport { parseDuration } from \"../utils/config.js\";\nimport { loadProjectConfig, shouldNeverStop, getIdleTimeoutMs } from \"../utils/project-config.js\";\nimport type { AgentConfig } from \"../types.js\";\n\n/**\n * Stop idle projects that haven't been accessed recently\n * Respects per-project idle_timeout settings from .ddev/config.trafic.yaml\n */\nexport function stopIdleProjects(config: AgentConfig): void {\n const globalThresholdMs = parseDuration(config.idleTimeout);\n\n // Get all projects to check their individual configs\n const projects = loadProjectList(config.projectListPath);\n\n // Get projects idle according to global threshold\n const idleProjects = getIdleProjects(globalThresholdMs);\n\n for (const project of idleProjects) {\n const projectDir = projects.get(project.name);\n\n // Load per-project config if available\n if (projectDir) {\n const projectConfig = loadProjectConfig(projectDir);\n\n // Skip if project is configured to never stop\n if (shouldNeverStop(projectConfig)) {\n continue;\n }\n\n // Check project-specific idle timeout\n const projectThresholdMs = getIdleTimeoutMs(projectConfig);\n if (projectThresholdMs !== undefined) {\n const idleDuration = Date.now() - project.lastAccess;\n if (idleDuration < projectThresholdMs) {\n // Not idle according to project-specific timeout\n continue;\n }\n }\n }\n\n // Double-check project is actually running via DDEV\n const info = getProjectInfo(project.name);\n if (info?.status !== \"running\") {\n // Already stopped, just update our record\n setProjectStatus(project.name, \"stopped\");\n continue;\n }\n\n console.log(`Stopping idle project: ${project.name}`);\n const success = stopProject(project.name);\n\n if (success) {\n setProjectStatus(project.name, \"stopped\");\n console.log(`Stopped: ${project.name}`);\n } else {\n console.error(`Failed to stop: ${project.name}`);\n }\n }\n}\n\n/**\n * Start the idle check scheduler\n */\nexport function startIdleScheduler(config: AgentConfig): NodeJS.Timeout {\n const intervalMs = parseDuration(config.idleCheckInterval);\n\n console.log(\n `Idle scheduler started: checking every ${config.idleCheckInterval}, timeout ${config.idleTimeout}`,\n );\n\n // Run immediately on start\n stopIdleProjects(config);\n\n // Then run on interval\n return setInterval(() => {\n stopIdleProjects(config);\n }, intervalMs);\n}\n","import { execSync } from \"node:child_process\";\nimport { writeFileSync as fsWriteFile } from \"node:fs\";\n\nlet stepNumber = 0;\nlet dryRun = false;\n\n/**\n * Set dry-run mode\n */\nexport function setDryRun(value: boolean): void {\n dryRun = value;\n}\n\n/**\n * Reset step counter\n */\nexport function resetSteps(): void {\n stepNumber = 0;\n}\n\n/**\n * Log a step\n */\nexport function step(message: string): void {\n stepNumber++;\n console.log(`\\n\\x1b[36m[${stepNumber}]\\x1b[0m ${message}`);\n}\n\n/**\n * Log success\n */\nexport function success(message: string): void {\n console.log(` \\x1b[32m✓\\x1b[0m ${message}`);\n}\n\n/**\n * Log warning\n */\nexport function warn(message: string): void {\n console.log(` \\x1b[33m⚠\\x1b[0m ${message}`);\n}\n\n/**\n * Log error\n */\nexport function error(message: string): void {\n console.log(` \\x1b[31m✗\\x1b[0m ${message}`);\n}\n\n/**\n * Log info\n */\nexport function info(message: string): void {\n console.log(` ${message}`);\n}\n\n/**\n * Execute a command (or show in dry-run mode)\n */\nexport function exec(command: string, options?: { silent?: boolean }): string {\n if (dryRun) {\n if (!options?.silent) {\n console.log(` \\x1b[90m$ ${command}\\x1b[0m`);\n }\n return \"\";\n }\n\n try {\n return execSync(command, {\n encoding: \"utf-8\",\n stdio: options?.silent ? \"pipe\" : \"inherit\",\n });\n } catch (err) {\n const e = err as { status?: number; stderr?: string };\n throw new Error(`Command failed: ${command}\\n${e.stderr ?? \"\"}`);\n }\n}\n\n/**\n * Execute a command and return output (silent)\n */\nexport function execSilent(command: string): string {\n if (dryRun) {\n return \"\";\n }\n\n try {\n return execSync(command, {\n encoding: \"utf-8\",\n stdio: \"pipe\",\n }).trim();\n } catch {\n return \"\";\n }\n}\n\n/**\n * Check if a command exists\n */\nexport function commandExists(command: string): boolean {\n try {\n execSync(`which ${command}`, { stdio: \"pipe\" });\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Check if running as root\n */\nexport function isRoot(): boolean {\n return process.getuid?.() === 0;\n}\n\n/**\n * Write a file (or show in dry-run mode)\n */\nexport function writeFile(path: string, content: string): void {\n if (dryRun) {\n console.log(` \\x1b[90mWrite ${path}:\\x1b[0m`);\n const lines = content.split(\"\\n\").slice(0, 5);\n for (const line of lines) {\n console.log(` \\x1b[90m ${line}\\x1b[0m`);\n }\n if (content.split(\"\\n\").length > 5) {\n console.log(` \\x1b[90m ...\\x1b[0m`);\n }\n return;\n }\n\n fsWriteFile(path, content);\n}\n","import { step, success, info, exec, execSilent, warn } from \"./steps.js\";\n\n/**\n * Create the ddev user with proper permissions\n */\nexport function createDdevUser(): void {\n step(\"Create ddev user\");\n\n // Check if user exists\n const userExists = execSilent(\"id -u ddev 2>/dev/null\");\n if (userExists) {\n info(\"User 'ddev' already exists\");\n } else {\n exec(\"useradd -m -s /bin/bash ddev\");\n success(\"Created user 'ddev'\");\n }\n\n // Add to docker group (will be created by Docker install)\n exec(\"usermod -aG docker ddev 2>/dev/null || true\", { silent: true });\n\n // Create www directory\n exec(\"mkdir -p /home/ddev/www\");\n exec(\"chown ddev:ddev /home/ddev/www\");\n exec(\"chmod 750 /home/ddev/www\");\n success(\"Created /home/ddev/www\");\n\n // Create .ssh directory if needed\n exec(\"mkdir -p /home/ddev/.ssh\");\n exec(\"chmod 700 /home/ddev/.ssh\");\n exec(\"chown ddev:ddev /home/ddev/.ssh\");\n\n // Note: ddev user doesn't need sudo when DNS is properly configured\n // - Docker access: via docker group membership\n // - DDEV: runs as regular user, no sudo needed\n // - Agent: runs as systemd service (root manages it)\n // - Hostname resolution: via DNS (*.tld → server IP), not /etc/hosts\n //\n // If DNS is NOT configured for the custom TLD, DDEV will try to edit\n // /etc/hosts which requires sudo. In that case, you'd need:\n // echo \"ddev ALL=(ALL) NOPASSWD: /usr/local/bin/ddev-hostname\" >> /etc/sudoers.d/ddev\n // But it's better to just configure DNS properly.\n success(\"User ddev configured (no sudo needed with proper DNS)\");\n}\n\n/**\n * Setup authorized_keys for ddev user\n */\nexport function setupAuthorizedKeys(publicKey?: string): void {\n if (!publicKey) {\n warn(\"No SSH public key provided, skipping authorized_keys setup\");\n return;\n }\n\n step(\"Setup SSH authorized_keys\");\n\n const authKeysPath = \"/home/ddev/.ssh/authorized_keys\";\n exec(`touch ${authKeysPath}`);\n exec(`chmod 600 ${authKeysPath}`);\n exec(`chown ddev:ddev ${authKeysPath}`);\n\n // Check if key already exists\n const existing = execSilent(`cat ${authKeysPath}`);\n if (existing.includes(publicKey.trim())) {\n info(\"SSH key already authorized\");\n } else {\n exec(`echo '${publicKey}' >> ${authKeysPath}`);\n success(\"Added SSH public key\");\n }\n}\n","import { writeFileSync, existsSync, readFileSync } from \"node:fs\";\nimport { step, success, info, exec, commandExists, warn } from \"./steps.js\";\n\n/**\n * Install Docker using the official script\n */\nexport function installDocker(): void {\n step(\"Install Docker\");\n\n if (commandExists(\"docker\")) {\n const version = exec(\"docker --version\", { silent: true });\n info(`Docker already installed: ${version?.trim() ?? \"unknown version\"}`);\n return;\n }\n\n // Install Docker using official script\n info(\"Downloading and running Docker install script...\");\n exec(\"curl -fsSL https://get.docker.com -o /tmp/get-docker.sh\");\n exec(\"sh /tmp/get-docker.sh\", { silent: true });\n exec(\"rm /tmp/get-docker.sh\");\n\n // Enable and start Docker\n exec(\"systemctl enable docker\", { silent: true });\n exec(\"systemctl start docker\", { silent: true });\n\n success(\"Docker installed and started\");\n\n // Add ddev user to docker group\n exec(\"usermod -aG docker ddev\", { silent: true });\n success(\"Added ddev user to docker group\");\n}\n\n/**\n * Configure Docker for production use\n */\nexport function configureDocker(): void {\n step(\"Configure Docker\");\n\n // Create daemon.json with production settings\n const daemonConfig = {\n \"log-driver\": \"json-file\",\n \"log-opts\": {\n \"max-size\": \"10m\",\n \"max-file\": \"3\",\n },\n \"storage-driver\": \"overlay2\",\n \"live-restore\": true,\n };\n\n exec(\"mkdir -p /etc/docker\");\n\n const configPath = \"/etc/docker/daemon.json\";\n\n // Check if config already exists\n if (existsSync(configPath)) {\n try {\n const existing = JSON.parse(readFileSync(configPath, \"utf-8\"));\n // Merge with existing config\n const merged = { ...existing, ...daemonConfig };\n writeFileSync(configPath, JSON.stringify(merged, null, 2));\n info(\"Merged with existing Docker config\");\n } catch {\n warn(\"Could not parse existing Docker config, skipping\");\n return;\n }\n } else {\n writeFileSync(configPath, JSON.stringify(daemonConfig, null, 2));\n }\n\n // Reload Docker to apply config\n exec(\"systemctl reload docker || systemctl restart docker\");\n success(\"Docker configured with production settings\");\n}\n\n/**\n * Setup Docker system prune cron job\n */\nexport function setupDockerPrune(): void {\n step(\"Setup Docker cleanup cron\");\n\n const cronContent = `# Trafic: Clean up Docker resources weekly\n0 3 * * 0 root docker system prune -af --volumes 2>&1 | logger -t docker-prune\n`;\n\n writeFileSync(\"/etc/cron.d/trafic-docker-prune\", cronContent);\n exec(\"chmod 644 /etc/cron.d/trafic-docker-prune\");\n\n success(\"Docker prune scheduled weekly at 3am on Sundays\");\n}\n","import { writeFileSync } from \"node:fs\";\nimport { step, success, info, exec, commandExists } from \"./steps.js\";\n\n/**\n * Install system dependencies required by Trafic\n */\nexport function installSystemDeps(): void {\n step(\"Install system dependencies\");\n\n exec(\"apt-get update -qq\", { silent: true });\n exec(\"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y jq curl rsync\", { silent: true });\n\n success(\"System dependencies installed: jq, curl, rsync\");\n}\n\n/**\n * Add the official DDEV apt repository and GPG key.\n * Idempotent: safe to call even if the repository is already configured.\n */\nexport function addDdevAptRepo(): void {\n exec(\"install -m 0755 -d /etc/apt/keyrings\", { silent: true });\n exec(\n \"curl -fsSL https://pkg.ddev.com/apt/gpg.key | gpg --dearmor | tee /etc/apt/keyrings/ddev.gpg > /dev/null\",\n { silent: true },\n );\n exec(\"chmod a+r /etc/apt/keyrings/ddev.gpg\", { silent: true });\n exec(\n \"echo \\\"deb [signed-by=/etc/apt/keyrings/ddev.gpg] https://pkg.ddev.com/apt/ * *\\\" | tee /etc/apt/sources.list.d/ddev.list > /dev/null\",\n { silent: true },\n );\n}\n\n/**\n * Install DDEV\n */\nexport function installDdev(): void {\n step(\"Install DDEV\");\n\n if (commandExists(\"ddev\")) {\n const version = exec(\"ddev --version 2>/dev/null | head -1\", { silent: true });\n info(`DDEV already installed: ${version?.trim() ?? \"unknown version\"}`);\n return;\n }\n\n // Install DDEV via the official apt repository\n // See https://docs.ddev.com/en/stable/users/install/ddev-installation/#debianubuntu\n info(\"Adding DDEV apt repository...\");\n addDdevAptRepo();\n\n info(\"Installing DDEV...\");\n exec(\"apt-get update -qq\", { silent: true });\n exec(\"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ddev\", { silent: true });\n\n // Initialize mkcert certificate authority\n exec(\"mkcert -install\", { silent: true });\n\n const version = exec(\"ddev --version 2>/dev/null | head -1\", { silent: true });\n success(`DDEV installed: ${version?.trim() ?? \"unknown version\"}`);\n}\n\n/**\n * Configure DDEV global settings for production\n *\n * Note on DNS and /etc/hosts:\n * When using a custom TLD (not ddev.site), DDEV may try to edit /etc/hosts\n * if it can't resolve project hostnames via DNS. To avoid this:\n * 1. Configure DNS so *.{tld} resolves to the server's IP (recommended)\n * 2. Or install dnsmasq for local wildcard DNS resolution\n *\n * With proper DNS, DDEV won't need sudo for hostname management.\n */\nexport function configureDdev(tld: string, email?: string): void {\n step(\"Configure DDEV\");\n\n // Run as ddev user\n const ddevCmd = (cmd: string) => `su - ddev -c '${cmd}'`;\n\n // Configure all global settings in a single call\n exec(\n ddevCmd(\n `ddev config global --project-tld=${tld} --router-http-port=80 --router-https-port=443 --use-letsencrypt=${email ? \"true\" : \"false\"} ${email ? `--letsencrypt-email=${email}` : \"\"} --instrumentation-opt-in=false`,\n ),\n { silent: true },\n );\n\n success(`DDEV configured with TLD: ${tld}`);\n info(`Ensure DNS is configured: *.${tld} → server IP`);\n\n if (email) {\n success(`Let's Encrypt enabled with email: ${email}`);\n }\n\n success(\"DDEV global settings configured\");\n}\n\n/**\n * Install dnsmasq for local DNS resolution (optional)\n * This allows the server itself to resolve *.{tld} hostnames\n * without needing to edit /etc/hosts for each project\n */\nexport function installDnsmasq(tld: string): void {\n step(\"Install dnsmasq for local DNS\");\n\n exec(\"apt-get update && apt-get install -y dnsmasq\", { silent: true });\n\n // Get server's public IP\n const serverIp = exec(\n \"curl -4 -s ifconfig.me || hostname -I | awk '{print $1}'\",\n { silent: true },\n )?.trim() || \"127.0.0.1\";\n\n // Configure dnsmasq to resolve *.{tld} to server IP\n const dnsmasqConfig = `# Trafic: Local DNS for DDEV projects\n# Resolve all *.${tld} to this server\naddress=/${tld}/${serverIp}\n\n# Don't read /etc/resolv.conf\nno-resolv\n\n# Use upstream DNS for everything else\nserver=1.1.1.1\nserver=8.8.8.8\n\n# Listen on localhost only\nlisten-address=127.0.0.1\nbind-interfaces\n`;\n\n writeFileSync(\"/etc/dnsmasq.d/trafic.conf\", dnsmasqConfig);\n\n // Configure system to use local dnsmasq\n exec(\"systemctl restart dnsmasq\");\n exec(\"systemctl enable dnsmasq\");\n\n // Update resolv.conf to use local DNS first\n info(\"Configure /etc/resolv.conf to use 127.0.0.1 as primary DNS\");\n\n success(`dnsmasq configured: *.${tld} → ${serverIp}`);\n}\n\n/**\n * Configure Traefik for forward auth\n */\nexport function configureTraefik(): void {\n step(\"Configure Traefik for forward auth\");\n\n // Create custom Traefik config directory\n exec(\"mkdir -p /home/ddev/.ddev/traefik\", { silent: true });\n exec(\"chown -R ddev:ddev /home/ddev/.ddev\", { silent: true });\n\n // Static configuration for Trafic middleware\n const staticConfig = `# Trafic: Forward auth middleware\nhttp:\n middlewares:\n trafic-auth:\n forwardAuth:\n address: \"http://host.docker.internal:9876/__auth__\"\n authResponseHeaders:\n - \"X-Trafic-Project\"\n\n trafic-errors:\n errors:\n status:\n - \"502\"\n - \"503\"\n service: trafic-service\n query: \"/\"\n\n services:\n trafic-service:\n loadBalancer:\n servers:\n - url: \"http://host.docker.internal:9876\"\n`;\n\n writeFileSync(\"/home/ddev/.ddev/traefik/trafic.yaml\", staticConfig);\n exec(\"chown ddev:ddev /home/ddev/.ddev/traefik/trafic.yaml\", { silent: true });\n\n // Create default router config that applies middleware to all projects\n const routerConfig = `# Trafic: Default router configuration\n# This file makes all DDEV projects use the Trafic auth middleware\nhttp:\n routers:\n trafic-catch-all:\n rule: \"HostRegexp(\\`.+\\`)\"\n priority: 1\n middlewares:\n - trafic-auth\n - trafic-errors\n service: api@internal\n`;\n\n writeFileSync(\"/home/ddev/.ddev/traefik/dynamic.yaml\", routerConfig);\n exec(\"chown ddev:ddev /home/ddev/.ddev/traefik/dynamic.yaml\", { silent: true });\n\n success(\"Traefik configured with Trafic middleware\");\n}\n","import { step, success, info, exec, commandExists } from \"./steps.js\";\nimport { writeFileSync, existsSync } from \"node:fs\";\n\n/**\n * Install Node.js using fnm\n */\nexport function installNode(): void {\n step(\"Install Node.js\");\n\n // Check if Node is already installed\n if (commandExists(\"node\")) {\n const version = exec(\"node --version\", { silent: true });\n info(`Node.js already installed: ${version?.trim()}`);\n return;\n }\n\n // Install fnm (Fast Node Manager)\n exec(\n 'curl -fsSL https://fnm.vercel.app/install | bash -s -- --install-dir \"/opt/fnm\" --skip-shell',\n );\n\n // Add fnm to system path\n const profileContent = `# Trafic: fnm (Node.js version manager)\nexport FNM_DIR=\"/opt/fnm\"\nexport PATH=\"/opt/fnm:$PATH\"\neval \"$(fnm env)\"\n`;\n\n writeFileSync(\"/etc/profile.d/fnm.sh\", profileContent);\n exec(\"chmod 644 /etc/profile.d/fnm.sh\");\n\n // Install Node.js 24\n exec(\"export FNM_DIR=/opt/fnm && /opt/fnm/fnm install 24\");\n exec(\"export FNM_DIR=/opt/fnm && /opt/fnm/fnm default 24\");\n\n // Create symlinks for system-wide access\n exec(\n \"ln -sf /opt/fnm/aliases/default/bin/node /usr/local/bin/node || true\",\n );\n exec(\"ln -sf /opt/fnm/aliases/default/bin/npm /usr/local/bin/npm || true\");\n exec(\"ln -sf /opt/fnm/aliases/default/bin/npx /usr/local/bin/npx || true\");\n\n success(\"Node.js 24 installed via fnm\");\n}\n\n/**\n * Install the Trafic agent\n */\nexport function installAgent(): void {\n step(\"Install Trafic agent\");\n\n // Install the agent globally\n exec(\"npm install -g @studiometa/trafic-agent\");\n\n success(\"Trafic agent installed\");\n}\n\n/**\n * Create the agent configuration\n */\nexport function createAgentConfig(tld: string, _email?: string): void {\n step(\"Create agent configuration\");\n\n // Create directories\n exec(\"mkdir -p /etc/trafic\");\n exec(\"mkdir -p /var/lib/trafic\");\n exec(\"chown ddev:ddev /var/lib/trafic\");\n\n const configPath = \"/etc/trafic/config.toml\";\n\n if (existsSync(configPath)) {\n info(\"Configuration already exists at /etc/trafic/config.toml — skipping\");\n info(\"Edit this file to configure authentication\");\n return;\n }\n\n const config = `# Trafic agent configuration\n# Generated by trafic setup\n\n# Required: TLD for DDEV projects\ntld = \"${tld}\"\n\n# Agent HTTP server port\nport = 9876\n\n# Database and paths\ndb_path = \"/var/lib/trafic/db.sqlite\"\nproject_list_path = \"/home/ddev/.ddev/project_list.yaml\"\nprojects_dir = \"/home/ddev/www\"\n\n# Scale-to-zero settings\nidle_timeout = \"4h\"\nidle_check_interval = \"5m\"\n\n# Authentication\n[auth]\ndefault_policy = \"basic\"\nallowed_ips = []\ntokens = []\nbasic_auth = []\nrules = []\n`;\n\n writeFileSync(configPath, config);\n exec(\"chmod 640 /etc/trafic/config.toml\");\n exec(\"chown root:ddev /etc/trafic/config.toml\");\n\n success(\"Configuration created at /etc/trafic/config.toml\");\n info(\"Edit this file to configure authentication\");\n}\n\n/**\n * Create systemd service for the agent\n */\nexport function createSystemdService(): void {\n step(\"Create systemd service\");\n\n // Find the trafic-agent binary path\n const agentPath = exec(\"which trafic-agent\", { silent: true })?.trim() || \"/usr/bin/trafic-agent\";\n\n const serviceContent = `[Unit]\nDescription=Trafic Agent - DDEV preview environment manager\nAfter=network.target docker.service\nRequires=docker.service\n\n[Service]\nType=simple\nUser=ddev\nGroup=ddev\nExecStart=${agentPath} start --config /etc/trafic/config.toml\nRestart=always\nRestartSec=5\nStandardOutput=journal\nStandardError=journal\nSyslogIdentifier=trafic-agent\n\n# Environment\nEnvironment=NODE_ENV=production\n# Skip ddev-hostname /etc/hosts management — on a server, hostnames resolve\n# to the public IP (not localhost), which would trigger ddev-hostname on every\n# ddev start. DDEV_NONINTERACTIVE=true skips that entirely.\nEnvironment=DDEV_NONINTERACTIVE=true\n\n# Security hardening\nNoNewPrivileges=true\nProtectSystem=strict\nProtectHome=read-only\nReadWritePaths=/var/lib/trafic /home/ddev/.ddev\nPrivateTmp=true\n\n[Install]\nWantedBy=multi-user.target\n`;\n\n writeFileSync(\"/etc/systemd/system/trafic-agent.service\", serviceContent);\n exec(\"chmod 644 /etc/systemd/system/trafic-agent.service\");\n\n // Reload systemd and enable service\n exec(\"systemctl daemon-reload\");\n exec(\"systemctl enable trafic-agent\");\n exec(\"systemctl start trafic-agent\");\n\n success(\"Trafic agent service started\");\n info(\"View logs: journalctl -u trafic-agent -f\");\n}\n","import { step, success, info, exec, warn, commandExists } from \"./steps.js\";\nimport { writeFileSync, existsSync } from \"node:fs\";\n\n/**\n * Configure SSH hardening\n */\nexport function hardenSsh(allowedUsers: string[] = [\"ddev\"]): void {\n step(\"Harden SSH configuration\");\n\n const sshConfig = `# Trafic SSH hardening\n# Generated by trafic setup\n\n# Allow root login via SSH key only (no password) — required for server management\nPermitRootLogin prohibit-password\n\n# Disable password authentication (keys only)\nPasswordAuthentication no\nPubkeyAuthentication yes\n\n# Limit authentication attempts\nMaxAuthTries 3\n\n# Idle timeout (5 minutes)\nClientAliveInterval 300\nClientAliveCountMax 2\n\n# Allowed users (root kept for emergency access)\nAllowUsers root ${allowedUsers.join(\" \")}\n\n# Disable X11 forwarding\nX11Forwarding no\n\n# Disable TCP forwarding\nAllowTcpForwarding no\n\n# Use strong algorithms only\nKexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com\nMACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com\n`;\n\n writeFileSync(\"/etc/ssh/sshd_config.d/trafic.conf\", sshConfig);\n exec(\"chmod 644 /etc/ssh/sshd_config.d/trafic.conf\");\n\n // Test config before restarting\n const testResult = exec(\"sshd -t 2>&1 || echo 'error'\", { silent: true });\n if (testResult?.includes(\"error\")) {\n warn(\"SSH config test failed, reverting...\");\n exec(\"rm /etc/ssh/sshd_config.d/trafic.conf\");\n return;\n }\n\n exec(\"systemctl reload ssh || systemctl reload sshd\");\n success(\"SSH hardened: password auth disabled, root key-only login retained\");\n info(`Allowed users: root, ${allowedUsers.join(\", \")}`);\n}\n\n/**\n * Configure UFW firewall\n */\nexport function configureFirewall(): void {\n step(\"Configure firewall (UFW)\");\n\n // Install UFW if not present\n if (!commandExists(\"ufw\")) {\n exec(\"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ufw\", { silent: true });\n }\n\n // Reset and configure\n exec(\"ufw --force reset\", { silent: true });\n exec(\"ufw default deny incoming\");\n exec(\"ufw default allow outgoing\");\n exec(\"ufw allow 22/tcp comment 'SSH'\");\n exec(\"ufw allow 80/tcp comment 'HTTP'\");\n exec(\"ufw allow 443/tcp comment 'HTTPS'\");\n\n // Enable firewall\n exec(\"ufw --force enable\");\n\n success(\"Firewall enabled: SSH (22), HTTP (80), HTTPS (443) allowed\");\n}\n\n/**\n * Configure automatic security updates\n */\nexport function configureUnattendedUpgrades(): void {\n step(\"Configure automatic security updates\");\n\n // Install unattended-upgrades\n exec(\n \"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y unattended-upgrades apt-listchanges\",\n { silent: true },\n );\n\n // Configure for security updates only\n const upgradesConfig = `// Trafic: Automatic security updates\nUnattended-Upgrade::Allowed-Origins {\n \"\\${distro_id}:\\${distro_codename}\";\n \"\\${distro_id}:\\${distro_codename}-security\";\n \"\\${distro_id}ESMApps:\\${distro_codename}-apps-security\";\n \"\\${distro_id}ESM:\\${distro_codename}-infra-security\";\n};\n\n// Do not automatically reboot\nUnattended-Upgrade::Automatic-Reboot \"false\";\n\n// Remove unused dependencies\nUnattended-Upgrade::Remove-Unused-Dependencies \"true\";\n\n// Email notifications (optional)\n// Unattended-Upgrade::Mail \"root\";\n`;\n\n writeFileSync(\n \"/etc/apt/apt.conf.d/50unattended-upgrades\",\n upgradesConfig,\n );\n\n // Enable automatic updates\n const autoConfig = `APT::Periodic::Update-Package-Lists \"1\";\nAPT::Periodic::Download-Upgradeable-Packages \"1\";\nAPT::Periodic::AutocleanInterval \"7\";\nAPT::Periodic::Unattended-Upgrade \"1\";\n`;\n\n writeFileSync(\"/etc/apt/apt.conf.d/20auto-upgrades\", autoConfig);\n\n // Enable the service\n exec(\"systemctl enable unattended-upgrades\");\n exec(\"systemctl start unattended-upgrades\");\n\n success(\"Automatic security updates enabled\");\n}\n\n/**\n * Configure Fail2ban\n */\nexport function configureFail2ban(): void {\n step(\"Configure Fail2ban\");\n\n // Install fail2ban\n if (!commandExists(\"fail2ban-client\")) {\n exec(\"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y fail2ban\", { silent: true });\n }\n\n // Configure jail\n const jailConfig = `# Trafic: Fail2ban configuration\n[DEFAULT]\nbantime = 3600\nfindtime = 600\nmaxretry = 5\nbackend = auto\n\n[sshd]\nenabled = true\nport = ssh\nfilter = sshd\nlogpath = /var/log/auth.log\nmaxretry = 3\nbantime = 7200\n`;\n\n writeFileSync(\"/etc/fail2ban/jail.local\", jailConfig);\n exec(\"chmod 644 /etc/fail2ban/jail.local\");\n\n // Restart fail2ban\n exec(\"systemctl enable fail2ban\");\n exec(\"systemctl restart fail2ban\");\n\n success(\"Fail2ban enabled: SSH brute force protection active\");\n}\n\n/**\n * Configure system limits\n */\nexport function configureSystemLimits(): void {\n step(\"Configure system limits\");\n\n const limitsConfig = `# Trafic: System limits for ddev user\nddev soft nofile 65536\nddev hard nofile 65536\nddev soft nproc 4096\nddev hard nproc 4096\n`;\n\n writeFileSync(\"/etc/security/limits.d/trafic.conf\", limitsConfig);\n exec(\"chmod 644 /etc/security/limits.d/trafic.conf\");\n\n success(\"System limits configured for ddev user\");\n}\n\n/**\n * Set proper file permissions\n */\nexport function configureFilePermissions(): void {\n step(\"Configure file permissions\");\n\n // Create directories\n exec(\"mkdir -p /etc/trafic\");\n exec(\"mkdir -p /var/lib/trafic\");\n exec(\"mkdir -p /var/backups/trafic\");\n\n // Set permissions\n exec(\"chmod 755 /etc/trafic\");\n exec(\"chmod 700 /var/lib/trafic\");\n exec(\"chmod 700 /var/backups/trafic\");\n exec(\"chown root:ddev /etc/trafic\");\n exec(\"chown ddev:ddev /var/lib/trafic\");\n exec(\"chown ddev:ddev /var/backups/trafic\");\n\n // Config file (if exists)\n if (existsSync(\"/etc/trafic/config.toml\")) {\n exec(\"chmod 640 /etc/trafic/config.toml\");\n exec(\"chown root:ddev /etc/trafic/config.toml\");\n }\n\n success(\"File permissions configured\");\n}\n\n/**\n * Run all hardening steps\n */\nexport function hardenServer(sshUsers: string[] = [\"ddev\"]): void {\n hardenSsh(sshUsers);\n configureFirewall();\n configureUnattendedUpgrades();\n configureFail2ban();\n configureSystemLimits();\n configureFilePermissions();\n}\n","import { execSync } from \"node:child_process\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport type { AuditCheck } from \"./types.js\";\n\n/**\n * Run a command silently and return output\n */\nfunction execSilent(command: string): string {\n try {\n return execSync(command, { encoding: \"utf-8\", stdio: \"pipe\" }).trim();\n } catch {\n return \"\";\n }\n}\n\n/**\n * Check if a service is active\n */\nfunction isServiceActive(service: string): boolean {\n const status = execSilent(`systemctl is-active ${service}`);\n return status === \"active\";\n}\n\n/**\n * Audit SSH configuration\n */\nfunction auditSsh(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Check root login\n const sshdConfig = existsSync(\"/etc/ssh/sshd_config\")\n ? readFileSync(\"/etc/ssh/sshd_config\", \"utf-8\")\n : \"\";\n const traficConfig = existsSync(\"/etc/ssh/sshd_config.d/trafic.conf\")\n ? readFileSync(\"/etc/ssh/sshd_config.d/trafic.conf\", \"utf-8\")\n : \"\";\n\n const config = sshdConfig + \"\\n\" + traficConfig;\n\n const rootLoginDisabled =\n config.includes(\"PermitRootLogin no\") ||\n config.includes(\"PermitRootLogin prohibit-password\");\n checks.push({\n name: \"SSH root login\",\n status: rootLoginDisabled ? \"pass\" : \"fail\",\n message: rootLoginDisabled\n ? \"Root login disabled\"\n : \"Root login may be enabled\",\n fix: \"Add 'PermitRootLogin no' to /etc/ssh/sshd_config.d/trafic.conf\",\n });\n\n const passwordAuthDisabled = config.includes(\"PasswordAuthentication no\");\n checks.push({\n name: \"SSH password auth\",\n status: passwordAuthDisabled ? \"pass\" : \"warn\",\n message: passwordAuthDisabled\n ? \"Password authentication disabled\"\n : \"Password authentication may be enabled\",\n fix: \"Add 'PasswordAuthentication no' to /etc/ssh/sshd_config.d/trafic.conf\",\n });\n\n return checks;\n}\n\n/**\n * Audit firewall configuration\n */\nfunction auditFirewall(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n const ufwStatus = execSilent(\"ufw status\");\n const isActive = ufwStatus.includes(\"Status: active\");\n\n checks.push({\n name: \"Firewall (UFW)\",\n status: isActive ? \"pass\" : \"fail\",\n message: isActive ? \"UFW is active\" : \"UFW is not active\",\n fix: \"Run: ufw enable\",\n });\n\n if (isActive) {\n const allows22 = ufwStatus.includes(\"22/tcp\");\n const allows80 = ufwStatus.includes(\"80/tcp\");\n const allows443 = ufwStatus.includes(\"443/tcp\");\n\n if (allows22 && allows80 && allows443) {\n checks.push({\n name: \"Firewall ports\",\n status: \"pass\",\n message: \"Required ports (22, 80, 443) are open\",\n });\n } else {\n checks.push({\n name: \"Firewall ports\",\n status: \"warn\",\n message: \"Some required ports may be blocked\",\n fix: \"Run: ufw allow 22/tcp && ufw allow 80/tcp && ufw allow 443/tcp\",\n });\n }\n }\n\n return checks;\n}\n\n/**\n * Audit security services\n */\nfunction auditServices(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Fail2ban\n const fail2banActive = isServiceActive(\"fail2ban\");\n checks.push({\n name: \"Fail2ban\",\n status: fail2banActive ? \"pass\" : \"warn\",\n message: fail2banActive ? \"Fail2ban is active\" : \"Fail2ban is not running\",\n fix: \"Run: apt install fail2ban && systemctl enable fail2ban --now\",\n });\n\n // Unattended upgrades\n const unattendedActive = isServiceActive(\"unattended-upgrades\");\n checks.push({\n name: \"Automatic updates\",\n status: unattendedActive ? \"pass\" : \"warn\",\n message: unattendedActive\n ? \"Unattended upgrades enabled\"\n : \"Automatic updates not configured\",\n fix: \"Run: apt install unattended-upgrades && dpkg-reconfigure unattended-upgrades\",\n });\n\n // Trafic agent\n const agentActive = isServiceActive(\"trafic-agent\");\n checks.push({\n name: \"Trafic agent\",\n status: agentActive ? \"pass\" : \"fail\",\n message: agentActive\n ? \"Trafic agent is running\"\n : \"Trafic agent is not running\",\n fix: \"Run: systemctl start trafic-agent\",\n });\n\n return checks;\n}\n\n/**\n * Audit Docker\n */\nfunction auditDocker(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Docker running\n const dockerActive = isServiceActive(\"docker\");\n checks.push({\n name: \"Docker\",\n status: dockerActive ? \"pass\" : \"fail\",\n message: dockerActive ? \"Docker is running\" : \"Docker is not running\",\n fix: \"Run: systemctl start docker\",\n });\n\n // Dangling images\n const danglingImages = execSilent(\n \"docker images -f dangling=true -q | wc -l\",\n );\n const count = parseInt(danglingImages, 10) || 0;\n if (count > 0) {\n checks.push({\n name: \"Docker cleanup\",\n status: \"warn\",\n message: `${count} dangling Docker images`,\n fix: \"Run: docker system prune -f\",\n });\n }\n\n // Disk usage\n const diskUsage = execSilent(\"df -h / | awk 'NR==2{print $5}'\");\n const usagePercent = parseInt(diskUsage, 10) || 0;\n checks.push({\n name: \"Disk usage\",\n status: usagePercent > 90 ? \"fail\" : usagePercent > 80 ? \"warn\" : \"pass\",\n message: `Disk usage: ${diskUsage}`,\n fix: usagePercent > 80 ? \"Consider cleaning up old projects or Docker resources\" : undefined,\n });\n\n return checks;\n}\n\n/**\n * Audit file permissions\n */\nfunction auditPermissions(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Config file\n if (existsSync(\"/etc/trafic/config.toml\")) {\n const stats = execSilent(\"stat -c '%a' /etc/trafic/config.toml\");\n const isSecure = stats === \"640\" || stats === \"600\";\n checks.push({\n name: \"Config permissions\",\n status: isSecure ? \"pass\" : \"warn\",\n message: isSecure\n ? \"Config file has secure permissions\"\n : `Config file permissions: ${stats} (should be 640)`,\n fix: \"Run: chmod 640 /etc/trafic/config.toml\",\n });\n }\n\n // Projects directory\n if (existsSync(\"/home/ddev/www\")) {\n const owner = execSilent(\"stat -c '%U:%G' /home/ddev/www\");\n const isCorrect = owner === \"ddev:ddev\";\n checks.push({\n name: \"Projects directory\",\n status: isCorrect ? \"pass\" : \"warn\",\n message: isCorrect\n ? \"Projects directory owned by ddev\"\n : `Projects directory owner: ${owner}`,\n fix: \"Run: chown ddev:ddev /home/ddev/www\",\n });\n }\n\n return checks;\n}\n\n/**\n * Run all audit checks\n */\nexport function runAudit(): AuditCheck[] {\n return [\n ...auditSsh(),\n ...auditFirewall(),\n ...auditServices(),\n ...auditDocker(),\n ...auditPermissions(),\n ];\n}\n\n/**\n * Print audit results\n */\nexport function printAuditResults(checks: AuditCheck[]): void {\n console.log(\"\\n\\x1b[1mSecurity Audit Results\\x1b[0m\\n\");\n\n for (const check of checks) {\n const icon =\n check.status === \"pass\"\n ? \"\\x1b[32m✓\\x1b[0m\"\n : check.status === \"warn\"\n ? \"\\x1b[33m⚠\\x1b[0m\"\n : \"\\x1b[31m✗\\x1b[0m\";\n\n console.log(`${icon} \\x1b[1m${check.name}\\x1b[0m: ${check.message}`);\n\n if (check.fix && check.status !== \"pass\") {\n console.log(` \\x1b[90m${check.fix}\\x1b[0m`);\n }\n }\n\n const passed = checks.filter((c) => c.status === \"pass\").length;\n const warnings = checks.filter((c) => c.status === \"warn\").length;\n const failed = checks.filter((c) => c.status === \"fail\").length;\n\n console.log(\n `\\n\\x1b[1mSummary:\\x1b[0m ${passed} passed, ${warnings} warnings, ${failed} failed`,\n );\n}\n","import { existsSync } from \"node:fs\";\nimport { exec } from \"../steps.js\";\nimport { addDdevAptRepo } from \"../ddev.js\";\nimport type { Migration } from \"../types.js\";\n\n/**\n * Migration 0001: Migrate DDEV from manual tarball to apt repository.\n *\n * Old installs downloaded a tarball from GitHub releases. This migration\n * removes the manually-installed binaries and reinstalls DDEV via the\n * official apt repository so future updates are handled by apt.\n *\n * Condition: skip if /etc/apt/sources.list.d/ddev.list already exists,\n * which means the apt repository is already configured.\n */\nexport const migration0001DdevAptRepo: Migration = {\n id: \"0001__ddev_apt_repo\",\n description: \"Migrate DDEV from manual tarball to apt repository\",\n\n run(): void {\n // Already using the apt repository — nothing to do\n if (existsSync(\"/etc/apt/sources.list.d/ddev.list\")) {\n return;\n }\n\n // 1. Remove manually-installed DDEV binaries\n exec(\"rm -f /usr/local/bin/ddev /usr/local/bin/ddev-hostname /usr/local/bin/mkcert\", {\n silent: true,\n });\n\n // 2. Add the official DDEV apt repository\n addDdevAptRepo();\n\n // 3. Install DDEV via apt\n exec(\"apt-get update -qq\", { silent: true });\n exec(\"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ddev\", {\n silent: true,\n });\n\n // 4. Re-install mkcert CA (mkcert is now bundled with DDEV via apt)\n exec(\"mkcert -install\", { silent: true });\n },\n};\n","import { existsSync, readFileSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport type { Migration, MigrationState, MigrationListEntry } from \"../types.js\";\nimport { migration0001DdevAptRepo } from \"./0001__ddev_apt_repo.js\";\n\n/** Path to the persisted migration state file */\nexport const MIGRATIONS_STATE_FILE = \"/etc/trafic/.migrations.json\";\n\n/** Ordered registry of all known migrations */\nexport const ALL_MIGRATIONS: Migration[] = [migration0001DdevAptRepo];\n\n// ---------------------------------------------------------------------------\n// State helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Load the current migration state from disk.\n * Returns an empty state if the file does not exist yet.\n */\nexport function loadMigrationState(): MigrationState {\n if (!existsSync(MIGRATIONS_STATE_FILE)) {\n return { applied: [] };\n }\n\n try {\n const raw = readFileSync(MIGRATIONS_STATE_FILE, \"utf-8\");\n const parsed = JSON.parse(raw) as unknown;\n if (\n parsed !== null &&\n typeof parsed === \"object\" &&\n \"applied\" in parsed &&\n Array.isArray((parsed as { applied: unknown }).applied)\n ) {\n return parsed as MigrationState;\n }\n } catch {\n // Corrupted file — start fresh\n }\n\n return { applied: [] };\n}\n\n/**\n * Persist the migration state to disk.\n * Creates /etc/trafic/ if it does not exist.\n */\nexport function saveMigrationState(state: MigrationState): void {\n mkdirSync(\"/etc/trafic\", { recursive: true });\n writeFileSync(MIGRATIONS_STATE_FILE, JSON.stringify(state, null, 2) + \"\\n\", \"utf-8\");\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Return all migrations with their current status.\n */\nexport function listMigrations(): MigrationListEntry[] {\n const state = loadMigrationState();\n return ALL_MIGRATIONS.map((migration) => ({\n migration,\n status: state.applied.includes(migration.id) ? \"applied\" : \"pending\",\n }));\n}\n\n/**\n * Run all pending migrations and persist state after each one.\n *\n * When `dryRun` is true the runner prints what it would do without\n * executing anything or touching the state file.\n */\nexport function runPendingMigrations(dryRun = false): void {\n const state = loadMigrationState();\n const pending = ALL_MIGRATIONS.filter((m) => !state.applied.includes(m.id));\n\n if (pending.length === 0) {\n console.log(\" No pending migrations.\");\n return;\n }\n\n for (const migration of pending) {\n console.log(`\\n → \\x1b[1m${migration.id}\\x1b[0m ${migration.description}`);\n\n if (dryRun) {\n console.log(` \\x1b[90m(dry-run: skipped)\\x1b[0m`);\n continue;\n }\n\n migration.run();\n\n // Persist state immediately so a partial failure leaves a consistent state\n state.applied.push(migration.id);\n saveMigrationState(state);\n\n console.log(` \\x1b[32m✓ Applied\\x1b[0m`);\n }\n}\n\n/**\n * Mark all known migrations as applied without running them.\n *\n * Called at the end of a fresh `setup()` run: a brand-new server has\n * already been configured correctly, so migrations that fix older\n * installations are not needed.\n */\nexport function markAllMigrationsApplied(): void {\n const state = loadMigrationState();\n\n for (const migration of ALL_MIGRATIONS) {\n if (!state.applied.includes(migration.id)) {\n state.applied.push(migration.id);\n }\n }\n\n saveMigrationState(state);\n}\n","import type { SetupOptions } from \"./types.js\";\nimport { step, info, error, setDryRun, resetSteps, isRoot } from \"./steps.js\";\nimport { createDdevUser } from \"./user.js\";\nimport { installDocker, configureDocker, setupDockerPrune } from \"./docker.js\";\nimport { installSystemDeps, installDdev, configureDdev, configureTraefik } from \"./ddev.js\";\nimport { installNode, installAgent, createAgentConfig, createSystemdService } from \"./agent.js\";\nimport { hardenServer } from \"./hardening.js\";\nimport { runAudit, printAuditResults } from \"./audit.js\";\nimport { markAllMigrationsApplied } from \"./migrations/index.js\";\n\nexport * from \"./types.js\";\nexport * from \"./audit.js\";\n\n/**\n * Run the full server setup\n */\nexport async function setup(options: SetupOptions): Promise<void> {\n resetSteps();\n setDryRun(options.dryRun ?? false);\n\n console.log(\"\\n\\x1b[1m🚦 Trafic Server Setup\\x1b[0m\\n\");\n\n if (options.dryRun) {\n console.log(\"\\x1b[33m⚠ Dry-run mode: no changes will be made\\x1b[0m\\n\");\n }\n\n // Check root\n if (!isRoot() && !options.dryRun) {\n error(\"This command must be run as root\");\n console.log(\" Run: sudo trafic-agent setup ...\");\n process.exit(1);\n }\n\n info(`TLD: ${options.tld}`);\n if (options.email) {\n info(`Let's Encrypt email: ${options.email}`);\n }\n if (options.noHardening) {\n info(\"Hardening: disabled\");\n }\n\n try {\n // Step 1: Install system dependencies\n installSystemDeps();\n\n // Step 2: Create ddev user\n createDdevUser();\n\n // Step 3: Install Docker\n if (!options.noDocker) {\n installDocker();\n configureDocker();\n setupDockerPrune();\n } else {\n step(\"Install Docker\");\n info(\"Skipped (--no-docker)\");\n }\n\n // Step 4: Install DDEV\n if (!options.noDdev) {\n installDdev();\n configureDdev(options.tld, options.email);\n configureTraefik();\n } else {\n step(\"Install DDEV\");\n info(\"Skipped (--no-ddev)\");\n }\n\n // Step 5: Install Node.js\n installNode();\n\n // Step 6: Install and configure agent\n installAgent();\n createAgentConfig(options.tld, options.email);\n createSystemdService();\n\n // Step 7: Server hardening\n if (!options.noHardening) {\n const sshUsers = options.sshUsers ?? [\"ddev\"];\n hardenServer(sshUsers);\n } else {\n step(\"Server hardening\");\n info(\"Skipped (--no-hardening)\");\n }\n\n // Mark all migrations as applied — fresh servers are already up-to-date\n markAllMigrationsApplied();\n\n // Done\n console.log(\"\\n\\x1b[32m✓ Setup complete!\\x1b[0m\\n\");\n console.log(\"Next steps:\");\n console.log(` 1. Edit /etc/trafic/config.toml to configure authentication`);\n console.log(` 2. Add SSH public keys to /home/ddev/.ssh/authorized_keys`);\n console.log(` 3. Deploy your first project:`);\n console.log(` npx @studiometa/trafic-cli deploy --host=<this-server> --name=my-app`);\n console.log(`\\nYour projects will be available at https://<name>.${options.tld}`);\n } catch (err) {\n error(`Setup failed: ${err instanceof Error ? err.message : err}`);\n process.exit(1);\n }\n}\n\n/**\n * Run security audit\n */\nexport async function audit(): Promise<void> {\n const checks = runAudit();\n printAuditResults(checks);\n\n const failed = checks.filter((c) => c.status === \"fail\").length;\n if (failed > 0) {\n process.exit(1);\n }\n}\n"],"mappings":";;;;AAIA,IAAM,uBAAuB;CAC3B;CACA;CACA;CACD;AAED,IAAM,iBAA8B;CAClC,KAAK;CACL,MAAM;CACN,QAAQ;CACR,iBAAiB;CACjB,aAAa;CACb,aAAa;CACb,mBAAmB;CACnB,MAAM;EACJ,eAAe;EACf,YAAY,EAAE;EACd,QAAQ,EAAE;EACV,WAAW,EAAE;EACb,OAAO,EAAE;EACV;CACF;;;;AAKD,SAAgB,cAAc,UAA0B;CAEtD,MAAM,QAAQ,SAAS,MADT,oCACqB;AACnC,KAAI,CAAC,MAAO,QAAO;CAEnB,MAAM,QAAQ,SAAS,MAAM,MAAM,KAAK,GAAG;CAC3C,MAAM,UAAU,SAAS,MAAM,MAAM,KAAK,GAAG;CAC7C,MAAM,UAAU,SAAS,MAAM,MAAM,KAAK,GAAG;AAE7C,SAAQ,QAAQ,OAAO,UAAU,KAAK,WAAW;;;;;AAMnD,SAAgB,WAAW,YAAkC;CAE3D,IAAI,OAAO;AACX,KAAI,CAAC,KACH,QAAO,qBAAqB,MAAM,MAAM,WAAW,EAAE,CAAC;AAGxD,KAAI,CAAC,QAAQ,CAAC,WAAW,KAAK,EAAE;AAC9B,UAAQ,KAAK,uCAAuC;AACpD,SAAO;;CAIT,MAAM,SAAS,MADC,aAAa,MAAM,QAAQ,CACd;CAG7B,MAAM,OAAO,gBAAgB,OAAO,KAAgC;AAEpE,QAAO;EACL,KAAM,OAAO,OAAkB,eAAe;EAC9C,MAAO,OAAO,QAAmB,eAAe;EAChD,QAAS,OAAO,WAAsB,eAAe;EACrD,iBACG,OAAO,qBAAgC,eAAe;EACzD,aAAc,OAAO,gBAA2B,eAAe;EAC/D,aACG,OAAO,gBAA2B,eAAe;EACpD,mBACG,OAAO,uBACR,eAAe;EACjB;EACD;;AAGH,SAAS,gBAAgB,KAA2C;AAClE,KAAI,CAAC,IAAK,QAAO,eAAe;CAEhC,MAAM,QAAS,IAAI,SAA4C,EAAE;AAEjE,QAAO;EACL,eACG,IAAI,kBACL,eAAe,KAAK;EACtB,YACG,IAAI,eAA4B,eAAe,KAAK;EACvD,QAAS,IAAI,UAAuB,eAAe,KAAK;EACxD,WAAY,IAAI,cAA2B,eAAe,KAAK;EAC/D,OAAO,MAAM,KACV,OAAiB;GAChB,OAAO,EAAE;GACT,QAAQ,EAAE;GACV,QAAQ,EAAE;GACV,YAAY,EAAE;GACf,EACF;EACF;;;;;AAMH,SAAgB,eAAe,QAA+B;CAC5D,MAAM,SAAmB,EAAE;AAE3B,KAAI,CAAC,OAAO,IACV,QAAO,KAAK,kBAAkB;AAGhC,KAAI,OAAO,OAAO,KAAK,OAAO,OAAO,MACnC,QAAO,KAAK,mCAAmC;AAGjD,KACE,OAAO,KAAK,kBAAkB,WAC9B,OAAO,KAAK,kBAAkB,UAC9B,OAAO,KAAK,kBAAkB,WAC9B,OAAO,KAAK,kBAAkB,QAE9B,QAAO,KAAK,2EAAmE;AAGjF,QAAO;;;;;;ACpHT,SAAgB,iBAAiB,QAA2B;CAC1D,MAAM,oBAAoB,cAAc,OAAO,YAAY;CAG3D,MAAM,WAAW,gBAAgB,OAAO,gBAAgB;CAGxD,MAAM,eAAe,gBAAgB,kBAAkB;AAEvD,MAAK,MAAM,WAAW,cAAc;EAClC,MAAM,aAAa,SAAS,IAAI,QAAQ,KAAK;AAG7C,MAAI,YAAY;GACd,MAAM,gBAAgB,kBAAkB,WAAW;AAGnD,OAAI,gBAAgB,cAAc,CAChC;GAIF,MAAM,qBAAqB,iBAAiB,cAAc;AAC1D,OAAI,uBAAuB,KAAA;QACJ,KAAK,KAAK,GAAG,QAAQ,aACvB,mBAEjB;;;AAON,MADa,eAAe,QAAQ,KAAK,EAC/B,WAAW,WAAW;AAE9B,oBAAiB,QAAQ,MAAM,UAAU;AACzC;;AAGF,UAAQ,IAAI,0BAA0B,QAAQ,OAAO;AAGrD,MAFgB,YAAY,QAAQ,KAAK,EAE5B;AACX,oBAAiB,QAAQ,MAAM,UAAU;AACzC,WAAQ,IAAI,YAAY,QAAQ,OAAO;QAEvC,SAAQ,MAAM,mBAAmB,QAAQ,OAAO;;;;;;AAQtD,SAAgB,mBAAmB,QAAqC;CACtE,MAAM,aAAa,cAAc,OAAO,kBAAkB;AAE1D,SAAQ,IACN,0CAA0C,OAAO,kBAAkB,YAAY,OAAO,cACvF;AAGD,kBAAiB,OAAO;AAGxB,QAAO,kBAAkB;AACvB,mBAAiB,OAAO;IACvB,WAAW;;AC3EhB,IAAI,aAAa;AACjB,IAAI,SAAS;;;;AAKb,SAAgB,UAAU,OAAsB;AAC9C,UAAS;;;;;AAMX,SAAgB,aAAmB;AACjC,cAAa;;;;;AAMf,SAAgB,KAAK,SAAuB;AAC1C;AACA,SAAQ,IAAI,cAAc,WAAW,WAAW,UAAU;;;;;AAM5D,SAAgB,QAAQ,SAAuB;AAC7C,SAAQ,IAAI,wBAAwB,UAAU;;;;;AAMhD,SAAgB,KAAK,SAAuB;AAC1C,SAAQ,IAAI,wBAAwB,UAAU;;;;;AAMhD,SAAgB,MAAM,SAAuB;AAC3C,SAAQ,IAAI,wBAAwB,UAAU;;;;;AAMhD,SAAgB,KAAK,SAAuB;AAC1C,SAAQ,IAAI,OAAO,UAAU;;;;;AAM/B,SAAgB,KAAK,SAAiB,SAAwC;AAC5E,KAAI,QAAQ;AACV,MAAI,CAAC,SAAS,OACZ,SAAQ,IAAI,iBAAiB,QAAQ,SAAS;AAEhD,SAAO;;AAGT,KAAI;AACF,SAAO,SAAS,SAAS;GACvB,UAAU;GACV,OAAO,SAAS,SAAS,SAAS;GACnC,CAAC;UACK,KAAK;EACZ,MAAM,IAAI;AACV,QAAM,IAAI,MAAM,mBAAmB,QAAQ,IAAI,EAAE,UAAU,KAAK;;;;;;AAOpE,SAAgB,aAAW,SAAyB;AAClD,KAAI,OACF,QAAO;AAGT,KAAI;AACF,SAAO,SAAS,SAAS;GACvB,UAAU;GACV,OAAO;GACR,CAAC,CAAC,MAAM;SACH;AACN,SAAO;;;;;;AAOX,SAAgB,cAAc,SAA0B;AACtD,KAAI;AACF,WAAS,SAAS,WAAW,EAAE,OAAO,QAAQ,CAAC;AAC/C,SAAO;SACD;AACN,SAAO;;;;;;AAOX,SAAgB,SAAkB;AAChC,QAAO,QAAQ,UAAU,KAAK;;;;;AC3GhC,SAAgB,iBAAuB;AACrC,MAAK,mBAAmB;AAIxB,KADmB,aAAW,yBAAyB,CAErD,MAAK,6BAA6B;MAC7B;AACL,OAAK,+BAA+B;AACpC,UAAQ,sBAAsB;;AAIhC,MAAK,+CAA+C,EAAE,QAAQ,MAAM,CAAC;AAGrE,MAAK,0BAA0B;AAC/B,MAAK,iCAAiC;AACtC,MAAK,2BAA2B;AAChC,SAAQ,yBAAyB;AAGjC,MAAK,2BAA2B;AAChC,MAAK,4BAA4B;AACjC,MAAK,kCAAkC;AAYvC,SAAQ,wDAAwD;;;;;ACnClE,SAAgB,gBAAsB;AACpC,MAAK,iBAAiB;AAEtB,KAAI,cAAc,SAAS,EAAE;AAE3B,OAAK,6BADW,KAAK,oBAAoB,EAAE,QAAQ,MAAM,CAAC,EACf,MAAM,IAAI,oBAAoB;AACzE;;AAIF,MAAK,mDAAmD;AACxD,MAAK,0DAA0D;AAC/D,MAAK,yBAAyB,EAAE,QAAQ,MAAM,CAAC;AAC/C,MAAK,wBAAwB;AAG7B,MAAK,2BAA2B,EAAE,QAAQ,MAAM,CAAC;AACjD,MAAK,0BAA0B,EAAE,QAAQ,MAAM,CAAC;AAEhD,SAAQ,+BAA+B;AAGvC,MAAK,2BAA2B,EAAE,QAAQ,MAAM,CAAC;AACjD,SAAQ,kCAAkC;;;;;AAM5C,SAAgB,kBAAwB;AACtC,MAAK,mBAAmB;CAGxB,MAAM,eAAe;EACnB,cAAc;EACd,YAAY;GACV,YAAY;GACZ,YAAY;GACb;EACD,kBAAkB;EAClB,gBAAgB;EACjB;AAED,MAAK,uBAAuB;CAE5B,MAAM,aAAa;AAGnB,KAAI,WAAW,WAAW,CACxB,KAAI;EAGF,MAAM,SAAS;GAAE,GAFA,KAAK,MAAM,aAAa,YAAY,QAAQ,CAAC;GAEhC,GAAG;GAAc;AAC/C,gBAAc,YAAY,KAAK,UAAU,QAAQ,MAAM,EAAE,CAAC;AAC1D,OAAK,qCAAqC;SACpC;AACN,OAAK,mDAAmD;AACxD;;KAGF,eAAc,YAAY,KAAK,UAAU,cAAc,MAAM,EAAE,CAAC;AAIlE,MAAK,sDAAsD;AAC3D,SAAQ,6CAA6C;;;;;AAMvD,SAAgB,mBAAyB;AACvC,MAAK,4BAA4B;AAMjC,eAAc,mCAJM;;EAIyC;AAC7D,MAAK,4CAA4C;AAEjD,SAAQ,kDAAkD;;;;;ACjF5D,SAAgB,oBAA0B;AACxC,MAAK,8BAA8B;AAEnC,MAAK,sBAAsB,EAAE,QAAQ,MAAM,CAAC;AAC5C,MAAK,sFAAsF,EAAE,QAAQ,MAAM,CAAC;AAE5G,SAAQ,iDAAiD;;;;;;AAO3D,SAAgB,iBAAuB;AACrC,MAAK,wCAAwC,EAAE,QAAQ,MAAM,CAAC;AAC9D,MACE,4GACA,EAAE,QAAQ,MAAM,CACjB;AACD,MAAK,wCAAwC,EAAE,QAAQ,MAAM,CAAC;AAC9D,MACE,yIACA,EAAE,QAAQ,MAAM,CACjB;;;;;AAMH,SAAgB,cAAoB;AAClC,MAAK,eAAe;AAEpB,KAAI,cAAc,OAAO,EAAE;AAEzB,OAAK,2BADW,KAAK,wCAAwC,EAAE,QAAQ,MAAM,CAAC,EACrC,MAAM,IAAI,oBAAoB;AACvE;;AAKF,MAAK,gCAAgC;AACrC,iBAAgB;AAEhB,MAAK,qBAAqB;AAC1B,MAAK,sBAAsB,EAAE,QAAQ,MAAM,CAAC;AAC5C,MAAK,6EAA6E,EAAE,QAAQ,MAAM,CAAC;AAGnG,MAAK,mBAAmB,EAAE,QAAQ,MAAM,CAAC;AAGzC,SAAQ,mBADQ,KAAK,wCAAwC,EAAE,QAAQ,MAAM,CAAC,EAC1C,MAAM,IAAI,oBAAoB;;;;;;;;;;;;;AAcpE,SAAgB,cAAc,KAAa,OAAsB;AAC/D,MAAK,iBAAiB;CAGtB,MAAM,WAAW,QAAgB,iBAAiB,IAAI;AAGtD,MACE,QACE,oCAAoC,IAAI,mEAAmE,QAAQ,SAAS,QAAQ,GAAG,QAAQ,uBAAuB,UAAU,GAAG,iCACpL,EACD,EAAE,QAAQ,MAAM,CACjB;AAED,SAAQ,6BAA6B,MAAM;AAC3C,MAAK,+BAA+B,IAAI,cAAc;AAEtD,KAAI,MACF,SAAQ,qCAAqC,QAAQ;AAGvD,SAAQ,kCAAkC;;;;;AAmD5C,SAAgB,mBAAyB;AACvC,MAAK,qCAAqC;AAG1C,MAAK,qCAAqC,EAAE,QAAQ,MAAM,CAAC;AAC3D,MAAK,uCAAuC,EAAE,QAAQ,MAAM,CAAC;AA2B7D,eAAc,wCAxBO;;;;;;;;;;;;;;;;;;;;;;EAwB8C;AACnE,MAAK,wDAAwD,EAAE,QAAQ,MAAM,CAAC;AAgB9E,eAAc,yCAbO;;;;;;;;;;;EAa+C;AACpE,MAAK,yDAAyD,EAAE,QAAQ,MAAM,CAAC;AAE/E,SAAQ,4CAA4C;;;;;AC7LtD,SAAgB,cAAoB;AAClC,MAAK,kBAAkB;AAGvB,KAAI,cAAc,OAAO,EAAE;AAEzB,OAAK,8BADW,KAAK,kBAAkB,EAAE,QAAQ,MAAM,CAAC,EACZ,MAAM,GAAG;AACrD;;AAIF,MACE,iGACD;AASD,eAAc,yBANS;;;;EAM+B;AACtD,MAAK,kCAAkC;AAGvC,MAAK,qDAAqD;AAC1D,MAAK,qDAAqD;AAG1D,MACE,uEACD;AACD,MAAK,qEAAqE;AAC1E,MAAK,qEAAqE;AAE1E,SAAQ,+BAA+B;;;;;AAMzC,SAAgB,eAAqB;AACnC,MAAK,uBAAuB;AAG5B,MAAK,0CAA0C;AAE/C,SAAQ,yBAAyB;;;;;AAMnC,SAAgB,kBAAkB,KAAa,QAAuB;AACpE,MAAK,6BAA6B;AAGlC,MAAK,uBAAuB;AAC5B,MAAK,2BAA2B;AAChC,MAAK,kCAAkC;CAEvC,MAAM,aAAa;AAEnB,KAAI,WAAW,WAAW,EAAE;AAC1B,OAAK,qEAAqE;AAC1E,OAAK,6CAA6C;AAClD;;AA8BF,eAAc,YA3BC;;;;SAIR,IAAI;;;;;;;;;;;;;;;;;;;;;EAuBsB;AACjC,MAAK,oCAAoC;AACzC,MAAK,0CAA0C;AAE/C,SAAQ,mDAAmD;AAC3D,MAAK,6CAA6C;;;;;AAMpD,SAAgB,uBAA6B;AAC3C,MAAK,yBAAyB;AAuC9B,eAAc,4CAlCS;;;;;;;;;YAFL,KAAK,sBAAsB,EAAE,QAAQ,MAAM,CAAC,EAAE,MAAM,IAAI,wBAWtD;;;;;;;;;;;;;;;;;;;;;;;EAyBqD;AACzE,MAAK,qDAAqD;AAG1D,MAAK,0BAA0B;AAC/B,MAAK,gCAAgC;AACrC,MAAK,+BAA+B;AAEpC,SAAQ,+BAA+B;AACvC,MAAK,2CAA2C;;;;;AC7JlD,SAAgB,UAAU,eAAyB,CAAC,OAAO,EAAQ;AACjE,MAAK,2BAA2B;AAkChC,eAAc,sCAhCI;;;;;;;;;;;;;;;;;;kBAkBF,aAAa,KAAK,IAAI,CAAC;;;;;;;;;;;;EAcuB;AAC9D,MAAK,+CAA+C;AAIpD,KADmB,KAAK,gCAAgC,EAAE,QAAQ,MAAM,CAAC,EACzD,SAAS,QAAQ,EAAE;AACjC,OAAK,uCAAuC;AAC5C,OAAK,wCAAwC;AAC7C;;AAGF,MAAK,gDAAgD;AACrD,SAAQ,qEAAqE;AAC7E,MAAK,wBAAwB,aAAa,KAAK,KAAK,GAAG;;;;;AAMzD,SAAgB,oBAA0B;AACxC,MAAK,2BAA2B;AAGhC,KAAI,CAAC,cAAc,MAAM,CACvB,MAAK,4EAA4E,EAAE,QAAQ,MAAM,CAAC;AAIpG,MAAK,qBAAqB,EAAE,QAAQ,MAAM,CAAC;AAC3C,MAAK,4BAA4B;AACjC,MAAK,6BAA6B;AAClC,MAAK,iCAAiC;AACtC,MAAK,kCAAkC;AACvC,MAAK,oCAAoC;AAGzC,MAAK,qBAAqB;AAE1B,SAAQ,6DAA6D;;;;;AAMvE,SAAgB,8BAAoC;AAClD,MAAK,uCAAuC;AAG5C,MACE,4GACA,EAAE,QAAQ,MAAM,CACjB;AAqBD,eACE,6CAnBqB;;;;;;;;;;;;;;;;EAqBtB;AASD,eAAc,uCANK;;;;EAM6C;AAGhE,MAAK,uCAAuC;AAC5C,MAAK,sCAAsC;AAE3C,SAAQ,qCAAqC;;;;;AAM/C,SAAgB,oBAA0B;AACxC,MAAK,qBAAqB;AAG1B,KAAI,CAAC,cAAc,kBAAkB,CACnC,MAAK,iFAAiF,EAAE,QAAQ,MAAM,CAAC;AAoBzG,eAAc,4BAhBK;;;;;;;;;;;;;;EAgBkC;AACrD,MAAK,qCAAqC;AAG1C,MAAK,4BAA4B;AACjC,MAAK,6BAA6B;AAElC,SAAQ,sDAAsD;;;;;AAMhE,SAAgB,wBAA8B;AAC5C,MAAK,0BAA0B;AAS/B,eAAc,sCAPO;;;;;EAO4C;AACjE,MAAK,+CAA+C;AAEpD,SAAQ,yCAAyC;;;;;AAMnD,SAAgB,2BAAiC;AAC/C,MAAK,6BAA6B;AAGlC,MAAK,uBAAuB;AAC5B,MAAK,2BAA2B;AAChC,MAAK,+BAA+B;AAGpC,MAAK,wBAAwB;AAC7B,MAAK,4BAA4B;AACjC,MAAK,gCAAgC;AACrC,MAAK,8BAA8B;AACnC,MAAK,kCAAkC;AACvC,MAAK,sCAAsC;AAG3C,KAAI,WAAW,0BAA0B,EAAE;AACzC,OAAK,oCAAoC;AACzC,OAAK,0CAA0C;;AAGjD,SAAQ,8BAA8B;;;;;AAMxC,SAAgB,aAAa,WAAqB,CAAC,OAAO,EAAQ;AAChE,WAAU,SAAS;AACnB,oBAAmB;AACnB,8BAA6B;AAC7B,oBAAmB;AACnB,wBAAuB;AACvB,2BAA0B;;;;;AC7N5B,SAAS,WAAW,SAAyB;AAC3C,KAAI;AACF,SAAO,SAAS,SAAS;GAAE,UAAU;GAAS,OAAO;GAAQ,CAAC,CAAC,MAAM;SAC/D;AACN,SAAO;;;;;;AAOX,SAAS,gBAAgB,SAA0B;AAEjD,QADe,WAAW,uBAAuB,UAAU,KACzC;;;;;AAMpB,SAAS,WAAyB;CAChC,MAAM,SAAuB,EAAE;CAG/B,MAAM,aAAa,WAAW,uBAAuB,GACjD,aAAa,wBAAwB,QAAQ,GAC7C;CACJ,MAAM,eAAe,WAAW,qCAAqC,GACjE,aAAa,sCAAsC,QAAQ,GAC3D;CAEJ,MAAM,SAAS,aAAa,OAAO;CAEnC,MAAM,oBACJ,OAAO,SAAS,qBAAqB,IACrC,OAAO,SAAS,oCAAoC;AACtD,QAAO,KAAK;EACV,MAAM;EACN,QAAQ,oBAAoB,SAAS;EACrC,SAAS,oBACL,wBACA;EACJ,KAAK;EACN,CAAC;CAEF,MAAM,uBAAuB,OAAO,SAAS,4BAA4B;AACzE,QAAO,KAAK;EACV,MAAM;EACN,QAAQ,uBAAuB,SAAS;EACxC,SAAS,uBACL,qCACA;EACJ,KAAK;EACN,CAAC;AAEF,QAAO;;;;;AAMT,SAAS,gBAA8B;CACrC,MAAM,SAAuB,EAAE;CAE/B,MAAM,YAAY,WAAW,aAAa;CAC1C,MAAM,WAAW,UAAU,SAAS,iBAAiB;AAErD,QAAO,KAAK;EACV,MAAM;EACN,QAAQ,WAAW,SAAS;EAC5B,SAAS,WAAW,kBAAkB;EACtC,KAAK;EACN,CAAC;AAEF,KAAI,UAAU;EACZ,MAAM,WAAW,UAAU,SAAS,SAAS;EAC7C,MAAM,WAAW,UAAU,SAAS,SAAS;EAC7C,MAAM,YAAY,UAAU,SAAS,UAAU;AAE/C,MAAI,YAAY,YAAY,UAC1B,QAAO,KAAK;GACV,MAAM;GACN,QAAQ;GACR,SAAS;GACV,CAAC;MAEF,QAAO,KAAK;GACV,MAAM;GACN,QAAQ;GACR,SAAS;GACT,KAAK;GACN,CAAC;;AAIN,QAAO;;;;;AAMT,SAAS,gBAA8B;CACrC,MAAM,SAAuB,EAAE;CAG/B,MAAM,iBAAiB,gBAAgB,WAAW;AAClD,QAAO,KAAK;EACV,MAAM;EACN,QAAQ,iBAAiB,SAAS;EAClC,SAAS,iBAAiB,uBAAuB;EACjD,KAAK;EACN,CAAC;CAGF,MAAM,mBAAmB,gBAAgB,sBAAsB;AAC/D,QAAO,KAAK;EACV,MAAM;EACN,QAAQ,mBAAmB,SAAS;EACpC,SAAS,mBACL,gCACA;EACJ,KAAK;EACN,CAAC;CAGF,MAAM,cAAc,gBAAgB,eAAe;AACnD,QAAO,KAAK;EACV,MAAM;EACN,QAAQ,cAAc,SAAS;EAC/B,SAAS,cACL,4BACA;EACJ,KAAK;EACN,CAAC;AAEF,QAAO;;;;;AAMT,SAAS,cAA4B;CACnC,MAAM,SAAuB,EAAE;CAG/B,MAAM,eAAe,gBAAgB,SAAS;AAC9C,QAAO,KAAK;EACV,MAAM;EACN,QAAQ,eAAe,SAAS;EAChC,SAAS,eAAe,sBAAsB;EAC9C,KAAK;EACN,CAAC;CAGF,MAAM,iBAAiB,WACrB,4CACD;CACD,MAAM,QAAQ,SAAS,gBAAgB,GAAG,IAAI;AAC9C,KAAI,QAAQ,EACV,QAAO,KAAK;EACV,MAAM;EACN,QAAQ;EACR,SAAS,GAAG,MAAM;EAClB,KAAK;EACN,CAAC;CAIJ,MAAM,YAAY,WAAW,kCAAkC;CAC/D,MAAM,eAAe,SAAS,WAAW,GAAG,IAAI;AAChD,QAAO,KAAK;EACV,MAAM;EACN,QAAQ,eAAe,KAAK,SAAS,eAAe,KAAK,SAAS;EAClE,SAAS,eAAe;EACxB,KAAK,eAAe,KAAK,0DAA0D,KAAA;EACpF,CAAC;AAEF,QAAO;;;;;AAMT,SAAS,mBAAiC;CACxC,MAAM,SAAuB,EAAE;AAG/B,KAAI,WAAW,0BAA0B,EAAE;EACzC,MAAM,QAAQ,WAAW,uCAAuC;EAChE,MAAM,WAAW,UAAU,SAAS,UAAU;AAC9C,SAAO,KAAK;GACV,MAAM;GACN,QAAQ,WAAW,SAAS;GAC5B,SAAS,WACL,uCACA,4BAA4B,MAAM;GACtC,KAAK;GACN,CAAC;;AAIJ,KAAI,WAAW,iBAAiB,EAAE;EAChC,MAAM,QAAQ,WAAW,iCAAiC;EAC1D,MAAM,YAAY,UAAU;AAC5B,SAAO,KAAK;GACV,MAAM;GACN,QAAQ,YAAY,SAAS;GAC7B,SAAS,YACL,qCACA,6BAA6B;GACjC,KAAK;GACN,CAAC;;AAGJ,QAAO;;;;;AAMT,SAAgB,WAAyB;AACvC,QAAO;EACL,GAAG,UAAU;EACb,GAAG,eAAe;EAClB,GAAG,eAAe;EAClB,GAAG,aAAa;EAChB,GAAG,kBAAkB;EACtB;;;;;AAMH,SAAgB,kBAAkB,QAA4B;AAC5D,SAAQ,IAAI,2CAA2C;AAEvD,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,OACJ,MAAM,WAAW,SACb,qBACA,MAAM,WAAW,SACf,qBACA;AAER,UAAQ,IAAI,GAAG,KAAK,UAAU,MAAM,KAAK,WAAW,MAAM,UAAU;AAEpE,MAAI,MAAM,OAAO,MAAM,WAAW,OAChC,SAAQ,IAAI,aAAa,MAAM,IAAI,SAAS;;CAIhD,MAAM,SAAS,OAAO,QAAQ,MAAM,EAAE,WAAW,OAAO,CAAC;CACzD,MAAM,WAAW,OAAO,QAAQ,MAAM,EAAE,WAAW,OAAO,CAAC;CAC3D,MAAM,SAAS,OAAO,QAAQ,MAAM,EAAE,WAAW,OAAO,CAAC;AAEzD,SAAQ,IACN,4BAA4B,OAAO,WAAW,SAAS,aAAa,OAAO,SAC5E;;;;;;;;;;;;ACxPH,MAAa,2BAAsC;CACjD,IAAI;CACJ,aAAa;CAEb,MAAY;AAEV,MAAI,WAAW,oCAAoC,CACjD;AAIF,OAAK,gFAAgF,EACnF,QAAQ,MACT,CAAC;AAGF,kBAAgB;AAGhB,OAAK,sBAAsB,EAAE,QAAQ,MAAM,CAAC;AAC5C,OAAK,6EAA6E,EAChF,QAAQ,MACT,CAAC;AAGF,OAAK,mBAAmB,EAAE,QAAQ,MAAM,CAAC;;CAE5C;;ACrCD,MAAa,wBAAwB;;AAGrC,MAAa,iBAA8B,CAAC,yBAAyB;;;;;AAUrE,SAAgB,qBAAqC;AACnD,KAAI,CAAC,WAAA,+BAAiC,CACpC,QAAO,EAAE,SAAS,EAAE,EAAE;AAGxB,KAAI;EACF,MAAM,MAAM,aAAa,uBAAuB,QAAQ;EACxD,MAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,MACE,WAAW,QACX,OAAO,WAAW,YAClB,aAAa,UACb,MAAM,QAAS,OAAgC,QAAQ,CAEvD,QAAO;SAEH;AAIR,QAAO,EAAE,SAAS,EAAE,EAAE;;;;;;AAOxB,SAAgB,mBAAmB,OAA6B;AAC9D,WAAU,eAAe,EAAE,WAAW,MAAM,CAAC;AAC7C,eAAc,uBAAuB,KAAK,UAAU,OAAO,MAAM,EAAE,GAAG,MAAM,QAAQ;;;;;AAUtF,SAAgB,iBAAuC;CACrD,MAAM,QAAQ,oBAAoB;AAClC,QAAO,eAAe,KAAK,eAAe;EACxC;EACA,QAAQ,MAAM,QAAQ,SAAS,UAAU,GAAG,GAAG,YAAY;EAC5D,EAAE;;;;;;;;AASL,SAAgB,qBAAqB,SAAS,OAAa;CACzD,MAAM,QAAQ,oBAAoB;CAClC,MAAM,UAAU,eAAe,QAAQ,MAAM,CAAC,MAAM,QAAQ,SAAS,EAAE,GAAG,CAAC;AAE3E,KAAI,QAAQ,WAAW,GAAG;AACxB,UAAQ,IAAI,2BAA2B;AACvC;;AAGF,MAAK,MAAM,aAAa,SAAS;AAC/B,UAAQ,IAAI,gBAAgB,UAAU,GAAG,WAAW,UAAU,cAAc;AAE5E,MAAI,QAAQ;AACV,WAAQ,IAAI,wCAAwC;AACpD;;AAGF,YAAU,KAAK;AAGf,QAAM,QAAQ,KAAK,UAAU,GAAG;AAChC,qBAAmB,MAAM;AAEzB,UAAQ,IAAI,+BAA+B;;;;;;;;;;AAW/C,SAAgB,2BAAiC;CAC/C,MAAM,QAAQ,oBAAoB;AAElC,MAAK,MAAM,aAAa,eACtB,KAAI,CAAC,MAAM,QAAQ,SAAS,UAAU,GAAG,CACvC,OAAM,QAAQ,KAAK,UAAU,GAAG;AAIpC,oBAAmB,MAAM;;;;;AClG3B,eAAsB,MAAM,SAAsC;AAChE,aAAY;AACZ,WAAU,QAAQ,UAAU,MAAM;AAElC,SAAQ,IAAI,2CAA2C;AAEvD,KAAI,QAAQ,OACV,SAAQ,IAAI,2DAA2D;AAIzE,KAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,QAAQ;AAChC,QAAM,mCAAmC;AACzC,UAAQ,IAAI,qCAAqC;AACjD,UAAQ,KAAK,EAAE;;AAGjB,MAAK,QAAQ,QAAQ,MAAM;AAC3B,KAAI,QAAQ,MACV,MAAK,wBAAwB,QAAQ,QAAQ;AAE/C,KAAI,QAAQ,YACV,MAAK,sBAAsB;AAG7B,KAAI;AAEF,qBAAmB;AAGnB,kBAAgB;AAGhB,MAAI,CAAC,QAAQ,UAAU;AACrB,kBAAe;AACf,oBAAiB;AACjB,qBAAkB;SACb;AACL,QAAK,iBAAiB;AACtB,QAAK,wBAAwB;;AAI/B,MAAI,CAAC,QAAQ,QAAQ;AACnB,gBAAa;AACb,iBAAc,QAAQ,KAAK,QAAQ,MAAM;AACzC,qBAAkB;SACb;AACL,QAAK,eAAe;AACpB,QAAK,sBAAsB;;AAI7B,eAAa;AAGb,gBAAc;AACd,oBAAkB,QAAQ,KAAK,QAAQ,MAAM;AAC7C,wBAAsB;AAGtB,MAAI,CAAC,QAAQ,YAEX,cADiB,QAAQ,YAAY,CAAC,OAAO,CACvB;OACjB;AACL,QAAK,mBAAmB;AACxB,QAAK,2BAA2B;;AAIlC,4BAA0B;AAG1B,UAAQ,IAAI,uCAAuC;AACnD,UAAQ,IAAI,cAAc;AAC1B,UAAQ,IAAI,gEAAgE;AAC5E,UAAQ,IAAI,8DAA8D;AAC1E,UAAQ,IAAI,kCAAkC;AAC9C,UAAQ,IAAI,4EAA4E;AACxF,UAAQ,IAAI,uDAAuD,QAAQ,MAAM;UAC1E,KAAK;AACZ,QAAM,iBAAiB,eAAe,QAAQ,IAAI,UAAU,MAAM;AAClE,UAAQ,KAAK,EAAE;;;;;;AAOnB,eAAsB,QAAuB;CAC3C,MAAM,SAAS,UAAU;AACzB,mBAAkB,OAAO;AAGzB,KADe,OAAO,QAAQ,MAAM,EAAE,WAAW,OAAO,CAAC,SAC5C,EACX,SAAQ,KAAK,EAAE"}
|
package/dist/utils/db.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
2
|
import type { ProjectRecord, AccessLog } from "../types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Initialize the database connection and schema
|
|
5
5
|
*/
|
|
6
|
-
export declare function initDb(dbPath: string):
|
|
6
|
+
export declare function initDb(dbPath: string): DatabaseSync;
|
|
7
7
|
/**
|
|
8
8
|
* Get the database instance
|
|
9
9
|
*/
|
|
10
|
-
export declare function getDb():
|
|
10
|
+
export declare function getDb(): DatabaseSync;
|
|
11
11
|
/**
|
|
12
12
|
* Close the database connection
|
|
13
13
|
*/
|
package/dist/utils/db.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/utils/db.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/utils/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAI5D;;GAEG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CA0BnD;AAED;;GAEG;AACH,wBAAgB,KAAK,IAAI,YAAY,CAKpC;AAED;;GAEG;AACH,wBAAgB,OAAO,IAAI,IAAI,CAK9B;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAQtD;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAMlE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,CAAC,QAAQ,CAAC,GAC9B,IAAI,CAQN;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,EAAE,CASpE;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG,IAAI,CAO1D;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,SAAM,GAAG,SAAS,EAAE,CAUvE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMjD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@studiometa/trafic-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Server agent for DDEV preview environments — auth, scale-to-zero, auto-start",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -44,12 +44,8 @@
|
|
|
44
44
|
"typecheck": "tsgo --noEmit"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"better-sqlite3": "^11.10.0",
|
|
48
47
|
"smol-toml": "^1.6.0"
|
|
49
48
|
},
|
|
50
|
-
"devDependencies": {
|
|
51
|
-
"@types/better-sqlite3": "^7.6.13"
|
|
52
|
-
},
|
|
53
49
|
"engines": {
|
|
54
50
|
"node": ">=24.0.0"
|
|
55
51
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"server-D0N_6y4c.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 Database from \"better-sqlite3\";\nimport type { ProjectRecord, AccessLog } from \"../types.js\";\n\nlet db: Database.Database | null = null;\n\n/**\n * Initialize the database connection and schema\n */\nexport function initDb(dbPath: string): Database.Database {\n db = new Database(dbPath);\n db.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(): Database.Database {\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 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 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 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 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 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"setup-BfiuQiwa.js","names":[],"sources":["../src/utils/config.ts","../src/tasks/stop-idle.ts","../src/setup/steps.ts","../src/setup/user.ts","../src/setup/docker.ts","../src/setup/ddev.ts","../src/setup/agent.ts","../src/setup/hardening.ts","../src/setup/audit.ts","../src/setup/index.ts"],"sourcesContent":["import { readFileSync, existsSync } from \"node:fs\";\nimport { parse } from \"smol-toml\";\nimport type { AgentConfig, AuthConfig, AuthRule } from \"../types.js\";\n\nconst DEFAULT_CONFIG_PATHS = [\n \"/etc/trafic/config.toml\",\n \"./config.toml\",\n \"./trafic.toml\",\n];\n\nconst DEFAULT_CONFIG: AgentConfig = {\n tld: \"\",\n port: 9876,\n dbPath: \"/var/lib/trafic/db.sqlite\",\n projectListPath: \"/home/ddev/.ddev/project_list.yaml\",\n projectsDir: \"/home/ddev/www\",\n idleTimeout: \"30m\",\n idleCheckInterval: \"5m\",\n auth: {\n defaultPolicy: \"basic\",\n allowedIps: [],\n tokens: [],\n basicAuth: [],\n rules: [],\n },\n};\n\n/**\n * Parse a duration string (e.g., \"30m\", \"1h\", \"2h30m\") to milliseconds\n */\nexport function parseDuration(duration: string): number {\n const regex = /(?:(\\d+)h)?(?:(\\d+)m)?(?:(\\d+)s)?/;\n const match = duration.match(regex);\n if (!match) return 0;\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 return (hours * 3600 + minutes * 60 + seconds) * 1000;\n}\n\n/**\n * Load configuration from TOML file\n */\nexport function loadConfig(configPath?: string): AgentConfig {\n // Find config file\n let path = configPath;\n if (!path) {\n path = DEFAULT_CONFIG_PATHS.find((p) => existsSync(p));\n }\n\n if (!path || !existsSync(path)) {\n console.warn(\"No config file found, using defaults\");\n return DEFAULT_CONFIG;\n }\n\n const content = readFileSync(path, \"utf-8\");\n const parsed = parse(content) as Record<string, unknown>;\n\n // Merge with defaults\n const auth = mergeAuthConfig(parsed.auth as Record<string, unknown>);\n\n return {\n tld: (parsed.tld as string) ?? DEFAULT_CONFIG.tld,\n port: (parsed.port as number) ?? DEFAULT_CONFIG.port,\n dbPath: (parsed.db_path as string) ?? DEFAULT_CONFIG.dbPath,\n projectListPath:\n (parsed.project_list_path as string) ?? DEFAULT_CONFIG.projectListPath,\n projectsDir: (parsed.projects_dir as string) ?? DEFAULT_CONFIG.projectsDir,\n idleTimeout:\n (parsed.idle_timeout as string) ?? DEFAULT_CONFIG.idleTimeout,\n idleCheckInterval:\n (parsed.idle_check_interval as string) ??\n DEFAULT_CONFIG.idleCheckInterval,\n auth,\n };\n}\n\nfunction mergeAuthConfig(raw?: Record<string, unknown>): AuthConfig {\n if (!raw) return DEFAULT_CONFIG.auth;\n\n const rules = (raw.rules as Array<Record<string, unknown>>) ?? [];\n\n return {\n defaultPolicy:\n (raw.default_policy as AuthConfig[\"defaultPolicy\"]) ??\n DEFAULT_CONFIG.auth.defaultPolicy,\n allowedIps:\n (raw.allowed_ips as string[]) ?? DEFAULT_CONFIG.auth.allowedIps,\n tokens: (raw.tokens as string[]) ?? DEFAULT_CONFIG.auth.tokens,\n basicAuth: (raw.basic_auth as string[]) ?? DEFAULT_CONFIG.auth.basicAuth,\n rules: rules.map(\n (r): AuthRule => ({\n match: r.match as string,\n policy: r.policy as AuthRule[\"policy\"],\n tokens: r.tokens as string[] | undefined,\n allowedIps: r.allowed_ips as string[] | undefined,\n }),\n ),\n };\n}\n\n/**\n * Validate configuration\n */\nexport function validateConfig(config: AgentConfig): string[] {\n const errors: string[] = [];\n\n if (!config.tld) {\n errors.push(\"tld is required\");\n }\n\n if (config.port < 1 || config.port > 65535) {\n errors.push(\"port must be between 1 and 65535\");\n }\n\n if (\n config.auth.defaultPolicy !== \"allow\" &&\n config.auth.defaultPolicy !== \"deny\" &&\n config.auth.defaultPolicy !== \"basic\" &&\n config.auth.defaultPolicy !== \"token\"\n ) {\n errors.push('auth.default_policy must be \"allow\", \"deny\", \"basic\", or \"token\"');\n }\n\n return errors;\n}\n","import { getIdleProjects, setProjectStatus } from \"../utils/db.js\";\nimport { stopProject, getProjectInfo, loadProjectList } from \"../utils/ddev.js\";\nimport { parseDuration } from \"../utils/config.js\";\nimport { loadProjectConfig, shouldNeverStop, getIdleTimeoutMs } from \"../utils/project-config.js\";\nimport type { AgentConfig } from \"../types.js\";\n\n/**\n * Stop idle projects that haven't been accessed recently\n * Respects per-project idle_timeout settings from .ddev/config.trafic.yaml\n */\nexport function stopIdleProjects(config: AgentConfig): void {\n const globalThresholdMs = parseDuration(config.idleTimeout);\n\n // Get all projects to check their individual configs\n const projects = loadProjectList(config.projectListPath);\n\n // Get projects idle according to global threshold\n const idleProjects = getIdleProjects(globalThresholdMs);\n\n for (const project of idleProjects) {\n const projectDir = projects.get(project.name);\n\n // Load per-project config if available\n if (projectDir) {\n const projectConfig = loadProjectConfig(projectDir);\n\n // Skip if project is configured to never stop\n if (shouldNeverStop(projectConfig)) {\n continue;\n }\n\n // Check project-specific idle timeout\n const projectThresholdMs = getIdleTimeoutMs(projectConfig);\n if (projectThresholdMs !== undefined) {\n const idleDuration = Date.now() - project.lastAccess;\n if (idleDuration < projectThresholdMs) {\n // Not idle according to project-specific timeout\n continue;\n }\n }\n }\n\n // Double-check project is actually running via DDEV\n const info = getProjectInfo(project.name);\n if (info?.status !== \"running\") {\n // Already stopped, just update our record\n setProjectStatus(project.name, \"stopped\");\n continue;\n }\n\n console.log(`Stopping idle project: ${project.name}`);\n const success = stopProject(project.name);\n\n if (success) {\n setProjectStatus(project.name, \"stopped\");\n console.log(`Stopped: ${project.name}`);\n } else {\n console.error(`Failed to stop: ${project.name}`);\n }\n }\n}\n\n/**\n * Start the idle check scheduler\n */\nexport function startIdleScheduler(config: AgentConfig): NodeJS.Timeout {\n const intervalMs = parseDuration(config.idleCheckInterval);\n\n console.log(\n `Idle scheduler started: checking every ${config.idleCheckInterval}, timeout ${config.idleTimeout}`,\n );\n\n // Run immediately on start\n stopIdleProjects(config);\n\n // Then run on interval\n return setInterval(() => {\n stopIdleProjects(config);\n }, intervalMs);\n}\n","import { execSync } from \"node:child_process\";\nimport { writeFileSync as fsWriteFile } from \"node:fs\";\n\nlet stepNumber = 0;\nlet dryRun = false;\n\n/**\n * Set dry-run mode\n */\nexport function setDryRun(value: boolean): void {\n dryRun = value;\n}\n\n/**\n * Reset step counter\n */\nexport function resetSteps(): void {\n stepNumber = 0;\n}\n\n/**\n * Log a step\n */\nexport function step(message: string): void {\n stepNumber++;\n console.log(`\\n\\x1b[36m[${stepNumber}]\\x1b[0m ${message}`);\n}\n\n/**\n * Log success\n */\nexport function success(message: string): void {\n console.log(` \\x1b[32m✓\\x1b[0m ${message}`);\n}\n\n/**\n * Log warning\n */\nexport function warn(message: string): void {\n console.log(` \\x1b[33m⚠\\x1b[0m ${message}`);\n}\n\n/**\n * Log error\n */\nexport function error(message: string): void {\n console.log(` \\x1b[31m✗\\x1b[0m ${message}`);\n}\n\n/**\n * Log info\n */\nexport function info(message: string): void {\n console.log(` ${message}`);\n}\n\n/**\n * Execute a command (or show in dry-run mode)\n */\nexport function exec(command: string, options?: { silent?: boolean }): string {\n if (dryRun) {\n if (!options?.silent) {\n console.log(` \\x1b[90m$ ${command}\\x1b[0m`);\n }\n return \"\";\n }\n\n try {\n return execSync(command, {\n encoding: \"utf-8\",\n stdio: options?.silent ? \"pipe\" : \"inherit\",\n });\n } catch (err) {\n const e = err as { status?: number; stderr?: string };\n throw new Error(`Command failed: ${command}\\n${e.stderr ?? \"\"}`);\n }\n}\n\n/**\n * Execute a command and return output (silent)\n */\nexport function execSilent(command: string): string {\n if (dryRun) {\n return \"\";\n }\n\n try {\n return execSync(command, {\n encoding: \"utf-8\",\n stdio: \"pipe\",\n }).trim();\n } catch {\n return \"\";\n }\n}\n\n/**\n * Check if a command exists\n */\nexport function commandExists(command: string): boolean {\n try {\n execSync(`which ${command}`, { stdio: \"pipe\" });\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Check if running as root\n */\nexport function isRoot(): boolean {\n return process.getuid?.() === 0;\n}\n\n/**\n * Write a file (or show in dry-run mode)\n */\nexport function writeFile(path: string, content: string): void {\n if (dryRun) {\n console.log(` \\x1b[90mWrite ${path}:\\x1b[0m`);\n const lines = content.split(\"\\n\").slice(0, 5);\n for (const line of lines) {\n console.log(` \\x1b[90m ${line}\\x1b[0m`);\n }\n if (content.split(\"\\n\").length > 5) {\n console.log(` \\x1b[90m ...\\x1b[0m`);\n }\n return;\n }\n\n fsWriteFile(path, content);\n}\n","import { step, success, info, exec, execSilent, warn } from \"./steps.js\";\n\n/**\n * Create the ddev user with proper permissions\n */\nexport function createDdevUser(): void {\n step(\"Create ddev user\");\n\n // Check if user exists\n const userExists = execSilent(\"id -u ddev 2>/dev/null\");\n if (userExists) {\n info(\"User 'ddev' already exists\");\n } else {\n exec(\"useradd -m -s /bin/bash ddev\");\n success(\"Created user 'ddev'\");\n }\n\n // Add to docker group (will be created by Docker install)\n exec(\"usermod -aG docker ddev 2>/dev/null || true\", { silent: true });\n\n // Create www directory\n exec(\"mkdir -p /home/ddev/www\");\n exec(\"chown ddev:ddev /home/ddev/www\");\n exec(\"chmod 750 /home/ddev/www\");\n success(\"Created /home/ddev/www\");\n\n // Create .ssh directory if needed\n exec(\"mkdir -p /home/ddev/.ssh\");\n exec(\"chmod 700 /home/ddev/.ssh\");\n exec(\"chown ddev:ddev /home/ddev/.ssh\");\n\n // Note: ddev user doesn't need sudo when DNS is properly configured\n // - Docker access: via docker group membership\n // - DDEV: runs as regular user, no sudo needed\n // - Agent: runs as systemd service (root manages it)\n // - Hostname resolution: via DNS (*.tld → server IP), not /etc/hosts\n //\n // If DNS is NOT configured for the custom TLD, DDEV will try to edit\n // /etc/hosts which requires sudo. In that case, you'd need:\n // echo \"ddev ALL=(ALL) NOPASSWD: /usr/local/bin/ddev-hostname\" >> /etc/sudoers.d/ddev\n // But it's better to just configure DNS properly.\n success(\"User ddev configured (no sudo needed with proper DNS)\");\n}\n\n/**\n * Setup authorized_keys for ddev user\n */\nexport function setupAuthorizedKeys(publicKey?: string): void {\n if (!publicKey) {\n warn(\"No SSH public key provided, skipping authorized_keys setup\");\n return;\n }\n\n step(\"Setup SSH authorized_keys\");\n\n const authKeysPath = \"/home/ddev/.ssh/authorized_keys\";\n exec(`touch ${authKeysPath}`);\n exec(`chmod 600 ${authKeysPath}`);\n exec(`chown ddev:ddev ${authKeysPath}`);\n\n // Check if key already exists\n const existing = execSilent(`cat ${authKeysPath}`);\n if (existing.includes(publicKey.trim())) {\n info(\"SSH key already authorized\");\n } else {\n exec(`echo '${publicKey}' >> ${authKeysPath}`);\n success(\"Added SSH public key\");\n }\n}\n","import { writeFileSync, existsSync, readFileSync } from \"node:fs\";\nimport { step, success, info, exec, commandExists, warn } from \"./steps.js\";\n\n/**\n * Install Docker using the official script\n */\nexport function installDocker(): void {\n step(\"Install Docker\");\n\n if (commandExists(\"docker\")) {\n const version = exec(\"docker --version\", { silent: true });\n info(`Docker already installed: ${version?.trim() ?? \"unknown version\"}`);\n return;\n }\n\n // Install Docker using official script\n info(\"Downloading and running Docker install script...\");\n exec(\"curl -fsSL https://get.docker.com -o /tmp/get-docker.sh\");\n exec(\"sh /tmp/get-docker.sh\", { silent: true });\n exec(\"rm /tmp/get-docker.sh\");\n\n // Enable and start Docker\n exec(\"systemctl enable docker\", { silent: true });\n exec(\"systemctl start docker\", { silent: true });\n\n success(\"Docker installed and started\");\n\n // Add ddev user to docker group\n exec(\"usermod -aG docker ddev\", { silent: true });\n success(\"Added ddev user to docker group\");\n}\n\n/**\n * Configure Docker for production use\n */\nexport function configureDocker(): void {\n step(\"Configure Docker\");\n\n // Create daemon.json with production settings\n const daemonConfig = {\n \"log-driver\": \"json-file\",\n \"log-opts\": {\n \"max-size\": \"10m\",\n \"max-file\": \"3\",\n },\n \"storage-driver\": \"overlay2\",\n \"live-restore\": true,\n };\n\n exec(\"mkdir -p /etc/docker\");\n\n const configPath = \"/etc/docker/daemon.json\";\n\n // Check if config already exists\n if (existsSync(configPath)) {\n try {\n const existing = JSON.parse(readFileSync(configPath, \"utf-8\"));\n // Merge with existing config\n const merged = { ...existing, ...daemonConfig };\n writeFileSync(configPath, JSON.stringify(merged, null, 2));\n info(\"Merged with existing Docker config\");\n } catch {\n warn(\"Could not parse existing Docker config, skipping\");\n return;\n }\n } else {\n writeFileSync(configPath, JSON.stringify(daemonConfig, null, 2));\n }\n\n // Reload Docker to apply config\n exec(\"systemctl reload docker || systemctl restart docker\");\n success(\"Docker configured with production settings\");\n}\n\n/**\n * Setup Docker system prune cron job\n */\nexport function setupDockerPrune(): void {\n step(\"Setup Docker cleanup cron\");\n\n const cronContent = `# Trafic: Clean up Docker resources weekly\n0 3 * * 0 root docker system prune -af --volumes 2>&1 | logger -t docker-prune\n`;\n\n writeFileSync(\"/etc/cron.d/trafic-docker-prune\", cronContent);\n exec(\"chmod 644 /etc/cron.d/trafic-docker-prune\");\n\n success(\"Docker prune scheduled weekly at 3am on Sundays\");\n}\n","import { writeFileSync } from \"node:fs\";\nimport { step, success, info, exec, commandExists } from \"./steps.js\";\n\n/**\n * Install system dependencies required by Trafic\n */\nexport function installSystemDeps(): void {\n step(\"Install system dependencies\");\n\n exec(\"apt-get update -qq\", { silent: true });\n exec(\"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y jq curl rsync\", { silent: true });\n\n success(\"System dependencies installed: jq, curl, rsync\");\n}\n\n/**\n * Install DDEV\n */\nexport function installDdev(): void {\n step(\"Install DDEV\");\n\n if (commandExists(\"ddev\")) {\n const version = exec(\"ddev --version 2>/dev/null | head -1\", { silent: true });\n info(`DDEV already installed: ${version?.trim() ?? \"unknown version\"}`);\n return;\n }\n\n // Install DDEV via the official apt repository\n // See https://docs.ddev.com/en/stable/users/install/ddev-installation/#debianubuntu\n info(\"Adding DDEV apt repository...\");\n exec(\"install -m 0755 -d /etc/apt/keyrings\", { silent: true });\n exec(\n \"curl -fsSL https://pkg.ddev.com/apt/gpg.key | gpg --dearmor | tee /etc/apt/keyrings/ddev.gpg > /dev/null\",\n { silent: true },\n );\n exec(\"chmod a+r /etc/apt/keyrings/ddev.gpg\", { silent: true });\n exec(\n \"echo \\\"deb [signed-by=/etc/apt/keyrings/ddev.gpg] https://pkg.ddev.com/apt/ * *\\\" | tee /etc/apt/sources.list.d/ddev.list > /dev/null\",\n { silent: true },\n );\n\n info(\"Installing DDEV...\");\n exec(\"apt-get update -qq\", { silent: true });\n exec(\"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ddev\", { silent: true });\n\n // Initialize mkcert certificate authority\n exec(\"mkcert -install\", { silent: true });\n\n const version = exec(\"ddev --version 2>/dev/null | head -1\", { silent: true });\n success(`DDEV installed: ${version?.trim() ?? \"unknown version\"}`);\n}\n\n/**\n * Configure DDEV global settings for production\n *\n * Note on DNS and /etc/hosts:\n * When using a custom TLD (not ddev.site), DDEV may try to edit /etc/hosts\n * if it can't resolve project hostnames via DNS. To avoid this:\n * 1. Configure DNS so *.{tld} resolves to the server's IP (recommended)\n * 2. Or install dnsmasq for local wildcard DNS resolution\n *\n * With proper DNS, DDEV won't need sudo for hostname management.\n */\nexport function configureDdev(tld: string, email?: string): void {\n step(\"Configure DDEV\");\n\n // Run as ddev user\n const ddevCmd = (cmd: string) => `su - ddev -c '${cmd}'`;\n\n // Configure all global settings in a single call\n exec(\n ddevCmd(\n `ddev config global --project-tld=${tld} --router-http-port=80 --router-https-port=443 --use-letsencrypt=${email ? \"true\" : \"false\"} ${email ? `--letsencrypt-email=${email}` : \"\"} --instrumentation-opt-in=false`,\n ),\n { silent: true },\n );\n\n success(`DDEV configured with TLD: ${tld}`);\n info(`Ensure DNS is configured: *.${tld} → server IP`);\n\n if (email) {\n success(`Let's Encrypt enabled with email: ${email}`);\n }\n\n success(\"DDEV global settings configured\");\n}\n\n/**\n * Install dnsmasq for local DNS resolution (optional)\n * This allows the server itself to resolve *.{tld} hostnames\n * without needing to edit /etc/hosts for each project\n */\nexport function installDnsmasq(tld: string): void {\n step(\"Install dnsmasq for local DNS\");\n\n exec(\"apt-get update && apt-get install -y dnsmasq\", { silent: true });\n\n // Get server's public IP\n const serverIp = exec(\n \"curl -4 -s ifconfig.me || hostname -I | awk '{print $1}'\",\n { silent: true },\n )?.trim() || \"127.0.0.1\";\n\n // Configure dnsmasq to resolve *.{tld} to server IP\n const dnsmasqConfig = `# Trafic: Local DNS for DDEV projects\n# Resolve all *.${tld} to this server\naddress=/${tld}/${serverIp}\n\n# Don't read /etc/resolv.conf\nno-resolv\n\n# Use upstream DNS for everything else\nserver=1.1.1.1\nserver=8.8.8.8\n\n# Listen on localhost only\nlisten-address=127.0.0.1\nbind-interfaces\n`;\n\n writeFileSync(\"/etc/dnsmasq.d/trafic.conf\", dnsmasqConfig);\n\n // Configure system to use local dnsmasq\n exec(\"systemctl restart dnsmasq\");\n exec(\"systemctl enable dnsmasq\");\n\n // Update resolv.conf to use local DNS first\n info(\"Configure /etc/resolv.conf to use 127.0.0.1 as primary DNS\");\n\n success(`dnsmasq configured: *.${tld} → ${serverIp}`);\n}\n\n/**\n * Configure Traefik for forward auth\n */\nexport function configureTraefik(): void {\n step(\"Configure Traefik for forward auth\");\n\n // Create custom Traefik config directory\n exec(\"mkdir -p /home/ddev/.ddev/traefik\", { silent: true });\n exec(\"chown -R ddev:ddev /home/ddev/.ddev\", { silent: true });\n\n // Static configuration for Trafic middleware\n const staticConfig = `# Trafic: Forward auth middleware\nhttp:\n middlewares:\n trafic-auth:\n forwardAuth:\n address: \"http://host.docker.internal:9876/__auth__\"\n authResponseHeaders:\n - \"X-Trafic-Project\"\n\n trafic-errors:\n errors:\n status:\n - \"502\"\n - \"503\"\n service: trafic-service\n query: \"/\"\n\n services:\n trafic-service:\n loadBalancer:\n servers:\n - url: \"http://host.docker.internal:9876\"\n`;\n\n writeFileSync(\"/home/ddev/.ddev/traefik/trafic.yaml\", staticConfig);\n exec(\"chown ddev:ddev /home/ddev/.ddev/traefik/trafic.yaml\", { silent: true });\n\n // Create default router config that applies middleware to all projects\n const routerConfig = `# Trafic: Default router configuration\n# This file makes all DDEV projects use the Trafic auth middleware\nhttp:\n routers:\n trafic-catch-all:\n rule: \"HostRegexp(\\`.+\\`)\"\n priority: 1\n middlewares:\n - trafic-auth\n - trafic-errors\n service: api@internal\n`;\n\n writeFileSync(\"/home/ddev/.ddev/traefik/dynamic.yaml\", routerConfig);\n exec(\"chown ddev:ddev /home/ddev/.ddev/traefik/dynamic.yaml\", { silent: true });\n\n success(\"Traefik configured with Trafic middleware\");\n}\n","import { step, success, info, exec, commandExists } from \"./steps.js\";\nimport { writeFileSync, existsSync } from \"node:fs\";\n\n/**\n * Install Node.js using fnm\n */\nexport function installNode(): void {\n step(\"Install Node.js\");\n\n // Check if Node is already installed\n if (commandExists(\"node\")) {\n const version = exec(\"node --version\", { silent: true });\n info(`Node.js already installed: ${version?.trim()}`);\n return;\n }\n\n // Install fnm (Fast Node Manager)\n exec(\n 'curl -fsSL https://fnm.vercel.app/install | bash -s -- --install-dir \"/opt/fnm\" --skip-shell',\n );\n\n // Add fnm to system path\n const profileContent = `# Trafic: fnm (Node.js version manager)\nexport FNM_DIR=\"/opt/fnm\"\nexport PATH=\"/opt/fnm:$PATH\"\neval \"$(fnm env)\"\n`;\n\n writeFileSync(\"/etc/profile.d/fnm.sh\", profileContent);\n exec(\"chmod 644 /etc/profile.d/fnm.sh\");\n\n // Install Node.js 24\n exec(\"export FNM_DIR=/opt/fnm && /opt/fnm/fnm install 24\");\n exec(\"export FNM_DIR=/opt/fnm && /opt/fnm/fnm default 24\");\n\n // Create symlinks for system-wide access\n exec(\n \"ln -sf /opt/fnm/aliases/default/bin/node /usr/local/bin/node || true\",\n );\n exec(\"ln -sf /opt/fnm/aliases/default/bin/npm /usr/local/bin/npm || true\");\n exec(\"ln -sf /opt/fnm/aliases/default/bin/npx /usr/local/bin/npx || true\");\n\n success(\"Node.js 24 installed via fnm\");\n}\n\n/**\n * Install the Trafic agent\n */\nexport function installAgent(): void {\n step(\"Install Trafic agent\");\n\n // Install the agent globally\n exec(\"npm install -g @studiometa/trafic-agent\");\n\n success(\"Trafic agent installed\");\n}\n\n/**\n * Create the agent configuration\n */\nexport function createAgentConfig(tld: string, _email?: string): void {\n step(\"Create agent configuration\");\n\n // Create directories\n exec(\"mkdir -p /etc/trafic\");\n exec(\"mkdir -p /var/lib/trafic\");\n exec(\"chown ddev:ddev /var/lib/trafic\");\n\n const configPath = \"/etc/trafic/config.toml\";\n\n if (existsSync(configPath)) {\n info(\"Configuration already exists at /etc/trafic/config.toml — skipping\");\n info(\"Edit this file to configure authentication\");\n return;\n }\n\n const config = `# Trafic agent configuration\n# Generated by trafic setup\n\n# Required: TLD for DDEV projects\ntld = \"${tld}\"\n\n# Agent HTTP server port\nport = 9876\n\n# Database and paths\ndb_path = \"/var/lib/trafic/db.sqlite\"\nproject_list_path = \"/home/ddev/.ddev/project_list.yaml\"\nprojects_dir = \"/home/ddev/www\"\n\n# Scale-to-zero settings\nidle_timeout = \"4h\"\nidle_check_interval = \"5m\"\n\n# Authentication\n[auth]\ndefault_policy = \"basic\"\nallowed_ips = []\ntokens = []\nbasic_auth = []\nrules = []\n`;\n\n writeFileSync(configPath, config);\n exec(\"chmod 640 /etc/trafic/config.toml\");\n exec(\"chown root:ddev /etc/trafic/config.toml\");\n\n success(\"Configuration created at /etc/trafic/config.toml\");\n info(\"Edit this file to configure authentication\");\n}\n\n/**\n * Create systemd service for the agent\n */\nexport function createSystemdService(): void {\n step(\"Create systemd service\");\n\n // Find the trafic-agent binary path\n const agentPath = exec(\"which trafic-agent\", { silent: true })?.trim() || \"/usr/bin/trafic-agent\";\n\n const serviceContent = `[Unit]\nDescription=Trafic Agent - DDEV preview environment manager\nAfter=network.target docker.service\nRequires=docker.service\n\n[Service]\nType=simple\nUser=ddev\nGroup=ddev\nExecStart=${agentPath} start --config /etc/trafic/config.toml\nRestart=always\nRestartSec=5\nStandardOutput=journal\nStandardError=journal\nSyslogIdentifier=trafic-agent\n\n# Environment\nEnvironment=NODE_ENV=production\n# Skip ddev-hostname /etc/hosts management — on a server, hostnames resolve\n# to the public IP (not localhost), which would trigger ddev-hostname on every\n# ddev start. DDEV_NONINTERACTIVE=true skips that entirely.\nEnvironment=DDEV_NONINTERACTIVE=true\n\n# Security hardening\nNoNewPrivileges=true\nProtectSystem=strict\nProtectHome=read-only\nReadWritePaths=/var/lib/trafic /home/ddev/.ddev\nPrivateTmp=true\n\n[Install]\nWantedBy=multi-user.target\n`;\n\n writeFileSync(\"/etc/systemd/system/trafic-agent.service\", serviceContent);\n exec(\"chmod 644 /etc/systemd/system/trafic-agent.service\");\n\n // Reload systemd and enable service\n exec(\"systemctl daemon-reload\");\n exec(\"systemctl enable trafic-agent\");\n exec(\"systemctl start trafic-agent\");\n\n success(\"Trafic agent service started\");\n info(\"View logs: journalctl -u trafic-agent -f\");\n}\n","import { step, success, info, exec, warn, commandExists } from \"./steps.js\";\nimport { writeFileSync, existsSync } from \"node:fs\";\n\n/**\n * Configure SSH hardening\n */\nexport function hardenSsh(allowedUsers: string[] = [\"ddev\"]): void {\n step(\"Harden SSH configuration\");\n\n const sshConfig = `# Trafic SSH hardening\n# Generated by trafic setup\n\n# Allow root login via SSH key only (no password) — required for server management\nPermitRootLogin prohibit-password\n\n# Disable password authentication (keys only)\nPasswordAuthentication no\nPubkeyAuthentication yes\n\n# Limit authentication attempts\nMaxAuthTries 3\n\n# Idle timeout (5 minutes)\nClientAliveInterval 300\nClientAliveCountMax 2\n\n# Allowed users (root kept for emergency access)\nAllowUsers root ${allowedUsers.join(\" \")}\n\n# Disable X11 forwarding\nX11Forwarding no\n\n# Disable TCP forwarding\nAllowTcpForwarding no\n\n# Use strong algorithms only\nKexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com\nMACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com\n`;\n\n writeFileSync(\"/etc/ssh/sshd_config.d/trafic.conf\", sshConfig);\n exec(\"chmod 644 /etc/ssh/sshd_config.d/trafic.conf\");\n\n // Test config before restarting\n const testResult = exec(\"sshd -t 2>&1 || echo 'error'\", { silent: true });\n if (testResult?.includes(\"error\")) {\n warn(\"SSH config test failed, reverting...\");\n exec(\"rm /etc/ssh/sshd_config.d/trafic.conf\");\n return;\n }\n\n exec(\"systemctl reload ssh || systemctl reload sshd\");\n success(\"SSH hardened: password auth disabled, root key-only login retained\");\n info(`Allowed users: root, ${allowedUsers.join(\", \")}`);\n}\n\n/**\n * Configure UFW firewall\n */\nexport function configureFirewall(): void {\n step(\"Configure firewall (UFW)\");\n\n // Install UFW if not present\n if (!commandExists(\"ufw\")) {\n exec(\"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ufw\", { silent: true });\n }\n\n // Reset and configure\n exec(\"ufw --force reset\", { silent: true });\n exec(\"ufw default deny incoming\");\n exec(\"ufw default allow outgoing\");\n exec(\"ufw allow 22/tcp comment 'SSH'\");\n exec(\"ufw allow 80/tcp comment 'HTTP'\");\n exec(\"ufw allow 443/tcp comment 'HTTPS'\");\n\n // Enable firewall\n exec(\"ufw --force enable\");\n\n success(\"Firewall enabled: SSH (22), HTTP (80), HTTPS (443) allowed\");\n}\n\n/**\n * Configure automatic security updates\n */\nexport function configureUnattendedUpgrades(): void {\n step(\"Configure automatic security updates\");\n\n // Install unattended-upgrades\n exec(\n \"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y unattended-upgrades apt-listchanges\",\n { silent: true },\n );\n\n // Configure for security updates only\n const upgradesConfig = `// Trafic: Automatic security updates\nUnattended-Upgrade::Allowed-Origins {\n \"\\${distro_id}:\\${distro_codename}\";\n \"\\${distro_id}:\\${distro_codename}-security\";\n \"\\${distro_id}ESMApps:\\${distro_codename}-apps-security\";\n \"\\${distro_id}ESM:\\${distro_codename}-infra-security\";\n};\n\n// Do not automatically reboot\nUnattended-Upgrade::Automatic-Reboot \"false\";\n\n// Remove unused dependencies\nUnattended-Upgrade::Remove-Unused-Dependencies \"true\";\n\n// Email notifications (optional)\n// Unattended-Upgrade::Mail \"root\";\n`;\n\n writeFileSync(\n \"/etc/apt/apt.conf.d/50unattended-upgrades\",\n upgradesConfig,\n );\n\n // Enable automatic updates\n const autoConfig = `APT::Periodic::Update-Package-Lists \"1\";\nAPT::Periodic::Download-Upgradeable-Packages \"1\";\nAPT::Periodic::AutocleanInterval \"7\";\nAPT::Periodic::Unattended-Upgrade \"1\";\n`;\n\n writeFileSync(\"/etc/apt/apt.conf.d/20auto-upgrades\", autoConfig);\n\n // Enable the service\n exec(\"systemctl enable unattended-upgrades\");\n exec(\"systemctl start unattended-upgrades\");\n\n success(\"Automatic security updates enabled\");\n}\n\n/**\n * Configure Fail2ban\n */\nexport function configureFail2ban(): void {\n step(\"Configure Fail2ban\");\n\n // Install fail2ban\n if (!commandExists(\"fail2ban-client\")) {\n exec(\"DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y fail2ban\", { silent: true });\n }\n\n // Configure jail\n const jailConfig = `# Trafic: Fail2ban configuration\n[DEFAULT]\nbantime = 3600\nfindtime = 600\nmaxretry = 5\nbackend = auto\n\n[sshd]\nenabled = true\nport = ssh\nfilter = sshd\nlogpath = /var/log/auth.log\nmaxretry = 3\nbantime = 7200\n`;\n\n writeFileSync(\"/etc/fail2ban/jail.local\", jailConfig);\n exec(\"chmod 644 /etc/fail2ban/jail.local\");\n\n // Restart fail2ban\n exec(\"systemctl enable fail2ban\");\n exec(\"systemctl restart fail2ban\");\n\n success(\"Fail2ban enabled: SSH brute force protection active\");\n}\n\n/**\n * Configure system limits\n */\nexport function configureSystemLimits(): void {\n step(\"Configure system limits\");\n\n const limitsConfig = `# Trafic: System limits for ddev user\nddev soft nofile 65536\nddev hard nofile 65536\nddev soft nproc 4096\nddev hard nproc 4096\n`;\n\n writeFileSync(\"/etc/security/limits.d/trafic.conf\", limitsConfig);\n exec(\"chmod 644 /etc/security/limits.d/trafic.conf\");\n\n success(\"System limits configured for ddev user\");\n}\n\n/**\n * Set proper file permissions\n */\nexport function configureFilePermissions(): void {\n step(\"Configure file permissions\");\n\n // Create directories\n exec(\"mkdir -p /etc/trafic\");\n exec(\"mkdir -p /var/lib/trafic\");\n exec(\"mkdir -p /var/backups/trafic\");\n\n // Set permissions\n exec(\"chmod 755 /etc/trafic\");\n exec(\"chmod 700 /var/lib/trafic\");\n exec(\"chmod 700 /var/backups/trafic\");\n exec(\"chown root:ddev /etc/trafic\");\n exec(\"chown ddev:ddev /var/lib/trafic\");\n exec(\"chown ddev:ddev /var/backups/trafic\");\n\n // Config file (if exists)\n if (existsSync(\"/etc/trafic/config.toml\")) {\n exec(\"chmod 640 /etc/trafic/config.toml\");\n exec(\"chown root:ddev /etc/trafic/config.toml\");\n }\n\n success(\"File permissions configured\");\n}\n\n/**\n * Run all hardening steps\n */\nexport function hardenServer(sshUsers: string[] = [\"ddev\"]): void {\n hardenSsh(sshUsers);\n configureFirewall();\n configureUnattendedUpgrades();\n configureFail2ban();\n configureSystemLimits();\n configureFilePermissions();\n}\n","import { execSync } from \"node:child_process\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport type { AuditCheck } from \"./types.js\";\n\n/**\n * Run a command silently and return output\n */\nfunction execSilent(command: string): string {\n try {\n return execSync(command, { encoding: \"utf-8\", stdio: \"pipe\" }).trim();\n } catch {\n return \"\";\n }\n}\n\n/**\n * Check if a service is active\n */\nfunction isServiceActive(service: string): boolean {\n const status = execSilent(`systemctl is-active ${service}`);\n return status === \"active\";\n}\n\n/**\n * Audit SSH configuration\n */\nfunction auditSsh(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Check root login\n const sshdConfig = existsSync(\"/etc/ssh/sshd_config\")\n ? readFileSync(\"/etc/ssh/sshd_config\", \"utf-8\")\n : \"\";\n const traficConfig = existsSync(\"/etc/ssh/sshd_config.d/trafic.conf\")\n ? readFileSync(\"/etc/ssh/sshd_config.d/trafic.conf\", \"utf-8\")\n : \"\";\n\n const config = sshdConfig + \"\\n\" + traficConfig;\n\n const rootLoginDisabled =\n config.includes(\"PermitRootLogin no\") ||\n config.includes(\"PermitRootLogin prohibit-password\");\n checks.push({\n name: \"SSH root login\",\n status: rootLoginDisabled ? \"pass\" : \"fail\",\n message: rootLoginDisabled\n ? \"Root login disabled\"\n : \"Root login may be enabled\",\n fix: \"Add 'PermitRootLogin no' to /etc/ssh/sshd_config.d/trafic.conf\",\n });\n\n const passwordAuthDisabled = config.includes(\"PasswordAuthentication no\");\n checks.push({\n name: \"SSH password auth\",\n status: passwordAuthDisabled ? \"pass\" : \"warn\",\n message: passwordAuthDisabled\n ? \"Password authentication disabled\"\n : \"Password authentication may be enabled\",\n fix: \"Add 'PasswordAuthentication no' to /etc/ssh/sshd_config.d/trafic.conf\",\n });\n\n return checks;\n}\n\n/**\n * Audit firewall configuration\n */\nfunction auditFirewall(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n const ufwStatus = execSilent(\"ufw status\");\n const isActive = ufwStatus.includes(\"Status: active\");\n\n checks.push({\n name: \"Firewall (UFW)\",\n status: isActive ? \"pass\" : \"fail\",\n message: isActive ? \"UFW is active\" : \"UFW is not active\",\n fix: \"Run: ufw enable\",\n });\n\n if (isActive) {\n const allows22 = ufwStatus.includes(\"22/tcp\");\n const allows80 = ufwStatus.includes(\"80/tcp\");\n const allows443 = ufwStatus.includes(\"443/tcp\");\n\n if (allows22 && allows80 && allows443) {\n checks.push({\n name: \"Firewall ports\",\n status: \"pass\",\n message: \"Required ports (22, 80, 443) are open\",\n });\n } else {\n checks.push({\n name: \"Firewall ports\",\n status: \"warn\",\n message: \"Some required ports may be blocked\",\n fix: \"Run: ufw allow 22/tcp && ufw allow 80/tcp && ufw allow 443/tcp\",\n });\n }\n }\n\n return checks;\n}\n\n/**\n * Audit security services\n */\nfunction auditServices(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Fail2ban\n const fail2banActive = isServiceActive(\"fail2ban\");\n checks.push({\n name: \"Fail2ban\",\n status: fail2banActive ? \"pass\" : \"warn\",\n message: fail2banActive ? \"Fail2ban is active\" : \"Fail2ban is not running\",\n fix: \"Run: apt install fail2ban && systemctl enable fail2ban --now\",\n });\n\n // Unattended upgrades\n const unattendedActive = isServiceActive(\"unattended-upgrades\");\n checks.push({\n name: \"Automatic updates\",\n status: unattendedActive ? \"pass\" : \"warn\",\n message: unattendedActive\n ? \"Unattended upgrades enabled\"\n : \"Automatic updates not configured\",\n fix: \"Run: apt install unattended-upgrades && dpkg-reconfigure unattended-upgrades\",\n });\n\n // Trafic agent\n const agentActive = isServiceActive(\"trafic-agent\");\n checks.push({\n name: \"Trafic agent\",\n status: agentActive ? \"pass\" : \"fail\",\n message: agentActive\n ? \"Trafic agent is running\"\n : \"Trafic agent is not running\",\n fix: \"Run: systemctl start trafic-agent\",\n });\n\n return checks;\n}\n\n/**\n * Audit Docker\n */\nfunction auditDocker(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Docker running\n const dockerActive = isServiceActive(\"docker\");\n checks.push({\n name: \"Docker\",\n status: dockerActive ? \"pass\" : \"fail\",\n message: dockerActive ? \"Docker is running\" : \"Docker is not running\",\n fix: \"Run: systemctl start docker\",\n });\n\n // Dangling images\n const danglingImages = execSilent(\n \"docker images -f dangling=true -q | wc -l\",\n );\n const count = parseInt(danglingImages, 10) || 0;\n if (count > 0) {\n checks.push({\n name: \"Docker cleanup\",\n status: \"warn\",\n message: `${count} dangling Docker images`,\n fix: \"Run: docker system prune -f\",\n });\n }\n\n // Disk usage\n const diskUsage = execSilent(\"df -h / | awk 'NR==2{print $5}'\");\n const usagePercent = parseInt(diskUsage, 10) || 0;\n checks.push({\n name: \"Disk usage\",\n status: usagePercent > 90 ? \"fail\" : usagePercent > 80 ? \"warn\" : \"pass\",\n message: `Disk usage: ${diskUsage}`,\n fix: usagePercent > 80 ? \"Consider cleaning up old projects or Docker resources\" : undefined,\n });\n\n return checks;\n}\n\n/**\n * Audit file permissions\n */\nfunction auditPermissions(): AuditCheck[] {\n const checks: AuditCheck[] = [];\n\n // Config file\n if (existsSync(\"/etc/trafic/config.toml\")) {\n const stats = execSilent(\"stat -c '%a' /etc/trafic/config.toml\");\n const isSecure = stats === \"640\" || stats === \"600\";\n checks.push({\n name: \"Config permissions\",\n status: isSecure ? \"pass\" : \"warn\",\n message: isSecure\n ? \"Config file has secure permissions\"\n : `Config file permissions: ${stats} (should be 640)`,\n fix: \"Run: chmod 640 /etc/trafic/config.toml\",\n });\n }\n\n // Projects directory\n if (existsSync(\"/home/ddev/www\")) {\n const owner = execSilent(\"stat -c '%U:%G' /home/ddev/www\");\n const isCorrect = owner === \"ddev:ddev\";\n checks.push({\n name: \"Projects directory\",\n status: isCorrect ? \"pass\" : \"warn\",\n message: isCorrect\n ? \"Projects directory owned by ddev\"\n : `Projects directory owner: ${owner}`,\n fix: \"Run: chown ddev:ddev /home/ddev/www\",\n });\n }\n\n return checks;\n}\n\n/**\n * Run all audit checks\n */\nexport function runAudit(): AuditCheck[] {\n return [\n ...auditSsh(),\n ...auditFirewall(),\n ...auditServices(),\n ...auditDocker(),\n ...auditPermissions(),\n ];\n}\n\n/**\n * Print audit results\n */\nexport function printAuditResults(checks: AuditCheck[]): void {\n console.log(\"\\n\\x1b[1mSecurity Audit Results\\x1b[0m\\n\");\n\n for (const check of checks) {\n const icon =\n check.status === \"pass\"\n ? \"\\x1b[32m✓\\x1b[0m\"\n : check.status === \"warn\"\n ? \"\\x1b[33m⚠\\x1b[0m\"\n : \"\\x1b[31m✗\\x1b[0m\";\n\n console.log(`${icon} \\x1b[1m${check.name}\\x1b[0m: ${check.message}`);\n\n if (check.fix && check.status !== \"pass\") {\n console.log(` \\x1b[90m${check.fix}\\x1b[0m`);\n }\n }\n\n const passed = checks.filter((c) => c.status === \"pass\").length;\n const warnings = checks.filter((c) => c.status === \"warn\").length;\n const failed = checks.filter((c) => c.status === \"fail\").length;\n\n console.log(\n `\\n\\x1b[1mSummary:\\x1b[0m ${passed} passed, ${warnings} warnings, ${failed} failed`,\n );\n}\n","import type { SetupOptions } from \"./types.js\";\nimport { step, info, error, setDryRun, resetSteps, isRoot } from \"./steps.js\";\nimport { createDdevUser } from \"./user.js\";\nimport { installDocker, configureDocker, setupDockerPrune } from \"./docker.js\";\nimport { installSystemDeps, installDdev, configureDdev, configureTraefik } from \"./ddev.js\";\nimport { installNode, installAgent, createAgentConfig, createSystemdService } from \"./agent.js\";\nimport { hardenServer } from \"./hardening.js\";\nimport { runAudit, printAuditResults } from \"./audit.js\";\n\nexport * from \"./types.js\";\nexport * from \"./audit.js\";\n\n/**\n * Run the full server setup\n */\nexport async function setup(options: SetupOptions): Promise<void> {\n resetSteps();\n setDryRun(options.dryRun ?? false);\n\n console.log(\"\\n\\x1b[1m🚦 Trafic Server Setup\\x1b[0m\\n\");\n\n if (options.dryRun) {\n console.log(\"\\x1b[33m⚠ Dry-run mode: no changes will be made\\x1b[0m\\n\");\n }\n\n // Check root\n if (!isRoot() && !options.dryRun) {\n error(\"This command must be run as root\");\n console.log(\" Run: sudo trafic-agent setup ...\");\n process.exit(1);\n }\n\n info(`TLD: ${options.tld}`);\n if (options.email) {\n info(`Let's Encrypt email: ${options.email}`);\n }\n if (options.noHardening) {\n info(\"Hardening: disabled\");\n }\n\n try {\n // Step 1: Install system dependencies\n installSystemDeps();\n\n // Step 2: Create ddev user\n createDdevUser();\n\n // Step 3: Install Docker\n if (!options.noDocker) {\n installDocker();\n configureDocker();\n setupDockerPrune();\n } else {\n step(\"Install Docker\");\n info(\"Skipped (--no-docker)\");\n }\n\n // Step 4: Install DDEV\n if (!options.noDdev) {\n installDdev();\n configureDdev(options.tld, options.email);\n configureTraefik();\n } else {\n step(\"Install DDEV\");\n info(\"Skipped (--no-ddev)\");\n }\n\n // Step 5: Install Node.js\n installNode();\n\n // Step 6: Install and configure agent\n installAgent();\n createAgentConfig(options.tld, options.email);\n createSystemdService();\n\n // Step 7: Server hardening\n if (!options.noHardening) {\n const sshUsers = options.sshUsers ?? [\"ddev\"];\n hardenServer(sshUsers);\n } else {\n step(\"Server hardening\");\n info(\"Skipped (--no-hardening)\");\n }\n\n // Done\n console.log(\"\\n\\x1b[32m✓ Setup complete!\\x1b[0m\\n\");\n console.log(\"Next steps:\");\n console.log(` 1. Edit /etc/trafic/config.toml to configure authentication`);\n console.log(` 2. Add SSH public keys to /home/ddev/.ssh/authorized_keys`);\n console.log(` 3. Deploy your first project:`);\n console.log(` npx @studiometa/trafic-cli deploy --host=<this-server> --name=my-app`);\n console.log(`\\nYour projects will be available at https://<name>.${options.tld}`);\n } catch (err) {\n error(`Setup failed: ${err instanceof Error ? err.message : err}`);\n process.exit(1);\n }\n}\n\n/**\n * Run security audit\n */\nexport async function audit(): Promise<void> {\n const checks = runAudit();\n printAuditResults(checks);\n\n const failed = checks.filter((c) => c.status === \"fail\").length;\n if (failed > 0) {\n process.exit(1);\n }\n}\n"],"mappings":"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"}
|