@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/README.md +20 -3
- package/package.json +7 -6
- package/src/auth.ts +126 -28
- package/src/db-adapter.ts +198 -32
- package/src/db.ts +58 -11
- package/src/index.ts +105 -39
- package/src/tools.ts +80 -130
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
395
|
-
const sessionCount = db.
|
|
396
|
-
const taskStats = db.
|
|
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.
|
|
499
|
+
const count = db.get<{ cnt: number }>("SELECT COUNT(*) as cnt FROM sessions");
|
|
434
500
|
const sse = getSSEStats();
|
|
435
|
-
const license = db.
|
|
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.
|
|
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.
|
|
490
|
-
"SELECT COUNT(*) as cnt FROM inbox WHERE session_name = ?1 AND acked = 0"
|
|
491
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
592
|
-
: db.
|
|
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.
|
|
595
|
-
: db.
|
|
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.
|
|
598
|
-
: db.
|
|
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.
|
|
601
|
-
: db.
|
|
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.
|
|
604
|
-
: db.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
749
|
+
const rows = db.all(sql, ...params);
|
|
684
750
|
const stats = netFilter
|
|
685
|
-
? db.
|
|
686
|
-
: db.
|
|
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.
|
|
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
|
|