@sleep2agi/commhub-server 0.5.1 → 0.5.2-preview.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +35 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/commhub-server",
3
- "version": "0.5.1",
3
+ "version": "0.5.2-preview.0",
4
4
  "description": "CommHub Server \u2014 AI Agent communication hub with MCP protocol, multi-network isolation, user auth, and 18 MCP tools.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/index.ts CHANGED
@@ -18,6 +18,24 @@ const SERVER_VERSION = (() => {
18
18
  } catch { return "?"; }
19
19
  })();
20
20
 
21
+ // In-memory log ring buffer — last N lines streamed via /api/server-logs.
22
+ // Wraps console.log/info/warn/error so EVERY existing log call lands here
23
+ // without source changes. Dashboard tails this buffer for "hub server log
24
+ // view" feature.
25
+ const LOG_RING_CAP = Number(process.env.COMMHUB_LOG_RING || 500);
26
+ type LogEntry = { ts: string; level: "log" | "info" | "warn" | "error"; line: string };
27
+ const logRing: LogEntry[] = [];
28
+ const _origConsole = { log: console.log.bind(console), info: console.info.bind(console), warn: console.warn.bind(console), error: console.error.bind(console) };
29
+ function pushLog(level: LogEntry["level"], args: any[]) {
30
+ const line = args.map(a => typeof a === "string" ? a : (() => { try { return JSON.stringify(a); } catch { return String(a); } })()).join(" ");
31
+ logRing.push({ ts: new Date().toISOString(), level, line: line.slice(0, 4000) });
32
+ if (logRing.length > LOG_RING_CAP) logRing.splice(0, logRing.length - LOG_RING_CAP);
33
+ }
34
+ console.log = (...args: any[]) => { pushLog("log", args); _origConsole.log(...args); };
35
+ console.info = (...args: any[]) => { pushLog("info", args); _origConsole.info(...args); };
36
+ console.warn = (...args: any[]) => { pushLog("warn", args); _origConsole.warn(...args); };
37
+ console.error = (...args: any[]) => { pushLog("error", args); _origConsole.error(...args); };
38
+
21
39
  // ── Rate limiter (in-memory, per IP) ──
22
40
  const rateLimits = new Map<string, { count: number; resetAt: number }>();
23
41
  function checkRateLimit(ip: string, maxPerMinute = 60): boolean {
@@ -838,6 +856,23 @@ Bun.serve({
838
856
  }));
839
857
  }
840
858
 
859
+ // ── REST: server log tail (in-memory ring buffer, last LOG_RING_CAP lines) ──
860
+ // Admin-only because logs may include user names + task content.
861
+ if (url.pathname === "/api/server-logs") {
862
+ const token = req.headers.get("Authorization")?.replace("Bearer ", "") || url.searchParams.get("token");
863
+ if (!token) return withCors(req, Response.json({ ok: false, error: "auth required" }, { status: 401 }));
864
+ const resolved = resolveToken(token);
865
+ if (!resolved) return withCors(req, Response.json({ ok: false, error: "invalid token" }, { status: 401 }));
866
+ if (resolved.user.role !== "admin") return withCors(req, Response.json({ ok: false, error: "admin only" }, { status: 403 }));
867
+ const limit = Math.min(Number(url.searchParams.get("limit")) || 200, LOG_RING_CAP);
868
+ const since = url.searchParams.get("since"); // ISO timestamp; only return logs newer
869
+ let entries = logRing.slice(-limit);
870
+ if (since) entries = entries.filter(e => e.ts > since);
871
+ // Newest first
872
+ entries = entries.slice().reverse();
873
+ return withCors(req, Response.json({ ok: true, logs: entries, capacity: LOG_RING_CAP }));
874
+ }
875
+
841
876
  // ── REST: audit log (V3) ──
842
877
  if (url.pathname === "/api/audit-log") {
843
878
  const token = req.headers.get("Authorization")?.replace("Bearer ", "") || url.searchParams.get("token");