@sleep2agi/commhub-server 0.7.0-preview.0 → 0.8.0-preview.0
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/auth.ts +49 -6
- package/src/index.ts +25 -11
- package/src/password-dict.ts +100 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sleep2agi/commhub-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0-preview.0",
|
|
4
4
|
"description": "CommHub Server — 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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* V3 Auth module — user registration, login, token management
|
|
3
3
|
*/
|
|
4
4
|
import { db, generateId, hashPassword, hashToken, generateToken, generateUserToken, generateNetworkToken, uuidv4 } from "./db.js";
|
|
5
|
+
import { WEAK_PASSWORDS } from "./password-dict.js";
|
|
5
6
|
|
|
6
7
|
export interface AuthUser {
|
|
7
8
|
user_id: string;
|
|
@@ -20,10 +21,17 @@ export interface AuthResult {
|
|
|
20
21
|
network_id?: string;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
function validatePasswordStrength(password: string, label = "password"): string | null {
|
|
25
|
+
if (!password || password.length < 8) return `${label} must be at least 8 characters`;
|
|
26
|
+
if (WEAK_PASSWORDS.has(password.toLowerCase())) return `${label} is too common`;
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
export function register(username: string, password: string, email?: string, displayName?: string): AuthResult {
|
|
24
31
|
if (!username || username.length < 2) return { ok: false, error: "username must be at least 2 characters" };
|
|
25
32
|
if (username.length > 50) return { ok: false, error: "username too long (max 50)" };
|
|
26
|
-
|
|
33
|
+
const passwordError = validatePasswordStrength(password);
|
|
34
|
+
if (passwordError) return { ok: false, error: passwordError };
|
|
27
35
|
if (!/^[a-zA-Z0-9_\-\u4e00-\u9fff]+$/.test(username)) return { ok: false, error: "username contains invalid characters" };
|
|
28
36
|
|
|
29
37
|
const existing = db.get<any>("SELECT user_id FROM users WHERE username = ?1", username);
|
|
@@ -126,10 +134,10 @@ export function createNetworkTokenForNode(userId: string, networkId: string, nod
|
|
|
126
134
|
return { ok: true, token };
|
|
127
135
|
}
|
|
128
136
|
|
|
129
|
-
export function resolveToken(token: string): { user: AuthUser; networkId: string | null; tokenName: string | null } | null {
|
|
137
|
+
export function resolveToken(token: string): { user: AuthUser; networkId: string | null; tokenName: string | null; tokenId: string | null } | null {
|
|
130
138
|
const tHash = hashToken(token);
|
|
131
139
|
const row = db.get<any>(
|
|
132
|
-
`SELECT t.user_id, t.network_id, t.scope, t.name AS token_name,
|
|
140
|
+
`SELECT t.token_id, t.user_id, t.network_id, t.scope, t.name AS token_name,
|
|
133
141
|
u.username, u.display_name, u.email, u.role
|
|
134
142
|
FROM api_tokens t JOIN users u ON t.user_id = u.user_id
|
|
135
143
|
WHERE t.token_hash = ?1 AND (t.expires_at IS NULL OR t.expires_at > datetime('now'))`,
|
|
@@ -143,6 +151,7 @@ export function resolveToken(token: string): { user: AuthUser; networkId: string
|
|
|
143
151
|
return {
|
|
144
152
|
user: { user_id: row.user_id, username: row.username, display_name: row.display_name, email: row.email, role: row.role },
|
|
145
153
|
networkId: row.network_id,
|
|
154
|
+
tokenId: row.token_id || null,
|
|
146
155
|
// tokenName carries the binding identity. For node-scoped ntok_, it's
|
|
147
156
|
// 'node:<alias>'; we strip the prefix and use it as the default
|
|
148
157
|
// from_session for any MCP send_task / send_message / etc, so peer
|
|
@@ -239,13 +248,47 @@ export function revokeToken(userId: string, tokenId: string): { ok: boolean; err
|
|
|
239
248
|
return result.changes > 0 ? { ok: true } : { ok: false, error: "token not found" };
|
|
240
249
|
}
|
|
241
250
|
|
|
242
|
-
export function
|
|
243
|
-
|
|
251
|
+
export function issueUserToken(userId: string, name = "user-login"): { token: string; token_id: string } {
|
|
252
|
+
const token = generateUserToken();
|
|
253
|
+
const tokenId = generateId("tok");
|
|
254
|
+
db.run(
|
|
255
|
+
"INSERT INTO api_tokens (token_id, token_hash, user_id, network_id, name, scope) VALUES (?1, ?2, ?3, NULL, ?4, 'user')",
|
|
256
|
+
[tokenId, hashToken(token), userId, name]
|
|
257
|
+
);
|
|
258
|
+
return { token, token_id: tokenId };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function revokeOtherUserTokens(userId: string, exceptTokenId?: string | null): number {
|
|
262
|
+
const result = exceptTokenId
|
|
263
|
+
? db.run("DELETE FROM api_tokens WHERE user_id = ?1 AND network_id IS NULL AND token_id != ?2", [userId, exceptTokenId])
|
|
264
|
+
: db.run("DELETE FROM api_tokens WHERE user_id = ?1 AND network_id IS NULL", [userId]);
|
|
265
|
+
return result.changes;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function changePassword(userId: string, oldPassword: string, newPassword: string, currentTokenId?: string | null): { ok: boolean; error?: string; revoked?: number } {
|
|
269
|
+
const passwordError = validatePasswordStrength(newPassword, "new password");
|
|
270
|
+
if (passwordError) return { ok: false, error: passwordError };
|
|
244
271
|
const user = db.get<any>("SELECT password_hash FROM users WHERE user_id = ?1", userId);
|
|
245
272
|
if (!user) return { ok: false, error: "user not found" };
|
|
246
273
|
if (user.password_hash !== hashPassword(oldPassword)) return { ok: false, error: "incorrect current password" };
|
|
247
274
|
db.run("UPDATE users SET password_hash = ?1, updated_at = datetime('now') WHERE user_id = ?2", [hashPassword(newPassword), userId]);
|
|
248
|
-
|
|
275
|
+
const revoked = revokeOtherUserTokens(userId, currentTokenId);
|
|
276
|
+
return { ok: true, revoked };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function resetUserPassword(targetUsername: string, callerIsHubAdmin: boolean): { ok: boolean; error?: string; username?: string; user_id?: string; password?: string; token?: string; token_id?: string; revoked?: number } {
|
|
280
|
+
if (!callerIsHubAdmin) return { ok: false, error: "hub admin required" };
|
|
281
|
+
const user = db.get<any>("SELECT user_id, username FROM users WHERE username = ?1", targetUsername);
|
|
282
|
+
if (!user) return { ok: false, error: "user not found" };
|
|
283
|
+
const password = `anet-${crypto.randomUUID().replace(/-/g, "").slice(0, 18)}`;
|
|
284
|
+
db.run("UPDATE users SET password_hash = ?1, updated_at = datetime('now') WHERE user_id = ?2", [hashPassword(password), user.user_id]);
|
|
285
|
+
const revoked = revokeOtherUserTokens(user.user_id, null);
|
|
286
|
+
const issued = issueUserToken(user.user_id, "admin-reset");
|
|
287
|
+
db.run(
|
|
288
|
+
"INSERT INTO audit_log (user_id, username, action, target_type, target_id, detail) VALUES (?1, ?2, 'password_reset_by_admin', 'user', ?3, ?4)",
|
|
289
|
+
[user.user_id, user.username, user.user_id, "local hub admin reset"]
|
|
290
|
+
);
|
|
291
|
+
return { ok: true, username: user.username, user_id: user.user_id, password, token: issued.token, token_id: issued.token_id, revoked };
|
|
249
292
|
}
|
|
250
293
|
|
|
251
294
|
// ══════════════════════════════════════
|
package/src/index.ts
CHANGED
|
@@ -4,24 +4,24 @@ 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, 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, createNetworkTokenForNode, type AuthUser } from "./auth.js";
|
|
7
|
+
import { register, login, resolveToken, getUserNetworks, getUserAllNetworks, createNetwork, deleteNetwork, renameNetwork, changePassword, issueUserToken, 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 HOST = process.env.HOST || "127.0.0.1";
|
|
11
11
|
const AUTH_TOKEN = process.env.COMMHUB_AUTH_TOKEN;
|
|
12
12
|
const DEV_OPEN = process.argv.includes("--dev-open") || process.env.COMMHUB_DEV_OPEN === "1";
|
|
13
13
|
const TMUX_ENABLED = process.env.COMMHUB_ENABLE_TMUX === "1";
|
|
14
|
-
const SECURITY_LABEL =
|
|
14
|
+
const SECURITY_LABEL = DEV_OPEN ? "⚠️ DEV OPEN MODE" : "🔒 secured";
|
|
15
15
|
const TMUX_ALLOWLIST = new Set(
|
|
16
16
|
(process.env.COMMHUB_TMUX_ALLOWLIST || "")
|
|
17
17
|
.split(",")
|
|
18
18
|
.map((s) => s.trim())
|
|
19
19
|
.filter(Boolean)
|
|
20
20
|
);
|
|
21
|
+
let masterTokenDeprecationLogged = false;
|
|
21
22
|
|
|
22
|
-
if (
|
|
23
|
-
console.
|
|
24
|
-
process.exit(1);
|
|
23
|
+
if (AUTH_TOKEN) {
|
|
24
|
+
console.warn("[commhub] COMMHUB_AUTH_TOKEN is deprecated and will be removed in v1.0. See RFC-001.");
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
// Read version from package.json so banners and /health stay in sync.
|
|
@@ -106,7 +106,16 @@ function requireAuth(req: Request): Response | null {
|
|
|
106
106
|
|
|
107
107
|
// Legacy: check global COMMHUB_AUTH_TOKEN
|
|
108
108
|
if (!AUTH_TOKEN && DEV_OPEN) return null; // explicit local/dev open mode
|
|
109
|
-
if (token === AUTH_TOKEN)
|
|
109
|
+
if (token === AUTH_TOKEN) {
|
|
110
|
+
const u = new URL(req.url);
|
|
111
|
+
const readOnlyApi = u.pathname.startsWith("/api/") && (req.method === "GET" || req.method === "HEAD" || req.method === "OPTIONS");
|
|
112
|
+
if (!readOnlyApi) return Response.json({ ok: false, error: "master-token auth is deprecated; use admin utok_" }, { status: 401 });
|
|
113
|
+
if (!masterTokenDeprecationLogged) {
|
|
114
|
+
console.warn("[commhub] master-token auth is deprecated and will be removed in v1.0. See RFC-001.");
|
|
115
|
+
masterTokenDeprecationLogged = true;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
110
119
|
|
|
111
120
|
return Response.json({ error: "unauthorized" }, { status: 401 });
|
|
112
121
|
}
|
|
@@ -472,9 +481,14 @@ Bun.serve({
|
|
|
472
481
|
if (!resolved) return withCors(req, Response.json({ ok: false, error: "invalid token" }, { status: 401 }));
|
|
473
482
|
try {
|
|
474
483
|
const body = await req.json() as any;
|
|
475
|
-
const result = changePassword(resolved.user.user_id, body.old_password, body.new_password);
|
|
476
|
-
if (result.ok)
|
|
477
|
-
|
|
484
|
+
const result = changePassword(resolved.user.user_id, body.old_password, body.new_password, resolved.tokenId);
|
|
485
|
+
if (result.ok) {
|
|
486
|
+
const issued = issueUserToken(resolved.user.user_id, "password-change");
|
|
487
|
+
if (resolved.tokenId) revokeToken(resolved.user.user_id, resolved.tokenId);
|
|
488
|
+
logAudit(resolved.user.user_id, resolved.user.username, "password_changed", "user", resolved.user.user_id);
|
|
489
|
+
return withCors(req, Response.json({ ...result, token: issued.token, token_id: issued.token_id }));
|
|
490
|
+
}
|
|
491
|
+
return withCors(req, Response.json(result, { status: 400 }));
|
|
478
492
|
} catch (e: any) {
|
|
479
493
|
return withCors(req, Response.json({ ok: false, error: e.message }, { status: 400 }));
|
|
480
494
|
}
|
|
@@ -706,8 +720,8 @@ Bun.serve({
|
|
|
706
720
|
sessions_count: count?.cnt ?? 0,
|
|
707
721
|
sse_connections: sse.total,
|
|
708
722
|
sse_sessions: sse.sessions,
|
|
709
|
-
auth:
|
|
710
|
-
security:
|
|
723
|
+
auth: DEV_OPEN ? "dev-open" : "user-token",
|
|
724
|
+
security: DEV_OPEN ? "dev-open" : "secured",
|
|
711
725
|
tmux: TMUX_ENABLED ? "enabled" : "disabled",
|
|
712
726
|
v3_auth: true,
|
|
713
727
|
multi_network: true,
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const WEAK_PASSWORDS_RAW = `
|
|
2
|
+
123456
|
|
3
|
+
password
|
|
4
|
+
123456789
|
|
5
|
+
12345
|
|
6
|
+
12345678
|
|
7
|
+
qwerty
|
|
8
|
+
1234567
|
|
9
|
+
111111
|
|
10
|
+
1234567890
|
|
11
|
+
123123
|
|
12
|
+
abc123
|
|
13
|
+
1234
|
|
14
|
+
password1
|
|
15
|
+
iloveyou
|
|
16
|
+
1q2w3e4r
|
|
17
|
+
000000
|
|
18
|
+
qwerty123
|
|
19
|
+
zaq12wsx
|
|
20
|
+
dragon
|
|
21
|
+
sunshine
|
|
22
|
+
princess
|
|
23
|
+
letmein
|
|
24
|
+
monkey
|
|
25
|
+
football
|
|
26
|
+
baseball
|
|
27
|
+
welcome
|
|
28
|
+
admin
|
|
29
|
+
login
|
|
30
|
+
master
|
|
31
|
+
hello
|
|
32
|
+
freedom
|
|
33
|
+
whatever
|
|
34
|
+
trustno1
|
|
35
|
+
qazwsx
|
|
36
|
+
654321
|
|
37
|
+
superman
|
|
38
|
+
batman
|
|
39
|
+
passw0rd
|
|
40
|
+
password123
|
|
41
|
+
asdfgh
|
|
42
|
+
zxcvbnm
|
|
43
|
+
qwertyuiop
|
|
44
|
+
1qaz2wsx
|
|
45
|
+
lovely
|
|
46
|
+
flower
|
|
47
|
+
hunter
|
|
48
|
+
shadow
|
|
49
|
+
buster
|
|
50
|
+
soccer
|
|
51
|
+
hockey
|
|
52
|
+
killer
|
|
53
|
+
george
|
|
54
|
+
charlie
|
|
55
|
+
andrew
|
|
56
|
+
michael
|
|
57
|
+
jessica
|
|
58
|
+
michelle
|
|
59
|
+
pepper
|
|
60
|
+
jordan
|
|
61
|
+
harley
|
|
62
|
+
ranger
|
|
63
|
+
ginger
|
|
64
|
+
joshua
|
|
65
|
+
maggie
|
|
66
|
+
mustang
|
|
67
|
+
computer
|
|
68
|
+
internet
|
|
69
|
+
secret
|
|
70
|
+
summer
|
|
71
|
+
winter
|
|
72
|
+
spring
|
|
73
|
+
autumn
|
|
74
|
+
orange
|
|
75
|
+
banana
|
|
76
|
+
cookie
|
|
77
|
+
coffee
|
|
78
|
+
matrix
|
|
79
|
+
starwars
|
|
80
|
+
pokemon
|
|
81
|
+
naruto
|
|
82
|
+
888888
|
|
83
|
+
666666
|
|
84
|
+
5201314
|
|
85
|
+
121212
|
|
86
|
+
112233
|
|
87
|
+
159753
|
|
88
|
+
987654321
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
const weak = new Set(WEAK_PASSWORDS_RAW.trim().split(/\s+/).map((p) => p.toLowerCase()));
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i <= 999; i++) {
|
|
94
|
+
weak.add(String(i).padStart(6, "0"));
|
|
95
|
+
weak.add(`password${i}`);
|
|
96
|
+
weak.add(`qwerty${i}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const WEAK_PASSWORDS = weak;
|
|
100
|
+
|