@sleep2agi/commhub-server 0.5.0-preview.11 → 0.5.0-preview.12

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 (3) hide show
  1. package/package.json +1 -1
  2. package/src/db.ts +29 -0
  3. package/src/index.ts +25 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/commhub-server",
3
- "version": "0.5.0-preview.11",
3
+ "version": "0.5.0-preview.12",
4
4
  "description": "CommHub MCP Server — AI Agent communication hub with SSE push, MCP protocol, and REST API",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/db.ts CHANGED
@@ -201,6 +201,26 @@ db.exec(`
201
201
  CREATE INDEX IF NOT EXISTS idx_tokens_user ON api_tokens(user_id);
202
202
  `);
203
203
 
204
+ // ── V3: audit_log table ──
205
+ db.exec(`
206
+ CREATE TABLE IF NOT EXISTS audit_log (
207
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
208
+ user_id TEXT,
209
+ username TEXT,
210
+ action TEXT NOT NULL,
211
+ target_type TEXT,
212
+ target_id TEXT,
213
+ detail TEXT,
214
+ ip TEXT,
215
+ network_id TEXT,
216
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
217
+ );
218
+
219
+ CREATE INDEX IF NOT EXISTS idx_audit_created ON audit_log(created_at DESC);
220
+ CREATE INDEX IF NOT EXISTS idx_audit_user ON audit_log(user_id);
221
+ CREATE INDEX IF NOT EXISTS idx_audit_network ON audit_log(network_id);
222
+ `);
223
+
204
224
  // ── V3: add network_id to existing tables ──
205
225
  for (const table of ["sessions", "nodes", "tasks", "inbox", "task_events"]) {
206
226
  try { db.exec(`ALTER TABLE ${table} ADD COLUMN network_id TEXT`); } catch {}
@@ -230,6 +250,15 @@ export function generateToken(): string {
230
250
  return `atok_${crypto.randomUUID().replace(/-/g, "")}`;
231
251
  }
232
252
 
253
+ export function logAudit(userId: string | null, username: string | null, action: string, targetType?: string, targetId?: string, detail?: string, ip?: string, networkId?: string) {
254
+ try {
255
+ db.run(
256
+ "INSERT INTO audit_log (user_id, username, action, target_type, target_id, detail, ip, network_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
257
+ [userId, username, action, targetType ?? null, targetId ?? null, detail ?? null, ip ?? null, networkId ?? null]
258
+ );
259
+ } catch {}
260
+ }
261
+
233
262
  export function logTaskEvent(taskId: string, fromStatus: string | null, toStatus: string, actor: string, detail?: string) {
234
263
  try {
235
264
  db.run(
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
3
3
  import { z } from "zod/v4";
4
4
  import { registerTools } from "./tools.js";
5
- import { db, logTaskEvent } from "./db.js";
5
+ import { db, logTaskEvent, logAudit } from "./db.js";
6
6
  import { createSSEStream, pushEvent, pushBroadcast, getSSEStats } from "./push.js";
7
7
  import { register, login, resolveToken, getUserNetworks, createNetwork, type AuthUser } from "./auth.js";
8
8
 
@@ -146,6 +146,7 @@ Bun.serve({
146
146
  try {
147
147
  const body = await req.json() as any;
148
148
  const result = register(body.username, body.password, body.email, body.display_name);
149
+ if (result.ok) logAudit(result.user!.user_id, body.username, "register", "user", result.user!.user_id);
149
150
  return withCors(req, Response.json(result, { status: result.ok ? 200 : 400 }));
150
151
  } catch (e: any) {
151
152
  return withCors(req, Response.json({ ok: false, error: e.message }, { status: 400 }));
@@ -156,6 +157,8 @@ Bun.serve({
156
157
  try {
157
158
  const body = await req.json() as any;
158
159
  const result = login(body.username, body.password);
160
+ if (result.ok) logAudit(result.user!.user_id, body.username, "login", "user", result.user!.user_id);
161
+ else logAudit(null, body.username, "login_failed", "user", null, "invalid credentials");
159
162
  return withCors(req, Response.json(result, { status: result.ok ? 200 : 401 }));
160
163
  } catch (e: any) {
161
164
  return withCors(req, Response.json({ ok: false, error: e.message }, { status: 400 }));
@@ -414,6 +417,27 @@ Bun.serve({
414
417
  }));
415
418
  }
416
419
 
420
+ // ── REST: audit log (V3) ──
421
+ if (url.pathname === "/api/audit-log") {
422
+ const token = req.headers.get("Authorization")?.replace("Bearer ", "") || url.searchParams.get("token");
423
+ if (!token) return withCors(req, Response.json({ ok: false, error: "auth required" }, { status: 401 }));
424
+ const resolved = resolveToken(token);
425
+ if (!resolved) return withCors(req, Response.json({ ok: false, error: "invalid token" }, { status: 401 }));
426
+ const limit = Math.min(Number(url.searchParams.get("limit")) || 50, 200);
427
+ const action = url.searchParams.get("action");
428
+ const userId = url.searchParams.get("user_id");
429
+ let sql = "SELECT * FROM audit_log WHERE 1=1";
430
+ const params: any[] = [];
431
+ // Non-admin can only see own logs
432
+ if (resolved.user.role !== "admin") { sql += ` AND user_id = ?${params.length + 1}`; params.push(resolved.user.user_id); }
433
+ if (action) { sql += ` AND action = ?${params.length + 1}`; params.push(action); }
434
+ if (userId && resolved.user.role === "admin") { sql += ` AND user_id = ?${params.length + 1}`; params.push(userId); }
435
+ sql += ` ORDER BY created_at DESC LIMIT ?${params.length + 1}`;
436
+ params.push(limit);
437
+ const logs = db.query(sql).all(...params);
438
+ return withCors(req, Response.json({ ok: true, logs, count: logs.length }));
439
+ }
440
+
417
441
  // ── REST: task events (V2 Sprint 2) ──
418
442
  if (url.pathname === "/api/task_events") {
419
443
  const taskId = url.searchParams.get("task_id");