@sleep2agi/commhub-server 0.5.0-preview.15 → 0.5.0-preview.17

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.15",
3
+ "version": "0.5.0-preview.17",
4
4
  "description": "CommHub MCP Server — AI Agent communication hub with SSE push, MCP protocol, and REST API",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Database Adapter Interface — async-first, supports SQLite and PostgreSQL
3
+ *
4
+ * SQLite adapter: wraps bun:sqlite sync calls in Promise (zero overhead)
5
+ * PostgreSQL adapter: uses bun:sql native async
6
+ *
7
+ * All callers use await — sync SQLite just resolves immediately.
8
+ */
9
+
10
+ export interface QueryResult {
11
+ changes: number;
12
+ }
13
+
14
+ export interface DbAdapter {
15
+ /** Execute a write query (INSERT/UPDATE/DELETE) */
16
+ run(sql: string, params?: any[]): QueryResult;
17
+
18
+ /** Query a single row */
19
+ get<T = any>(sql: string, ...params: any[]): T | null;
20
+
21
+ /** Query multiple rows */
22
+ all<T = any>(sql: string, ...params: any[]): T[];
23
+
24
+ /** Execute raw SQL (DDL) */
25
+ exec(sql: string): void;
26
+
27
+ /** Run a function inside a transaction */
28
+ transaction<T>(fn: () => T): T;
29
+
30
+ /** Close connection */
31
+ close(): void;
32
+
33
+ /** Dialect identifier */
34
+ readonly dialect: 'sqlite' | 'postgres';
35
+ }
36
+
37
+ /**
38
+ * Phase 1 strategy:
39
+ *
40
+ * Current code is sync (bun:sqlite). We keep it sync for now.
41
+ * All DB access goes through the adapter interface above.
42
+ *
43
+ * When we add PostgreSQL (Phase 2), the adapter interface
44
+ * will change to async. At that point we'll update callers
45
+ * in a single pass. The unified call sites from Phase 1
46
+ * make that pass mechanical rather than archaeological.
47
+ *
48
+ * Why not async-first now?
49
+ * - bun:sqlite is sync, wrapping in Promise adds noise
50
+ * - All MCP tool handlers are already async, so the future
51
+ * migration is: db.run() → await db.run(), straightforward
52
+ * - 750+ lines of tools.ts would need gratuitous await for zero benefit today
53
+ *
54
+ * The contract: every DB call goes through adapter methods,
55
+ * never through raw db.query() or db.run() on the bun:sqlite object.
56
+ * This is what makes Phase 2 feasible.
57
+ */
58
+
59
+ /** SQL helpers for cross-dialect compatibility */
60
+ export function sqlNow(dialect: 'sqlite' | 'postgres'): string {
61
+ return dialect === 'postgres' ? 'NOW()' : "datetime('now')";
62
+ }
63
+
64
+ export function sqlAddSeconds(dialect: 'sqlite' | 'postgres', seconds: number | string): string {
65
+ return dialect === 'postgres'
66
+ ? `NOW() + INTERVAL '${seconds} seconds'`
67
+ : `datetime('now', '+${seconds} seconds')`;
68
+ }
69
+
70
+ export function sqlPlaceholder(dialect: 'sqlite' | 'postgres', index: number): string {
71
+ return dialect === 'postgres' ? `$${index}` : `?${index}`;
72
+ }
package/src/db.ts CHANGED
@@ -221,6 +221,33 @@ db.exec(`
221
221
  CREATE INDEX IF NOT EXISTS idx_audit_network ON audit_log(network_id);
222
222
  `);
223
223
 
224
+ // ── V3: licenses table ──
225
+ db.exec(`
226
+ CREATE TABLE IF NOT EXISTS licenses (
227
+ id TEXT PRIMARY KEY,
228
+ license_key TEXT UNIQUE NOT NULL,
229
+ type TEXT DEFAULT 'trial',
230
+ max_agents INTEGER DEFAULT 5,
231
+ max_networks INTEGER DEFAULT 3,
232
+ max_tasks_day INTEGER DEFAULT 500,
233
+ activated_at TEXT,
234
+ expires_at TEXT,
235
+ owner_id TEXT,
236
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
237
+ );
238
+ `);
239
+
240
+ // Auto-create trial license on first run
241
+ const existingLicense = db.query<any, []>("SELECT id FROM licenses LIMIT 1").get();
242
+ if (!existingLicense) {
243
+ const trialId = crypto.randomUUID().replace(/-/g, "").slice(0, 12);
244
+ db.run(
245
+ "INSERT INTO licenses (id, license_key, type, expires_at) VALUES (?1, ?2, 'trial', datetime('now', '+14 days'))",
246
+ [`lic_${trialId}`, `trial-${trialId}`]
247
+ );
248
+ console.log("[commhub] 🎉 14-day free trial started!");
249
+ }
250
+
224
251
  // ── V3: add network_id to existing tables ──
225
252
  for (const table of ["sessions", "nodes", "tasks", "inbox", "task_events"]) {
226
253
  try { db.exec(`ALTER TABLE ${table} ADD COLUMN network_id TEXT`); } catch {}
package/src/index.ts CHANGED
@@ -163,6 +163,44 @@ Bun.serve({
163
163
  return createSSEStream(sessionName);
164
164
  }
165
165
 
166
+ // ── V3: License endpoints ──
167
+ if (url.pathname === "/api/license" && req.method === "GET") {
168
+ const license = db.query<any, []>("SELECT * FROM licenses ORDER BY created_at LIMIT 1").get();
169
+ if (!license) return withCors(req, Response.json({ ok: true, status: "no_license" }));
170
+ const now = new Date().toISOString().replace("T", " ").slice(0, 19);
171
+ const expired = license.expires_at && license.expires_at < now;
172
+ const daysLeft = license.expires_at
173
+ ? Math.max(0, Math.ceil((new Date(license.expires_at).getTime() - Date.now()) / 86400000))
174
+ : null;
175
+ return withCors(req, Response.json({
176
+ ok: true,
177
+ license: { type: license.type, expires_at: license.expires_at, days_left: daysLeft, expired },
178
+ limits: { max_agents: license.max_agents, max_networks: license.max_networks, max_tasks_day: license.max_tasks_day },
179
+ }));
180
+ }
181
+
182
+ if (url.pathname === "/api/license/activate" && req.method === "POST") {
183
+ try {
184
+ const body = await req.json() as any;
185
+ const key = body.key;
186
+ if (!key) return withCors(req, Response.json({ ok: false, error: "key required" }, { status: 400 }));
187
+ // For now: accept any key starting with "anet-" as valid pro license
188
+ if (!key.startsWith("anet-") || key.length < 16) {
189
+ return withCors(req, Response.json({ ok: false, error: "invalid license key" }, { status: 400 }));
190
+ }
191
+ // Upgrade existing license or create new
192
+ db.run("DELETE FROM licenses");
193
+ const licId = `lic_${crypto.randomUUID().replace(/-/g, "").slice(0, 12)}`;
194
+ db.run(
195
+ "INSERT INTO licenses (id, license_key, type, max_agents, max_networks, max_tasks_day, activated_at, expires_at) VALUES (?1, ?2, 'pro', 50, 10, 10000, datetime('now'), datetime('now', '+365 days'))",
196
+ [licId, key]
197
+ );
198
+ return withCors(req, Response.json({ ok: true, type: "pro", expires_in_days: 365 }));
199
+ } catch (e: any) {
200
+ return withCors(req, Response.json({ ok: false, error: e.message }, { status: 400 }));
201
+ }
202
+ }
203
+
166
204
  // ── V3: Auth endpoints (public) ──
167
205
  if (url.pathname === "/api/auth/register" && req.method === "POST") {
168
206
  try {