@sleep2agi/commhub-server 0.5.0-preview.24 → 0.5.0-preview.26

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/src/index.ts CHANGED
@@ -4,7 +4,7 @@ import { z } from "zod/v4";
4
4
  import { registerTools } from "./tools.js";
5
5
  import { db, logTaskEvent, logAudit } from "./db.js";
6
6
  import { createSSEStream, pushEvent, pushBroadcast, getSSEStats } from "./push.js";
7
- import { register, login, resolveToken, getUserNetworks, createNetwork, deleteNetwork, renameNetwork, changePassword, listTokens, createToken, revokeToken, type AuthUser } from "./auth.js";
7
+ import { register, login, resolveToken, getUserNetworks, getUserAllNetworks, createNetwork, deleteNetwork, renameNetwork, changePassword, listTokens, createToken, revokeToken, getNetworkMembers, getUserNetworkRole, addNetworkMember, updateMemberRole, removeNetworkMember, createInvite, joinByInvite, type AuthUser } from "./auth.js";
8
8
 
9
9
  const PORT = Number(process.env.PORT) || 9200;
10
10
  const AUTH_TOKEN = process.env.COMMHUB_AUTH_TOKEN;
@@ -128,9 +128,8 @@ setInterval(() => {
128
128
  if (result.changes > 0) {
129
129
  console.log(`[patrol] expired ${result.changes} stale task(s)`);
130
130
  // Log events for expired tasks
131
- const expired = db.query<{ task_id: string }, []>(
132
- "SELECT task_id FROM tasks WHERE status = 'expired' AND completed_at >= datetime('now', '-1 minute')"
133
- ).all();
131
+ const expired = db.all<{ task_id: string }>(
132
+ "SELECT task_id FROM tasks WHERE status = 'expired' AND completed_at >= datetime('now', '-1 minute')");
134
133
  for (const t of expired) logTaskEvent(t.task_id, null, "expired", "patrol");
135
134
  }
136
135
  } catch {}
@@ -188,7 +187,7 @@ Bun.serve({
188
187
 
189
188
  // ── V3: License endpoints ──
190
189
  if (url.pathname === "/api/license" && req.method === "GET") {
191
- const license = db.query<any, []>("SELECT * FROM licenses ORDER BY created_at LIMIT 1").get();
190
+ const license = db.get<any>("SELECT * FROM licenses ORDER BY created_at LIMIT 1");
192
191
  if (!license) return withCors(req, Response.json({ ok: true, status: "no_license" }));
193
192
  const now = new Date().toISOString().replace("T", " ").slice(0, 19);
194
193
  const expired = license.expires_at && license.expires_at < now;
@@ -283,7 +282,7 @@ Bun.serve({
283
282
  db.run(`UPDATE users SET ${updates.join(", ")} WHERE user_id = ?${params.length}`, params);
284
283
  }
285
284
  // Re-fetch
286
- const user = db.query<any, [string]>("SELECT user_id, username, display_name, email, role FROM users WHERE user_id = ?1").get(resolved.user.user_id);
285
+ const user = db.get<any>("SELECT user_id, username, display_name, email, role FROM users WHERE user_id = ?1", resolved.user.user_id);
287
286
  return withCors(req, Response.json({ ok: true, user }));
288
287
  } catch (e: any) {
289
288
  return withCors(req, Response.json({ ok: false, error: e.message }, { status: 400 }));
@@ -347,7 +346,8 @@ Bun.serve({
347
346
  if (!token) return withCors(req, Response.json({ ok: false, error: "token required" }, { status: 401 }));
348
347
  const resolved = resolveToken(token);
349
348
  if (!resolved) return withCors(req, Response.json({ ok: false, error: "invalid token" }, { status: 401 }));
350
- const networks = getUserNetworks(resolved.user.user_id);
349
+ // V3.13: return all networks user is a member of (not just owner)
350
+ const networks = getUserAllNetworks(resolved.user.user_id);
351
351
  return withCors(req, Response.json({ ok: true, networks }));
352
352
  }
353
353
 
@@ -365,6 +365,72 @@ Bun.serve({
365
365
  }
366
366
  }
367
367
 
368
+ // ── V3.13: Network members + invites ──
369
+ const membersMatch = url.pathname.match(/^\/api\/networks\/([^/]+)\/members(?:\/([^/]+))?$/);
370
+ if (membersMatch) {
371
+ const token = req.headers.get("Authorization")?.replace("Bearer ", "") || url.searchParams.get("token");
372
+ if (!token) return withCors(req, Response.json({ ok: false, error: "auth required" }, { status: 401 }));
373
+ const resolved = resolveToken(token);
374
+ if (!resolved) return withCors(req, Response.json({ ok: false, error: "invalid token" }, { status: 401 }));
375
+ const netId = membersMatch[1];
376
+ const targetUid = membersMatch[2];
377
+ const callerRole = getUserNetworkRole(resolved.user.user_id, netId);
378
+ if (!callerRole) return withCors(req, Response.json({ ok: false, error: "not a member of this network" }, { status: 403 }));
379
+
380
+ if (req.method === "GET") {
381
+ if (!["owner", "admin"].includes(callerRole)) return withCors(req, Response.json({ ok: false, error: "owner/admin required" }, { status: 403 }));
382
+ const members = getNetworkMembers(netId);
383
+ return withCors(req, Response.json({ ok: true, members }));
384
+ }
385
+ if (req.method === "POST") {
386
+ if (!["owner", "admin"].includes(callerRole)) return withCors(req, Response.json({ ok: false, error: "owner/admin required" }, { status: 403 }));
387
+ const body = await req.json() as any;
388
+ const result = addNetworkMember(netId, body.user_id, body.role || "member", resolved.user.user_id);
389
+ if (result.ok) logAudit(resolved.user.user_id, resolved.user.username, "member_added", "network", netId, `${body.user_id} as ${body.role || "member"}`);
390
+ return withCors(req, Response.json(result, { status: result.ok ? 200 : 400 }));
391
+ }
392
+ if (req.method === "PUT" && targetUid) {
393
+ if (callerRole !== "owner") return withCors(req, Response.json({ ok: false, error: "owner required" }, { status: 403 }));
394
+ const body = await req.json() as any;
395
+ const result = updateMemberRole(netId, targetUid, body.role);
396
+ if (result.ok) logAudit(resolved.user.user_id, resolved.user.username, "member_role_changed", "network", netId, `${targetUid} → ${body.role}`);
397
+ return withCors(req, Response.json(result, { status: result.ok ? 200 : 400 }));
398
+ }
399
+ if (req.method === "DELETE" && targetUid) {
400
+ if (!["owner", "admin"].includes(callerRole)) return withCors(req, Response.json({ ok: false, error: "owner/admin required" }, { status: 403 }));
401
+ const result = removeNetworkMember(netId, targetUid);
402
+ if (result.ok) logAudit(resolved.user.user_id, resolved.user.username, "member_removed", "network", netId, targetUid);
403
+ return withCors(req, Response.json(result, { status: result.ok ? 200 : 400 }));
404
+ }
405
+ }
406
+
407
+ if (url.pathname.match(/^\/api\/networks\/([^/]+)\/invite$/) && req.method === "POST") {
408
+ const token = req.headers.get("Authorization")?.replace("Bearer ", "") || url.searchParams.get("token");
409
+ if (!token) return withCors(req, Response.json({ ok: false, error: "auth required" }, { status: 401 }));
410
+ const resolved = resolveToken(token);
411
+ if (!resolved) return withCors(req, Response.json({ ok: false, error: "invalid token" }, { status: 401 }));
412
+ const netId = url.pathname.split("/")[3];
413
+ const callerRole = getUserNetworkRole(resolved.user.user_id, netId);
414
+ if (!callerRole || !["owner", "admin"].includes(callerRole)) {
415
+ return withCors(req, Response.json({ ok: false, error: "owner/admin required" }, { status: 403 }));
416
+ }
417
+ const body = await req.json() as any;
418
+ const result = createInvite(netId, resolved.user.user_id, body.role || "member", body.max_uses || 1, body.expires_days);
419
+ if (result.ok) logAudit(resolved.user.user_id, resolved.user.username, "invite_created", "network", netId, result.invite_code);
420
+ return withCors(req, Response.json(result));
421
+ }
422
+
423
+ if (url.pathname === "/api/networks/join" && req.method === "POST") {
424
+ const token = req.headers.get("Authorization")?.replace("Bearer ", "") || url.searchParams.get("token");
425
+ if (!token) return withCors(req, Response.json({ ok: false, error: "auth required" }, { status: 401 }));
426
+ const resolved = resolveToken(token);
427
+ if (!resolved) return withCors(req, Response.json({ ok: false, error: "invalid token" }, { status: 401 }));
428
+ const body = await req.json() as any;
429
+ const result = joinByInvite(body.invite_code, resolved.user.user_id);
430
+ if (result.ok) logAudit(resolved.user.user_id, resolved.user.username, "network_joined", "network", result.network_id, `via invite, role=${result.role}`);
431
+ return withCors(req, Response.json(result, { status: result.ok ? 200 : 400 }));
432
+ }
433
+
368
434
  // ── V3: Admin APIs (require auth) ──
369
435
  if (url.pathname === "/api/users" && req.method === "GET") {
370
436
  const token = req.headers.get("Authorization")?.replace("Bearer ", "");
@@ -373,7 +439,7 @@ Bun.serve({
373
439
  if (!resolved || resolved.user.role !== "admin") {
374
440
  return withCors(req, Response.json({ ok: false, error: "admin required" }, { status: 403 }));
375
441
  }
376
- const users = db.query("SELECT user_id, username, display_name, email, role, created_at FROM users ORDER BY created_at").all();
442
+ const users = db.all("SELECT user_id, username, display_name, email, role, created_at FROM users ORDER BY created_at");
377
443
  return withCors(req, Response.json({ ok: true, users }));
378
444
  }
379
445
 
@@ -384,16 +450,16 @@ Bun.serve({
384
450
  const resolved = resolveToken(token);
385
451
  if (!resolved) return withCors(req, Response.json({ ok: false, error: "invalid token" }, { status: 401 }));
386
452
  const networkId = netDetailMatch[1];
387
- const network = db.query<any, [string]>("SELECT * FROM networks WHERE network_id = ?1").get(networkId);
453
+ const network = db.get<any>("SELECT * FROM networks WHERE network_id = ?1", networkId);
388
454
  if (!network) return withCors(req, Response.json({ ok: false, error: "network not found" }, { status: 404 }));
389
455
  // Ownership check: only owner or admin can view
390
456
  if (network.owner_id !== resolved.user.user_id && resolved.user.role !== "admin") {
391
457
  return withCors(req, Response.json({ ok: false, error: "access denied" }, { status: 403 }));
392
458
  }
393
459
  // Get network stats
394
- const nodeCount = db.query<{ cnt: number }, [string]>("SELECT COUNT(*) as cnt FROM nodes WHERE network_id = ?1").get(networkId);
395
- const sessionCount = db.query<{ cnt: number }, [string]>("SELECT COUNT(*) as cnt FROM sessions WHERE network_id = ?1").get(networkId);
396
- const taskStats = db.query<any, [string]>("SELECT status, COUNT(*) as count FROM tasks WHERE network_id = ?1 GROUP BY status").all(networkId);
460
+ const nodeCount = db.get<{ cnt: number }>("SELECT COUNT(*) as cnt FROM nodes WHERE network_id = ?1", networkId);
461
+ const sessionCount = db.get<{ cnt: number }>("SELECT COUNT(*) as cnt FROM sessions WHERE network_id = ?1", networkId);
462
+ const taskStats = db.all<any>("SELECT status, COUNT(*) as count FROM tasks WHERE network_id = ?1 GROUP BY status", networkId);
397
463
  return withCors(req, Response.json({
398
464
  ok: true, network,
399
465
  stats: { nodes: nodeCount?.cnt || 0, sessions: sessionCount?.cnt || 0, tasks: taskStats },
@@ -430,9 +496,9 @@ Bun.serve({
430
496
 
431
497
  // ── REST: health (public, no auth) ──
432
498
  if (url.pathname === "/health") {
433
- const count = db.query<{ cnt: number }, []>("SELECT COUNT(*) as cnt FROM sessions").get();
499
+ const count = db.get<{ cnt: number }>("SELECT COUNT(*) as cnt FROM sessions");
434
500
  const sse = getSSEStats();
435
- const license = db.query<any, []>("SELECT type, expires_at FROM licenses LIMIT 1").get();
501
+ const license = db.get<any>("SELECT type, expires_at FROM licenses LIMIT 1");
436
502
  return withCors(req, Response.json({
437
503
  ok: true,
438
504
  version: "1.0.0-preview",
@@ -461,7 +527,7 @@ Bun.serve({
461
527
  const sql = netFilter
462
528
  ? "SELECT * FROM sessions WHERE network_id = ?1 ORDER BY updated_at DESC"
463
529
  : "SELECT * FROM sessions ORDER BY updated_at DESC";
464
- const sessions = netFilter ? db.query(sql).all(netFilter) : db.query(sql).all();
530
+ const sessions = netFilter ? db.all(sql, netFilter) : db.all(sql);
465
531
  return withCors(req, Response.json({ ok: true, sessions }));
466
532
  }
467
533
 
@@ -486,9 +552,9 @@ Bun.serve({
486
552
  [id, body.alias, body.priority, body.task, fromSession]
487
553
  );
488
554
  // SSE push: 秒达
489
- const pending = db.query<{ cnt: number }, [string]>(
490
- "SELECT COUNT(*) as cnt FROM inbox WHERE session_name = ?1 AND acked = 0"
491
- ).get(body.alias);
555
+ const pending = db.get<{ cnt: number }>(
556
+ "SELECT COUNT(*) as cnt FROM inbox WHERE session_name = ?1 AND acked = 0",
557
+ body.alias);
492
558
  pushEvent(body.alias, { type: "new_task", inbox_count: pending?.cnt ?? 1, priority: body.priority, from: fromSession });
493
559
  return withCors(req, Response.json({ ok: true, message_id: id }));
494
560
  }
@@ -510,7 +576,7 @@ Bun.serve({
510
576
  const params: any[] = [];
511
577
  if (body.filter_server) { sql += " AND server = ?"; params.push(body.filter_server); }
512
578
  if (body.filter_status) { sql += " AND status = ?"; params.push(body.filter_status); }
513
- const targets = db.query<{ alias: string }, any[]>(sql).all(...params);
579
+ const targets = db.all<{ alias: string }>(sql, ...params);
514
580
  const ids: string[] = [];
515
581
  for (const t of targets) {
516
582
  const id = crypto.randomUUID();
@@ -577,9 +643,9 @@ Bun.serve({
577
643
  if (url.pathname === "/api/messages") {
578
644
  const limit = Number(url.searchParams.get("limit")) || 100;
579
645
  const since = url.searchParams.get("since") ?? new Date(Date.now() - 3600000).toISOString().replace("T", " ").slice(0, 19);
580
- const rows = db.query(
581
- "SELECT id, session_name as to_alias, from_session as from_alias, type, priority, content, created_at FROM inbox WHERE created_at >= ?1 ORDER BY created_at DESC LIMIT ?2"
582
- ).all(since, limit);
646
+ const rows = db.all(
647
+ "SELECT id, session_name as to_alias, from_session as from_alias, type, priority, content, created_at FROM inbox WHERE created_at >= ?1 ORDER BY created_at DESC LIMIT ?2",
648
+ since, limit);
583
649
  return withCors(req, Response.json({ ok: true, messages: rows }));
584
650
  }
585
651
 
@@ -588,20 +654,20 @@ Bun.serve({
588
654
  const n = url.searchParams.get("network_id");
589
655
  // Parameterized queries to prevent SQL injection
590
656
  const taskStats = n
591
- ? db.query<any, [string]>("SELECT status, COUNT(*) as count FROM tasks WHERE network_id = ?1 GROUP BY status").all(n)
592
- : db.query<any, []>("SELECT status, COUNT(*) as count FROM tasks GROUP BY status").all();
657
+ ? db.all<any>("SELECT status, COUNT(*) as count FROM tasks WHERE network_id = ?1 GROUP BY status", n)
658
+ : db.all<any>("SELECT status, COUNT(*) as count FROM tasks GROUP BY status");
593
659
  const sessionStats = n
594
- ? db.query<any, [string]>("SELECT status, COUNT(*) as count FROM sessions WHERE network_id = ?1 GROUP BY status").all(n)
595
- : db.query<any, []>("SELECT status, COUNT(*) as count FROM sessions GROUP BY status").all();
660
+ ? db.all<any>("SELECT status, COUNT(*) as count FROM sessions WHERE network_id = ?1 GROUP BY status", n)
661
+ : db.all<any>("SELECT status, COUNT(*) as count FROM sessions GROUP BY status");
596
662
  const totalTasks = n
597
- ? db.query<{ cnt: number }, [string]>("SELECT COUNT(*) as cnt FROM tasks WHERE network_id = ?1").get(n)
598
- : db.query<{ cnt: number }, []>("SELECT COUNT(*) as cnt FROM tasks").get();
663
+ ? db.get<{ cnt: number }>("SELECT COUNT(*) as cnt FROM tasks WHERE network_id = ?1", n)
664
+ : db.get<{ cnt: number }>("SELECT COUNT(*) as cnt FROM tasks");
599
665
  const totalNodes = n
600
- ? db.query<{ cnt: number }, [string]>("SELECT COUNT(*) as cnt FROM nodes WHERE network_id = ?1").get(n)
601
- : db.query<{ cnt: number }, []>("SELECT COUNT(*) as cnt FROM nodes").get();
666
+ ? db.get<{ cnt: number }>("SELECT COUNT(*) as cnt FROM nodes WHERE network_id = ?1", n)
667
+ : db.get<{ cnt: number }>("SELECT COUNT(*) as cnt FROM nodes");
602
668
  const recentTasks = n
603
- ? db.query<any, [string]>("SELECT task_id, from_name, to_name, status, created_at FROM tasks WHERE network_id = ?1 ORDER BY created_at DESC LIMIT 5").all(n)
604
- : db.query<any, []>("SELECT task_id, from_name, to_name, status, created_at FROM tasks ORDER BY created_at DESC LIMIT 5").all();
669
+ ? db.all<any>("SELECT task_id, from_name, to_name, status, created_at FROM tasks WHERE network_id = ?1 ORDER BY created_at DESC LIMIT 5", n)
670
+ : db.all<any>("SELECT task_id, from_name, to_name, status, created_at FROM tasks ORDER BY created_at DESC LIMIT 5");
605
671
  return withCors(req, Response.json({
606
672
  ok: true,
607
673
  network_id: n || null,
@@ -629,7 +695,7 @@ Bun.serve({
629
695
  if (userId && resolved.user.role === "admin") { sql += ` AND user_id = ?${params.length + 1}`; params.push(userId); }
630
696
  sql += ` ORDER BY created_at DESC LIMIT ?${params.length + 1}`;
631
697
  params.push(limit);
632
- const logs = db.query(sql).all(...params);
698
+ const logs = db.all(sql, ...params);
633
699
  return withCors(req, Response.json({ ok: true, logs, count: logs.length }));
634
700
  }
635
701
 
@@ -642,7 +708,7 @@ Bun.serve({
642
708
  if (taskId) { sql += " WHERE task_id = ?1"; params.push(taskId); }
643
709
  sql += " ORDER BY created_at DESC LIMIT ?";
644
710
  params.push(limit);
645
- const rows = db.query(sql).all(...params);
711
+ const rows = db.all(sql, ...params);
646
712
  return withCors(req, Response.json({ ok: true, events: rows, count: rows.length }));
647
713
  }
648
714
 
@@ -657,7 +723,7 @@ Bun.serve({
657
723
  if (nodeId) { sql += ` AND node_id = ?${params.length + 1}`; params.push(nodeId); }
658
724
  if (alias) { sql += ` AND alias = ?${params.length + 1}`; params.push(alias); }
659
725
  sql += " ORDER BY updated_at DESC";
660
- const rows = db.query(sql).all(...params);
726
+ const rows = db.all(sql, ...params);
661
727
  return withCors(req, Response.json({ ok: true, nodes: rows, count: rows.length }));
662
728
  }
663
729
 
@@ -680,17 +746,17 @@ Bun.serve({
680
746
  sql += ` ORDER BY created_at DESC LIMIT ?${params.length + 1}`;
681
747
  params.push(limit);
682
748
 
683
- const rows = db.query(sql).all(...params);
749
+ const rows = db.all(sql, ...params);
684
750
  const stats = netFilter
685
- ? db.query<any, [string]>("SELECT status, COUNT(*) as count FROM tasks WHERE network_id = ?1 GROUP BY status").all(netFilter)
686
- : db.query<any, []>("SELECT status, COUNT(*) as count FROM tasks GROUP BY status").all();
751
+ ? db.all<any>("SELECT status, COUNT(*) as count FROM tasks WHERE network_id = ?1 GROUP BY status", netFilter)
752
+ : db.all<any>("SELECT status, COUNT(*) as count FROM tasks GROUP BY status");
687
753
  return withCors(req, Response.json({ ok: true, tasks: rows, count: rows.length, stats }));
688
754
  }
689
755
 
690
756
  // ── REST: recent completions ──
691
757
  if (url.pathname === "/api/completions") {
692
758
  const since = url.searchParams.get("since") ?? new Date(Date.now() - 86400000).toISOString();
693
- const rows = db.query("SELECT * FROM completions WHERE completed_at >= ?1 ORDER BY completed_at DESC LIMIT 100").all(since);
759
+ const rows = db.all("SELECT * FROM completions WHERE completed_at >= ?1 ORDER BY completed_at DESC LIMIT 100", since);
694
760
  return withCors(req, Response.json({ ok: true, completions: rows }));
695
761
  }
696
762