@studiometa/trafic-agent 0.1.8 → 0.1.9

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.
@@ -0,0 +1,687 @@
1
+ import { existsSync, readFileSync, watch } from "node:fs";
2
+ import { execSync } from "node:child_process";
3
+ import Database from "better-sqlite3";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { createServer } from "node:http";
6
+ import { fileURLToPath } from "node:url";
7
+ /**
8
+ * Check if an IP matches a pattern (supports CIDR notation)
9
+ */
10
+ function matchIp(ip, pattern) {
11
+ if (ip === pattern) return true;
12
+ if (pattern.includes("/")) return matchCidr(ip, pattern);
13
+ if (pattern.includes("*")) return new RegExp("^" + pattern.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$").test(ip);
14
+ return false;
15
+ }
16
+ /**
17
+ * Match IP against CIDR range
18
+ */
19
+ function matchCidr(ip, cidr) {
20
+ const [range, bits] = cidr.split("/");
21
+ const mask = parseInt(bits, 10);
22
+ const ipNum = ipToNumber(ip);
23
+ const rangeNum = ipToNumber(range);
24
+ if (ipNum === null || rangeNum === null) return false;
25
+ const maskNum = ~((1 << 32 - mask) - 1);
26
+ return (ipNum & maskNum) === (rangeNum & maskNum);
27
+ }
28
+ /**
29
+ * Convert IP string to number
30
+ */
31
+ function ipToNumber(ip) {
32
+ const parts = ip.split(".");
33
+ if (parts.length !== 4) return null;
34
+ let num = 0;
35
+ for (const part of parts) {
36
+ const n = parseInt(part, 10);
37
+ if (isNaN(n) || n < 0 || n > 255) return null;
38
+ num = num << 8 | n;
39
+ }
40
+ return num >>> 0;
41
+ }
42
+ /**
43
+ * Match hostname against a glob pattern
44
+ */
45
+ function matchHostname(hostname, pattern) {
46
+ if (hostname === pattern) return true;
47
+ return new RegExp("^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$").test(hostname);
48
+ }
49
+ /**
50
+ * Parse Basic auth header
51
+ */
52
+ function parseBasicAuth(header) {
53
+ if (!header.startsWith("Basic ")) return null;
54
+ try {
55
+ const encoded = header.slice(6);
56
+ const decoded = Buffer.from(encoded, "base64").toString("utf-8");
57
+ const colonIndex = decoded.indexOf(":");
58
+ if (colonIndex === -1) return null;
59
+ return {
60
+ username: decoded.slice(0, colonIndex),
61
+ password: decoded.slice(colonIndex + 1)
62
+ };
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+ /**
68
+ * Parse Bearer token from header
69
+ */
70
+ function parseBearerToken(header) {
71
+ if (!header.startsWith("Bearer ")) return null;
72
+ return header.slice(7);
73
+ }
74
+ /**
75
+ * Check authentication for a request
76
+ */
77
+ function checkAuth(request, config) {
78
+ const { hostname, ip, authorization } = request;
79
+ const realIp = request.forwardedFor?.split(",")[0].trim() ?? ip;
80
+ for (const pattern of config.allowedIps) if (matchIp(realIp, pattern)) return {
81
+ allowed: true,
82
+ reason: "ip"
83
+ };
84
+ for (const rule of config.rules) if (matchHostname(hostname, rule.match)) return checkRule(rule, realIp, authorization, config);
85
+ return applyDefaultPolicy(config, realIp, authorization);
86
+ }
87
+ /**
88
+ * Check a specific rule
89
+ */
90
+ function checkRule(rule, ip, authorization, config) {
91
+ if (rule.allowedIps) {
92
+ for (const pattern of rule.allowedIps) if (matchIp(ip, pattern)) return {
93
+ allowed: true,
94
+ reason: "rule"
95
+ };
96
+ }
97
+ switch (rule.policy) {
98
+ case "allow": return {
99
+ allowed: true,
100
+ reason: "rule"
101
+ };
102
+ case "deny": return {
103
+ allowed: false,
104
+ reason: "rule"
105
+ };
106
+ case "token": {
107
+ if (!authorization) return {
108
+ allowed: false,
109
+ reason: "rule"
110
+ };
111
+ const token = parseBearerToken(authorization);
112
+ const tokens = rule.tokens ?? config.tokens;
113
+ if (token && tokens.includes(token)) return {
114
+ allowed: true,
115
+ reason: "token"
116
+ };
117
+ return {
118
+ allowed: false,
119
+ reason: "rule"
120
+ };
121
+ }
122
+ case "basic": {
123
+ if (!authorization) return {
124
+ allowed: false,
125
+ reason: "rule"
126
+ };
127
+ const creds = parseBasicAuth(authorization);
128
+ if (creds) {
129
+ const credString = `${creds.username}:${creds.password}`;
130
+ if (config.basicAuth.includes(credString)) return {
131
+ allowed: true,
132
+ reason: "basic"
133
+ };
134
+ }
135
+ return {
136
+ allowed: false,
137
+ reason: "rule"
138
+ };
139
+ }
140
+ default: return {
141
+ allowed: false,
142
+ reason: "rule"
143
+ };
144
+ }
145
+ }
146
+ /**
147
+ * Apply default policy
148
+ */
149
+ function applyDefaultPolicy(config, ip, authorization) {
150
+ switch (config.defaultPolicy) {
151
+ case "allow": return {
152
+ allowed: true,
153
+ reason: "default"
154
+ };
155
+ case "deny": return {
156
+ allowed: false,
157
+ reason: "default"
158
+ };
159
+ case "basic": {
160
+ if (!authorization) return {
161
+ allowed: false,
162
+ reason: "default"
163
+ };
164
+ const creds = parseBasicAuth(authorization);
165
+ if (creds) {
166
+ const credString = `${creds.username}:${creds.password}`;
167
+ if (config.basicAuth.includes(credString)) return {
168
+ allowed: true,
169
+ reason: "basic"
170
+ };
171
+ }
172
+ const token = parseBearerToken(authorization);
173
+ if (token && config.tokens.includes(token)) return {
174
+ allowed: true,
175
+ reason: "token"
176
+ };
177
+ return {
178
+ allowed: false,
179
+ reason: "default"
180
+ };
181
+ }
182
+ case "token": {
183
+ if (!authorization) return {
184
+ allowed: false,
185
+ reason: "default"
186
+ };
187
+ const token = parseBearerToken(authorization);
188
+ if (token && config.tokens.includes(token)) return {
189
+ allowed: true,
190
+ reason: "token"
191
+ };
192
+ return {
193
+ allowed: false,
194
+ reason: "default"
195
+ };
196
+ }
197
+ default: return {
198
+ allowed: false,
199
+ reason: "default"
200
+ };
201
+ }
202
+ }
203
+ function parseSimpleYaml$1(content) {
204
+ const result = {};
205
+ const lines = content.split("\n");
206
+ for (const line of lines) {
207
+ const trimmed = line.trim();
208
+ if (!trimmed || trimmed.startsWith("#")) continue;
209
+ const colonIndex = trimmed.indexOf(":");
210
+ if (colonIndex === -1) continue;
211
+ const key = trimmed.slice(0, colonIndex).trim();
212
+ let value = trimmed.slice(colonIndex + 1).trim();
213
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
214
+ result[key] = value;
215
+ }
216
+ return result;
217
+ }
218
+ /**
219
+ * Load projects from DDEV's project_list.yaml
220
+ * Returns a map of project name -> app root path
221
+ */
222
+ function loadProjectList(projectListPath) {
223
+ if (!existsSync(projectListPath)) return /* @__PURE__ */ new Map();
224
+ const parsed = parseSimpleYaml$1(readFileSync(projectListPath, "utf-8"));
225
+ return new Map(Object.entries(parsed));
226
+ }
227
+ /**
228
+ * Build a hostname -> project name index
229
+ */
230
+ function buildHostnameIndex(projects, tld) {
231
+ const index = /* @__PURE__ */ new Map();
232
+ for (const [name] of projects) index.set(`${name}.${tld}`, name);
233
+ return index;
234
+ }
235
+ /**
236
+ * Get project name from hostname
237
+ */
238
+ function getProjectFromHostname(hostname, index) {
239
+ return index.get(hostname);
240
+ }
241
+ /**
242
+ * Watch project_list.yaml for changes
243
+ */
244
+ function watchProjectList(projectListPath, onChange) {
245
+ if (!existsSync(projectListPath)) {
246
+ console.warn(`Project list not found: ${projectListPath}`);
247
+ return;
248
+ }
249
+ watch(projectListPath, (eventType) => {
250
+ if (eventType === "change") onChange();
251
+ });
252
+ }
253
+ /**
254
+ * Get detailed project info using ddev describe
255
+ */
256
+ function getProjectInfo(name) {
257
+ try {
258
+ const output = execSync(`ddev describe ${name} -j`, {
259
+ encoding: "utf-8",
260
+ timeout: 1e4
261
+ });
262
+ const data = JSON.parse(output);
263
+ if (!data.raw) return void 0;
264
+ const raw = data.raw;
265
+ return {
266
+ name: raw.name,
267
+ status: raw.status,
268
+ appRoot: raw.approot,
269
+ httpURLs: raw.httpURLs ?? [],
270
+ httpsURLs: raw.httpsURLs ?? [],
271
+ type: raw.type,
272
+ phpVersion: raw.php_version,
273
+ dbType: raw.dbinfo?.dbType
274
+ };
275
+ } catch {
276
+ return;
277
+ }
278
+ }
279
+ /**
280
+ * Start a DDEV project
281
+ */
282
+ function startProject(name) {
283
+ try {
284
+ execSync(`ddev start ${name}`, {
285
+ encoding: "utf-8",
286
+ timeout: 12e4,
287
+ stdio: "pipe"
288
+ });
289
+ return true;
290
+ } catch (error) {
291
+ console.error(`Failed to start project ${name}:`, error);
292
+ return false;
293
+ }
294
+ }
295
+ /**
296
+ * Stop a DDEV project
297
+ */
298
+ function stopProject(name) {
299
+ try {
300
+ execSync(`ddev stop ${name}`, {
301
+ encoding: "utf-8",
302
+ timeout: 6e4,
303
+ stdio: "pipe"
304
+ });
305
+ return true;
306
+ } catch (error) {
307
+ console.error(`Failed to stop project ${name}:`, error);
308
+ return false;
309
+ }
310
+ }
311
+ /**
312
+ * List all DDEV projects with their status
313
+ */
314
+ function listProjects() {
315
+ try {
316
+ const output = execSync("ddev list -j", {
317
+ encoding: "utf-8",
318
+ timeout: 3e4
319
+ });
320
+ const data = JSON.parse(output);
321
+ if (!Array.isArray(data)) return [];
322
+ return data.map((p) => ({
323
+ name: p.name,
324
+ status: p.status,
325
+ appRoot: p.approot,
326
+ httpURLs: p.httpURLs ?? [],
327
+ httpsURLs: p.httpsURLs ?? [],
328
+ type: p.type
329
+ }));
330
+ } catch {
331
+ return [];
332
+ }
333
+ }
334
+ var db = null;
335
+ /**
336
+ * Initialize the database connection and schema
337
+ */
338
+ function initDb(dbPath) {
339
+ db = new Database(dbPath);
340
+ db.pragma("journal_mode = WAL");
341
+ db.exec(`
342
+ CREATE TABLE IF NOT EXISTS projects (
343
+ name TEXT PRIMARY KEY,
344
+ last_access INTEGER NOT NULL,
345
+ status TEXT NOT NULL DEFAULT 'stopped'
346
+ );
347
+
348
+ CREATE TABLE IF NOT EXISTS access_logs (
349
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
350
+ project TEXT NOT NULL,
351
+ timestamp INTEGER NOT NULL,
352
+ ip TEXT NOT NULL,
353
+ user_agent TEXT NOT NULL,
354
+ path TEXT NOT NULL
355
+ );
356
+
357
+ CREATE INDEX IF NOT EXISTS idx_access_logs_project ON access_logs(project);
358
+ CREATE INDEX IF NOT EXISTS idx_access_logs_timestamp ON access_logs(timestamp);
359
+ `);
360
+ return db;
361
+ }
362
+ /**
363
+ * Get the database instance
364
+ */
365
+ function getDb() {
366
+ if (!db) throw new Error("Database not initialized. Call initDb first.");
367
+ return db;
368
+ }
369
+ /**
370
+ * Close the database connection
371
+ */
372
+ function closeDb() {
373
+ if (db) {
374
+ db.close();
375
+ db = null;
376
+ }
377
+ }
378
+ /**
379
+ * Update project last access time
380
+ */
381
+ function updateProjectAccess(name) {
382
+ getDb().prepare(`
383
+ INSERT INTO projects (name, last_access, status)
384
+ VALUES (?, ?, 'running')
385
+ ON CONFLICT(name) DO UPDATE SET last_access = excluded.last_access
386
+ `).run(name, Date.now());
387
+ }
388
+ /**
389
+ * Get project record
390
+ */
391
+ function getProject(name) {
392
+ return getDb().prepare("SELECT name, last_access as lastAccess, status FROM projects WHERE name = ?").get(name);
393
+ }
394
+ /**
395
+ * Set project status
396
+ */
397
+ function setProjectStatus(name, status) {
398
+ getDb().prepare(`
399
+ INSERT INTO projects (name, last_access, status)
400
+ VALUES (?, ?, ?)
401
+ ON CONFLICT(name) DO UPDATE SET status = excluded.status
402
+ `).run(name, Date.now(), status);
403
+ }
404
+ /**
405
+ * Get idle projects (last access older than threshold)
406
+ */
407
+ function getIdleProjects(thresholdMs) {
408
+ const database = getDb();
409
+ const cutoff = Date.now() - thresholdMs;
410
+ return database.prepare(`
411
+ SELECT name, last_access as lastAccess, status
412
+ FROM projects
413
+ WHERE last_access < ? AND status = 'running'
414
+ `).all(cutoff);
415
+ }
416
+ /**
417
+ * Log an access
418
+ */
419
+ function logAccess(log) {
420
+ getDb().prepare(`
421
+ INSERT INTO access_logs (project, timestamp, ip, user_agent, path)
422
+ VALUES (?, ?, ?, ?, ?)
423
+ `).run(log.project, log.timestamp, log.ip, log.userAgent, log.path);
424
+ }
425
+ /**
426
+ * Get recent access logs for a project
427
+ */
428
+ function getAccessLogs(project, limit = 100) {
429
+ return getDb().prepare(`
430
+ SELECT id, project, timestamp, ip, user_agent as userAgent, path
431
+ FROM access_logs
432
+ WHERE project = ?
433
+ ORDER BY timestamp DESC
434
+ LIMIT ?
435
+ `).all(project, limit);
436
+ }
437
+ /**
438
+ * Clean old access logs (older than given days)
439
+ */
440
+ function cleanOldLogs(days) {
441
+ const database = getDb();
442
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
443
+ return database.prepare("DELETE FROM access_logs WHERE timestamp < ?").run(cutoff).changes;
444
+ }
445
+ /**
446
+ * Simple YAML parser for config.trafic.yaml
447
+ * Only supports simple key: value pairs (no nesting, no arrays)
448
+ */
449
+ function parseSimpleYaml(content) {
450
+ const result = {};
451
+ const lines = content.split("\n");
452
+ for (const line of lines) {
453
+ const trimmed = line.trim();
454
+ if (!trimmed || trimmed.startsWith("#")) continue;
455
+ const colonIndex = trimmed.indexOf(":");
456
+ if (colonIndex === -1) continue;
457
+ const key = trimmed.slice(0, colonIndex).trim();
458
+ let value = trimmed.slice(colonIndex + 1).trim();
459
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
460
+ result[key] = value;
461
+ }
462
+ return result;
463
+ }
464
+ /**
465
+ * Load project-specific Trafic config from .ddev/config.trafic.yaml
466
+ */
467
+ function loadProjectConfig(projectDir) {
468
+ const configPath = join(projectDir, ".ddev", "config.trafic.yaml");
469
+ if (!existsSync(configPath)) return {};
470
+ try {
471
+ const parsed = parseSimpleYaml(readFileSync(configPath, "utf-8"));
472
+ const config = {};
473
+ if (parsed.auth_policy) {
474
+ const policy = parsed.auth_policy.toLowerCase();
475
+ if ([
476
+ "allow",
477
+ "deny",
478
+ "basic",
479
+ "token"
480
+ ].includes(policy)) config.auth_policy = policy;
481
+ }
482
+ if (parsed.idle_timeout) config.idle_timeout = parsed.idle_timeout;
483
+ return config;
484
+ } catch (error) {
485
+ console.warn(`Failed to load project config from ${configPath}:`, error);
486
+ return {};
487
+ }
488
+ }
489
+ /**
490
+ * Check if a project should never be stopped (idle_timeout = "never")
491
+ */
492
+ function shouldNeverStop(config) {
493
+ return config.idle_timeout?.toLowerCase() === "never";
494
+ }
495
+ /**
496
+ * Get idle timeout in milliseconds, or undefined to use global default
497
+ */
498
+ function getIdleTimeoutMs(config) {
499
+ if (!config.idle_timeout) return void 0;
500
+ if (shouldNeverStop(config)) return void 0;
501
+ const match = config.idle_timeout.match(/(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/);
502
+ if (!match) return void 0;
503
+ const hours = parseInt(match[1] ?? "0", 10);
504
+ const minutes = parseInt(match[2] ?? "0", 10);
505
+ const seconds = parseInt(match[3] ?? "0", 10);
506
+ const ms = (hours * 3600 + minutes * 60 + seconds) * 1e3;
507
+ return ms > 0 ? ms : void 0;
508
+ }
509
+ var __dirname = dirname(fileURLToPath(import.meta.url));
510
+ var config;
511
+ var projectList;
512
+ var hostnameIndex;
513
+ var projectConfigs = /* @__PURE__ */ new Map();
514
+ /**
515
+ * Load HTML template
516
+ */
517
+ function loadTemplate(name) {
518
+ const templatePath = resolve(__dirname, "..", "templates", `${name}.html`);
519
+ try {
520
+ return readFileSync(templatePath, "utf-8");
521
+ } catch {
522
+ return `<html><body><h1>${name}</h1></body></html>`;
523
+ }
524
+ }
525
+ /**
526
+ * Get effective auth config for a project (merges global + per-project)
527
+ */
528
+ function getEffectiveAuthConfig(projectName) {
529
+ if (!projectName) return config.auth;
530
+ const projectConfig = projectConfigs.get(projectName);
531
+ if (!projectConfig?.auth_policy) return config.auth;
532
+ return {
533
+ ...config.auth,
534
+ defaultPolicy: projectConfig.auth_policy
535
+ };
536
+ }
537
+ /**
538
+ * Handle forward auth requests from Traefik
539
+ * Traefik sends the original request headers, we return 200 (allow) or 401 (deny)
540
+ */
541
+ function handleAuth(req, res) {
542
+ const hostname = req.headers["x-forwarded-host"] ?? "";
543
+ const ip = req.headers["x-forwarded-for"] ?? req.socket.remoteAddress ?? "";
544
+ const authorization = req.headers["authorization"];
545
+ const path = req.headers["x-forwarded-uri"] ?? "/";
546
+ const projectName = hostnameIndex.get(hostname);
547
+ const authConfig = getEffectiveAuthConfig(projectName);
548
+ if (checkAuth({
549
+ hostname,
550
+ ip: ip.split(",")[0].trim(),
551
+ authorization,
552
+ forwardedFor: ip
553
+ }, authConfig).allowed) {
554
+ if (projectName) {
555
+ updateProjectAccess(projectName);
556
+ logAccess({
557
+ project: projectName,
558
+ timestamp: Date.now(),
559
+ ip: ip.split(",")[0].trim(),
560
+ userAgent: req.headers["user-agent"] ?? "",
561
+ path
562
+ });
563
+ }
564
+ res.writeHead(200);
565
+ res.end();
566
+ } else {
567
+ res.writeHead(401, { "WWW-Authenticate": "Basic realm=\"Trafic\"" });
568
+ res.end("Unauthorized");
569
+ }
570
+ }
571
+ /**
572
+ * Handle errors middleware requests (502 from Traefik)
573
+ * When a project is stopped, Traefik returns 502. We show a waiting page and start the project.
574
+ */
575
+ async function handleErrors(req, res) {
576
+ const hostname = req.headers["x-forwarded-host"] ?? req.headers.host ?? "";
577
+ const projectName = hostnameIndex.get(hostname);
578
+ if (!projectName) {
579
+ const template = loadTemplate("error");
580
+ res.writeHead(404, { "Content-Type": "text/html" });
581
+ res.end(template.replace("{{message}}", "Project not found"));
582
+ return;
583
+ }
584
+ if (getProject(projectName)?.status === "starting") {
585
+ const template = loadTemplate("wait");
586
+ res.writeHead(503, {
587
+ "Content-Type": "text/html",
588
+ "Retry-After": "5"
589
+ });
590
+ res.end(template.replace(/\{\{project\}\}/g, projectName).replace(/\{\{hostname\}\}/g, hostname));
591
+ return;
592
+ }
593
+ setProjectStatus(projectName, "starting");
594
+ const template = loadTemplate("wait");
595
+ res.writeHead(503, {
596
+ "Content-Type": "text/html",
597
+ "Retry-After": "5"
598
+ });
599
+ res.end(template.replace(/\{\{project\}\}/g, projectName).replace(/\{\{hostname\}\}/g, hostname));
600
+ setTimeout(() => {
601
+ setProjectStatus(projectName, startProject(projectName) ? "running" : "stopped");
602
+ }, 100);
603
+ }
604
+ /**
605
+ * Handle status polling requests
606
+ */
607
+ function handleStatus(req, res) {
608
+ const projectName = new URL(req.url ?? "/", `http://${req.headers.host}`).searchParams.get("project");
609
+ if (!projectName) {
610
+ res.writeHead(400, { "Content-Type": "application/json" });
611
+ res.end(JSON.stringify({ error: "Missing project parameter" }));
612
+ return;
613
+ }
614
+ const info = getProjectInfo(projectName);
615
+ const record = getProject(projectName);
616
+ res.writeHead(200, { "Content-Type": "application/json" });
617
+ res.end(JSON.stringify({
618
+ name: projectName,
619
+ status: info?.status ?? record?.status ?? "unknown",
620
+ ready: info?.status === "running"
621
+ }));
622
+ }
623
+ /**
624
+ * Request handler
625
+ */
626
+ async function handleRequest(req, res) {
627
+ const url = req.url ?? "/";
628
+ try {
629
+ if (url === "/__auth__" || url.startsWith("/__auth__/")) handleAuth(req, res);
630
+ else if (url === "/__status__" || url.startsWith("/__status__/")) handleStatus(req, res);
631
+ else if (url === "/__health__") {
632
+ res.writeHead(200, { "Content-Type": "application/json" });
633
+ res.end(JSON.stringify({
634
+ status: "ok",
635
+ version: "__VERSION__"
636
+ }));
637
+ } else await handleErrors(req, res);
638
+ } catch (error) {
639
+ console.error("Request error:", error);
640
+ res.writeHead(500, { "Content-Type": "text/plain" });
641
+ res.end("Internal Server Error");
642
+ }
643
+ }
644
+ /**
645
+ * Reload project list and their configs
646
+ */
647
+ function reloadProjects() {
648
+ projectList = loadProjectList(config.projectListPath);
649
+ hostnameIndex = buildHostnameIndex(projectList, config.tld);
650
+ projectConfigs.clear();
651
+ for (const [name, projectDir] of projectList) {
652
+ const projectConfig = loadProjectConfig(projectDir);
653
+ if (projectConfig.auth_policy || projectConfig.idle_timeout) {
654
+ projectConfigs.set(name, projectConfig);
655
+ console.log(` ${name}: auth=${projectConfig.auth_policy ?? "default"}, idle=${projectConfig.idle_timeout ?? "default"}`);
656
+ }
657
+ }
658
+ console.log(`Loaded ${projectList.size} projects (${projectConfigs.size} with custom config)`);
659
+ }
660
+ /**
661
+ * Start the agent server
662
+ */
663
+ function startServer(agentConfig) {
664
+ config = agentConfig;
665
+ initDb(config.dbPath);
666
+ reloadProjects();
667
+ watchProjectList(config.projectListPath, () => {
668
+ console.log("Project list changed, reloading...");
669
+ reloadProjects();
670
+ });
671
+ createServer((req, res) => {
672
+ handleRequest(req, res).catch((error) => {
673
+ console.error("Unhandled error:", error);
674
+ if (!res.headersSent) {
675
+ res.writeHead(500);
676
+ res.end("Internal Server Error");
677
+ }
678
+ });
679
+ }).listen(config.port, () => {
680
+ console.log(`Trafic agent listening on port ${config.port}`);
681
+ console.log(`TLD: ${config.tld}`);
682
+ console.log(`Projects: ${projectList.size}`);
683
+ });
684
+ }
685
+ 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
+ //# sourceMappingURL=server-D0N_6y4c.js.map