@sleep2agi/commhub-server 0.5.0-preview.20 → 0.5.0-preview.22
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 +32 -0
- package/src/index.ts +47 -1
package/package.json
CHANGED
package/src/auth.ts
CHANGED
|
@@ -133,6 +133,38 @@ export function createNetwork(userId: string, name: string, description?: string
|
|
|
133
133
|
return { ok: true, network_id: networkId, network_name: name };
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
export function listTokens(userId: string) {
|
|
137
|
+
return db.query<any, [string]>(
|
|
138
|
+
"SELECT token_id, name, scope, network_id, last_used_at, created_at FROM api_tokens WHERE user_id = ?1 ORDER BY created_at DESC"
|
|
139
|
+
).all(userId);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function deleteNetwork(userId: string, networkId: string): { ok: boolean; error?: string } {
|
|
143
|
+
const net = db.query<any, [string]>("SELECT * FROM networks WHERE network_id = ?1").get(networkId);
|
|
144
|
+
if (!net) return { ok: false, error: "network not found" };
|
|
145
|
+
if (net.owner_id !== userId) return { ok: false, error: "not your network" };
|
|
146
|
+
// Check if any sessions/tasks still reference this network
|
|
147
|
+
const sessions = db.query<{ cnt: number }, [string]>("SELECT COUNT(*) as cnt FROM sessions WHERE network_id = ?1").get(networkId);
|
|
148
|
+
if (sessions && sessions.cnt > 0) return { ok: false, error: `network has ${sessions.cnt} active session(s) — stop them first` };
|
|
149
|
+
db.run("DELETE FROM networks WHERE network_id = ?1 AND owner_id = ?2", [networkId, userId]);
|
|
150
|
+
return { ok: true };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function createToken(userId: string, name: string, networkId?: string): { ok: boolean; token?: string; token_id?: string; error?: string } {
|
|
154
|
+
const token = generateToken();
|
|
155
|
+
const tokenId = generateId("tok");
|
|
156
|
+
db.run(
|
|
157
|
+
"INSERT INTO api_tokens (token_id, token_hash, user_id, network_id, name, scope) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
|
158
|
+
[tokenId, hashToken(token), userId, networkId || null, name, "full"]
|
|
159
|
+
);
|
|
160
|
+
return { ok: true, token, token_id: tokenId };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function revokeToken(userId: string, tokenId: string): { ok: boolean; error?: string } {
|
|
164
|
+
const result = db.run("DELETE FROM api_tokens WHERE token_id = ?1 AND user_id = ?2", [tokenId, userId]);
|
|
165
|
+
return result.changes > 0 ? { ok: true } : { ok: false, error: "token not found" };
|
|
166
|
+
}
|
|
167
|
+
|
|
136
168
|
export function changePassword(userId: string, oldPassword: string, newPassword: string): { ok: boolean; error?: string } {
|
|
137
169
|
if (!newPassword || newPassword.length < 6) return { ok: false, error: "new password must be at least 6 characters" };
|
|
138
170
|
const user = db.query<any, [string]>("SELECT password_hash FROM users WHERE user_id = ?1").get(userId);
|
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, changePassword, type AuthUser } from "./auth.js";
|
|
7
|
+
import { register, login, resolveToken, getUserNetworks, createNetwork, deleteNetwork, changePassword, listTokens, createToken, revokeToken, 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;
|
|
@@ -305,6 +305,42 @@ Bun.serve({
|
|
|
305
305
|
}
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
// ── V3: Token management ──
|
|
309
|
+
if (url.pathname === "/api/auth/tokens" && req.method === "GET") {
|
|
310
|
+
const token = req.headers.get("Authorization")?.replace("Bearer ", "");
|
|
311
|
+
if (!token) return withCors(req, Response.json({ ok: false, error: "auth required" }, { status: 401 }));
|
|
312
|
+
const resolved = resolveToken(token);
|
|
313
|
+
if (!resolved) return withCors(req, Response.json({ ok: false, error: "invalid token" }, { status: 401 }));
|
|
314
|
+
const tokens = listTokens(resolved.user.user_id);
|
|
315
|
+
return withCors(req, Response.json({ ok: true, tokens }));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (url.pathname === "/api/auth/tokens" && req.method === "POST") {
|
|
319
|
+
const token = req.headers.get("Authorization")?.replace("Bearer ", "");
|
|
320
|
+
if (!token) return withCors(req, Response.json({ ok: false, error: "auth required" }, { status: 401 }));
|
|
321
|
+
const resolved = resolveToken(token);
|
|
322
|
+
if (!resolved) return withCors(req, Response.json({ ok: false, error: "invalid token" }, { status: 401 }));
|
|
323
|
+
try {
|
|
324
|
+
const body = await req.json() as any;
|
|
325
|
+
const result = createToken(resolved.user.user_id, body.name || "api-token", body.network_id);
|
|
326
|
+
if (result.ok) logAudit(resolved.user.user_id, resolved.user.username, "token_created", "token", result.token_id);
|
|
327
|
+
return withCors(req, Response.json(result));
|
|
328
|
+
} catch (e: any) {
|
|
329
|
+
return withCors(req, Response.json({ ok: false, error: e.message }, { status: 400 }));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const tokenDeleteMatch = url.pathname.match(/^\/api\/auth\/tokens\/([^/]+)$/);
|
|
334
|
+
if (tokenDeleteMatch && req.method === "DELETE") {
|
|
335
|
+
const token = req.headers.get("Authorization")?.replace("Bearer ", "");
|
|
336
|
+
if (!token) return withCors(req, Response.json({ ok: false, error: "auth required" }, { status: 401 }));
|
|
337
|
+
const resolved = resolveToken(token);
|
|
338
|
+
if (!resolved) return withCors(req, Response.json({ ok: false, error: "invalid token" }, { status: 401 }));
|
|
339
|
+
const result = revokeToken(resolved.user.user_id, tokenDeleteMatch[1]);
|
|
340
|
+
if (result.ok) logAudit(resolved.user.user_id, resolved.user.username, "token_revoked", "token", tokenDeleteMatch[1]);
|
|
341
|
+
return withCors(req, Response.json(result, { status: result.ok ? 200 : 404 }));
|
|
342
|
+
}
|
|
343
|
+
|
|
308
344
|
// ── V3: Network management ──
|
|
309
345
|
if (url.pathname === "/api/networks" && req.method === "GET") {
|
|
310
346
|
const token = req.headers.get("Authorization")?.replace("Bearer ", "") || url.searchParams.get("token");
|
|
@@ -364,6 +400,16 @@ Bun.serve({
|
|
|
364
400
|
}));
|
|
365
401
|
}
|
|
366
402
|
|
|
403
|
+
if (netDetailMatch && req.method === "DELETE") {
|
|
404
|
+
const token = req.headers.get("Authorization")?.replace("Bearer ", "") || url.searchParams.get("token");
|
|
405
|
+
if (!token) return withCors(req, Response.json({ ok: false, error: "auth required" }, { status: 401 }));
|
|
406
|
+
const resolved = resolveToken(token);
|
|
407
|
+
if (!resolved) return withCors(req, Response.json({ ok: false, error: "invalid token" }, { status: 401 }));
|
|
408
|
+
const result = deleteNetwork(resolved.user.user_id, netDetailMatch[1]);
|
|
409
|
+
if (result.ok) logAudit(resolved.user.user_id, resolved.user.username, "network_deleted", "network", netDetailMatch[1]);
|
|
410
|
+
return withCors(req, Response.json(result, { status: result.ok ? 200 : 400 }));
|
|
411
|
+
}
|
|
412
|
+
|
|
367
413
|
// ── REST: health (public, no auth) ──
|
|
368
414
|
if (url.pathname === "/health") {
|
|
369
415
|
const count = db.query<{ cnt: number }, []>("SELECT COUNT(*) as cnt FROM sessions").get();
|