@sleep2agi/commhub-server 0.5.0-preview.11 → 0.5.0-preview.13
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/package.json +1 -1
- package/src/db.ts +29 -0
- package/src/index.ts +44 -8
package/package.json
CHANGED
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 }));
|
|
@@ -274,7 +277,11 @@ Bun.serve({
|
|
|
274
277
|
if (url.pathname === "/api/status") {
|
|
275
278
|
const cutoff = new Date(Date.now() - 10 * 60 * 1000).toISOString().replace("T", " ").slice(0, 19);
|
|
276
279
|
db.run("UPDATE sessions SET status = 'offline' WHERE updated_at < ?1 AND status != 'offline'", [cutoff]);
|
|
277
|
-
const
|
|
280
|
+
const netFilter = url.searchParams.get("network_id");
|
|
281
|
+
const sql = netFilter
|
|
282
|
+
? "SELECT * FROM sessions WHERE network_id = ?1 ORDER BY updated_at DESC"
|
|
283
|
+
: "SELECT * FROM sessions ORDER BY updated_at DESC";
|
|
284
|
+
const sessions = netFilter ? db.query(sql).all(netFilter) : db.query(sql).all();
|
|
278
285
|
return withCors(req, Response.json({ ok: true, sessions }));
|
|
279
286
|
}
|
|
280
287
|
|
|
@@ -398,15 +405,18 @@ Bun.serve({
|
|
|
398
405
|
|
|
399
406
|
// ── REST: stats summary ──
|
|
400
407
|
if (url.pathname === "/api/stats") {
|
|
401
|
-
const
|
|
402
|
-
const
|
|
403
|
-
const
|
|
404
|
-
const
|
|
408
|
+
const n = url.searchParams.get("network_id");
|
|
409
|
+
const nw = n ? ` WHERE network_id = '${n}'` : "";
|
|
410
|
+
const taskStats = db.query<any, []>(`SELECT status, COUNT(*) as count FROM tasks${nw} GROUP BY status`).all();
|
|
411
|
+
const sessionStats = db.query<any, []>(`SELECT status, COUNT(*) as count FROM sessions${nw} GROUP BY status`).all();
|
|
412
|
+
const totalTasks = db.query<{ cnt: number }, []>(`SELECT COUNT(*) as cnt FROM tasks${nw}`).get();
|
|
413
|
+
const totalNodes = db.query<{ cnt: number }, []>(`SELECT COUNT(*) as cnt FROM nodes${nw}`).get();
|
|
405
414
|
const recentTasks = db.query<any, []>(
|
|
406
|
-
|
|
415
|
+
`SELECT task_id, from_name, to_name, status, created_at FROM tasks${nw} ORDER BY created_at DESC LIMIT 5`
|
|
407
416
|
).all();
|
|
408
417
|
return withCors(req, Response.json({
|
|
409
418
|
ok: true,
|
|
419
|
+
network_id: n || null,
|
|
410
420
|
tasks: { total: totalTasks?.cnt || 0, by_status: taskStats },
|
|
411
421
|
sessions: { by_status: sessionStats },
|
|
412
422
|
nodes: { total: totalNodes?.cnt || 0 },
|
|
@@ -414,6 +424,27 @@ Bun.serve({
|
|
|
414
424
|
}));
|
|
415
425
|
}
|
|
416
426
|
|
|
427
|
+
// ── REST: audit log (V3) ──
|
|
428
|
+
if (url.pathname === "/api/audit-log") {
|
|
429
|
+
const token = req.headers.get("Authorization")?.replace("Bearer ", "") || url.searchParams.get("token");
|
|
430
|
+
if (!token) return withCors(req, Response.json({ ok: false, error: "auth required" }, { status: 401 }));
|
|
431
|
+
const resolved = resolveToken(token);
|
|
432
|
+
if (!resolved) return withCors(req, Response.json({ ok: false, error: "invalid token" }, { status: 401 }));
|
|
433
|
+
const limit = Math.min(Number(url.searchParams.get("limit")) || 50, 200);
|
|
434
|
+
const action = url.searchParams.get("action");
|
|
435
|
+
const userId = url.searchParams.get("user_id");
|
|
436
|
+
let sql = "SELECT * FROM audit_log WHERE 1=1";
|
|
437
|
+
const params: any[] = [];
|
|
438
|
+
// Non-admin can only see own logs
|
|
439
|
+
if (resolved.user.role !== "admin") { sql += ` AND user_id = ?${params.length + 1}`; params.push(resolved.user.user_id); }
|
|
440
|
+
if (action) { sql += ` AND action = ?${params.length + 1}`; params.push(action); }
|
|
441
|
+
if (userId && resolved.user.role === "admin") { sql += ` AND user_id = ?${params.length + 1}`; params.push(userId); }
|
|
442
|
+
sql += ` ORDER BY created_at DESC LIMIT ?${params.length + 1}`;
|
|
443
|
+
params.push(limit);
|
|
444
|
+
const logs = db.query(sql).all(...params);
|
|
445
|
+
return withCors(req, Response.json({ ok: true, logs, count: logs.length }));
|
|
446
|
+
}
|
|
447
|
+
|
|
417
448
|
// ── REST: task events (V2 Sprint 2) ──
|
|
418
449
|
if (url.pathname === "/api/task_events") {
|
|
419
450
|
const taskId = url.searchParams.get("task_id");
|
|
@@ -431,8 +462,10 @@ Bun.serve({
|
|
|
431
462
|
if (url.pathname === "/api/nodes") {
|
|
432
463
|
const nodeId = url.searchParams.get("node_id");
|
|
433
464
|
const alias = url.searchParams.get("alias");
|
|
465
|
+
const netFilter = url.searchParams.get("network_id");
|
|
434
466
|
let sql = "SELECT * FROM nodes WHERE 1=1";
|
|
435
467
|
const params: any[] = [];
|
|
468
|
+
if (netFilter) { sql += ` AND network_id = ?${params.length + 1}`; params.push(netFilter); }
|
|
436
469
|
if (nodeId) { sql += ` AND node_id = ?${params.length + 1}`; params.push(nodeId); }
|
|
437
470
|
if (alias) { sql += ` AND alias = ?${params.length + 1}`; params.push(alias); }
|
|
438
471
|
sql += " ORDER BY updated_at DESC";
|
|
@@ -446,10 +479,12 @@ Bun.serve({
|
|
|
446
479
|
const status = url.searchParams.get("status");
|
|
447
480
|
const toName = url.searchParams.get("to_name");
|
|
448
481
|
const fromName = url.searchParams.get("from_name");
|
|
482
|
+
const netFilter = url.searchParams.get("network_id");
|
|
449
483
|
const limit = Math.min(Number(url.searchParams.get("limit")) || 50, 200);
|
|
450
484
|
|
|
451
485
|
let sql = "SELECT * FROM tasks WHERE 1=1";
|
|
452
486
|
const params: any[] = [];
|
|
487
|
+
if (netFilter) { sql += ` AND network_id = ?${params.length + 1}`; params.push(netFilter); }
|
|
453
488
|
if (taskId) { sql += ` AND task_id = ?${params.length + 1}`; params.push(taskId); }
|
|
454
489
|
if (status) { sql += ` AND status = ?${params.length + 1}`; params.push(status); }
|
|
455
490
|
if (toName) { sql += ` AND to_name = ?${params.length + 1}`; params.push(toName); }
|
|
@@ -458,7 +493,8 @@ Bun.serve({
|
|
|
458
493
|
params.push(limit);
|
|
459
494
|
|
|
460
495
|
const rows = db.query(sql).all(...params);
|
|
461
|
-
const
|
|
496
|
+
const statsFilter = netFilter ? ` WHERE network_id = '${netFilter}'` : "";
|
|
497
|
+
const stats = db.query<any, []>(`SELECT status, COUNT(*) as count FROM tasks${statsFilter} GROUP BY status`).all();
|
|
462
498
|
return withCors(req, Response.json({ ok: true, tasks: rows, count: rows.length, stats }));
|
|
463
499
|
}
|
|
464
500
|
|