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

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/commhub-server",
3
- "version": "0.5.0-preview.26",
3
+ "version": "0.5.0-preview.27",
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/auth.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * V3 Auth module — user registration, login, token management
3
3
  */
4
- import { db, generateId, hashPassword, hashToken, generateToken, uuidv4 } from "./db.js";
4
+ import { db, generateId, hashPassword, hashToken, generateToken, generateUserToken, generateNetworkToken, uuidv4 } from "./db.js";
5
5
 
6
6
  export interface AuthUser {
7
7
  user_id: string;
@@ -15,7 +15,9 @@ export interface AuthResult {
15
15
  ok: boolean;
16
16
  error?: string;
17
17
  user?: AuthUser;
18
- token?: string;
18
+ token?: string; // user token (utok_)
19
+ network_token?: string; // network token (ntok_) for default network
20
+ network_id?: string;
19
21
  }
20
22
 
21
23
  export function register(username: string, password: string, email?: string, displayName?: string): AuthResult {
@@ -26,12 +28,16 @@ export function register(username: string, password: string, email?: string, dis
26
28
  const existing = db.get<any>("SELECT user_id FROM users WHERE username = ?1", username);
27
29
  if (existing) return { ok: false, error: "username already taken" };
28
30
 
31
+ // First user → auto admin
32
+ const userCount = db.get<{ cnt: number }>("SELECT COUNT(*) as cnt FROM users");
33
+ const isFirstUser = !userCount || userCount.cnt === 0;
34
+
29
35
  const userId = generateId("u");
30
36
  const pwHash = hashPassword(password);
31
37
 
32
38
  db.run(
33
- "INSERT INTO users (user_id, username, password_hash, email, display_name) VALUES (?1, ?2, ?3, ?4, ?5)",
34
- [userId, username, pwHash, email || null, displayName || username]
39
+ "INSERT INTO users (user_id, username, password_hash, email, display_name, role) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
40
+ [userId, username, pwHash, email || null, displayName || username, isFirstUser ? "admin" : "user"]
35
41
  );
36
42
 
37
43
  // Auto-create default network + add as owner member
@@ -45,18 +51,28 @@ export function register(username: string, password: string, email?: string, dis
45
51
  [networkId, userId]
46
52
  );
47
53
 
48
- // Auto-create API token
49
- const token = generateToken();
50
- const tokenId = generateId("tok");
54
+ // User token (utok_) — not bound to network, for CLI/Dashboard login
55
+ const userToken = generateUserToken();
56
+ const userTokenId = generateId("tok");
51
57
  db.run(
52
58
  "INSERT INTO api_tokens (token_id, token_hash, user_id, network_id, name, scope) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
53
- [tokenId, hashToken(token), userId, networkId, "default", "full"]
59
+ [userTokenId, hashToken(userToken), userId, null, "user-login", "user"]
60
+ );
61
+
62
+ // Network token (ntok_) — bound to default network, for agent-node
63
+ const networkToken = generateNetworkToken();
64
+ const networkTokenId = generateId("tok");
65
+ db.run(
66
+ "INSERT INTO api_tokens (token_id, token_hash, user_id, network_id, name, scope) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
67
+ [networkTokenId, hashToken(networkToken), userId, networkId, "default-network", "network"]
54
68
  );
55
69
 
56
70
  return {
57
71
  ok: true,
58
- user: { user_id: userId, username, display_name: displayName || username, email: email || null, role: "user" },
59
- token,
72
+ user: { user_id: userId, username, display_name: displayName || username, email: email || null, role: isFirstUser ? "admin" : "user" },
73
+ token: userToken,
74
+ network_token: networkToken,
75
+ network_id: networkId,
60
76
  };
61
77
  }
62
78
 
@@ -68,36 +84,54 @@ export function login(username: string, password: string): AuthResult {
68
84
  if (!user) return { ok: false, error: "invalid username or password" };
69
85
  if (user.password_hash !== hashPassword(password)) return { ok: false, error: "invalid username or password" };
70
86
 
71
- // Find or create token
72
- let tokenRow = db.get<any>(
73
- "SELECT token_id FROM api_tokens WHERE user_id = ?1 ORDER BY created_at DESC LIMIT 1",
87
+ // Generate/rotate user token (utok_, not bound to network)
88
+ let userTokenRow = db.get<any>(
89
+ "SELECT token_id FROM api_tokens WHERE user_id = ?1 AND scope = 'user' ORDER BY created_at DESC LIMIT 1",
74
90
  user.user_id);
75
91
 
76
- let token: string;
77
- if (tokenRow) {
78
- // Generate new token (rotate)
79
- token = generateToken();
92
+ const userToken = generateUserToken();
93
+ if (userTokenRow) {
80
94
  db.run("UPDATE api_tokens SET token_hash = ?1, last_used_at = datetime('now') WHERE token_id = ?2",
81
- [hashToken(token), tokenRow.token_id]);
95
+ [hashToken(userToken), userTokenRow.token_id]);
82
96
  } else {
83
- token = generateToken();
84
97
  const tokenId = generateId("tok");
85
- const networkId = db.get<any>(
86
- "SELECT network_id FROM networks WHERE owner_id = ?1 LIMIT 1",
87
- user.user_id)?.network_id;
88
98
  db.run(
89
- "INSERT INTO api_tokens (token_id, token_hash, user_id, network_id, name) VALUES (?1, ?2, ?3, ?4, ?5)",
90
- [tokenId, hashToken(token), user.user_id, networkId || null, "login"]
99
+ "INSERT INTO api_tokens (token_id, token_hash, user_id, network_id, name, scope) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
100
+ [tokenId, hashToken(userToken), user.user_id, null, "user-login", "user"]
91
101
  );
92
102
  }
93
103
 
104
+ // Find default network
105
+ const defaultNet = db.get<any>(
106
+ "SELECT network_id FROM network_members WHERE user_id = ?1 ORDER BY role = 'owner' DESC LIMIT 1",
107
+ user.user_id);
108
+ const networkId = defaultNet?.network_id || null;
109
+
110
+ // Backward compat: also try old atok_ tokens
111
+ const token = userToken;
112
+
94
113
  return {
95
114
  ok: true,
96
115
  user: { user_id: user.user_id, username: user.username, display_name: user.display_name, email: user.email, role: user.role },
97
116
  token,
117
+ network_id: networkId,
98
118
  };
99
119
  }
100
120
 
121
+ /** Create a network-scoped token (ntok_) for a specific node */
122
+ export function createNetworkTokenForNode(userId: string, networkId: string, nodeName: string): { ok: boolean; token?: string; error?: string } {
123
+ // Verify user is a member of this network with write access
124
+ const role = getUserNetworkRole(userId, networkId);
125
+ if (!role || role === "viewer") return { ok: false, error: "no write access to this network" };
126
+ const token = generateNetworkToken();
127
+ const tokenId = generateId("tok");
128
+ db.run(
129
+ "INSERT INTO api_tokens (token_id, token_hash, user_id, network_id, name, scope) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
130
+ [tokenId, hashToken(token), userId, networkId, `node:${nodeName}`, "network"]
131
+ );
132
+ return { ok: true, token };
133
+ }
134
+
101
135
  export function resolveToken(token: string): { user: AuthUser; networkId: string | null } | null {
102
136
  const tHash = hashToken(token);
103
137
  const row = db.get<any>(
package/src/db.ts CHANGED
@@ -324,6 +324,14 @@ export function generateToken(): string {
324
324
  return `atok_${crypto.randomUUID().replace(/-/g, "")}`;
325
325
  }
326
326
 
327
+ export function generateUserToken(): string {
328
+ return `utok_${crypto.randomUUID().replace(/-/g, "")}`;
329
+ }
330
+
331
+ export function generateNetworkToken(): string {
332
+ return `ntok_${crypto.randomUUID().replace(/-/g, "")}`;
333
+ }
334
+
327
335
  export function logAudit(userId: string | null, username: string | null, action: string, targetType?: string, targetId?: string, detail?: string, ip?: string, networkId?: string) {
328
336
  try {
329
337
  db.run(
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, getUserAllNetworks, createNetwork, deleteNetwork, renameNetwork, changePassword, listTokens, createToken, revokeToken, getNetworkMembers, getUserNetworkRole, addNetworkMember, updateMemberRole, removeNetworkMember, createInvite, joinByInvite, 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, createNetworkTokenForNode, 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;
@@ -304,6 +304,23 @@ Bun.serve({
304
304
  }
305
305
  }
306
306
 
307
+ // ── V3.13: Create network token for a node ──
308
+ if (url.pathname === "/api/auth/node-token" && req.method === "POST") {
309
+ const token = req.headers.get("Authorization")?.replace("Bearer ", "");
310
+ if (!token) return withCors(req, Response.json({ ok: false, error: "auth required" }, { status: 401 }));
311
+ const resolved = resolveToken(token);
312
+ if (!resolved) return withCors(req, Response.json({ ok: false, error: "invalid token" }, { status: 401 }));
313
+ try {
314
+ const body = await req.json() as any;
315
+ if (!body.network_id || !body.node_name) return withCors(req, Response.json({ ok: false, error: "network_id and node_name required" }, { status: 400 }));
316
+ const result = createNetworkTokenForNode(resolved.user.user_id, body.network_id, body.node_name);
317
+ if (result.ok) logAudit(resolved.user.user_id, resolved.user.username, "node_token_created", "network", body.network_id, body.node_name);
318
+ return withCors(req, Response.json(result, { status: result.ok ? 200 : 400 }));
319
+ } catch (e: any) {
320
+ return withCors(req, Response.json({ ok: false, error: e.message }, { status: 400 }));
321
+ }
322
+ }
323
+
307
324
  // ── V3: Token management ──
308
325
  if (url.pathname === "/api/auth/tokens" && req.method === "GET") {
309
326
  const token = req.headers.get("Authorization")?.replace("Bearer ", "");