@stackmemoryai/stackmemory 0.3.20 → 0.3.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.
Files changed (35) hide show
  1. package/dist/cli/claude-sm.js +1 -3
  2. package/dist/cli/claude-sm.js.map +2 -2
  3. package/dist/cli/codex-sm.js.map +1 -1
  4. package/dist/cli/commands/linear-unified.js +2 -3
  5. package/dist/cli/commands/linear-unified.js.map +2 -2
  6. package/dist/cli/commands/signup.js +46 -0
  7. package/dist/cli/commands/signup.js.map +7 -0
  8. package/dist/cli/commands/tasks.js +1 -1
  9. package/dist/cli/commands/tasks.js.map +2 -2
  10. package/dist/cli/index.js +3 -1
  11. package/dist/cli/index.js.map +2 -2
  12. package/dist/integrations/mcp/handlers/code-execution-handlers.js +262 -0
  13. package/dist/integrations/mcp/handlers/code-execution-handlers.js.map +7 -0
  14. package/dist/integrations/mcp/tool-definitions-code.js +121 -0
  15. package/dist/integrations/mcp/tool-definitions-code.js.map +7 -0
  16. package/dist/servers/railway/index.js +283 -93
  17. package/dist/servers/railway/index.js.map +3 -3
  18. package/package.json +2 -3
  19. package/scripts/claude-sm-autostart.js +1 -1
  20. package/scripts/clean-linear-backlog.js +2 -2
  21. package/scripts/debug-linear-update.js +1 -1
  22. package/scripts/debug-railway-build.js +87 -0
  23. package/scripts/delete-linear-tasks.js +2 -2
  24. package/scripts/install-code-execution-hooks.sh +96 -0
  25. package/scripts/linear-task-review.js +1 -1
  26. package/scripts/sync-and-clean-tasks.js +1 -1
  27. package/scripts/sync-linear-graphql.js +3 -3
  28. package/scripts/sync-linear-tasks.js +1 -1
  29. package/scripts/test-code-execution.js +143 -0
  30. package/scripts/update-linear-tasks-fixed.js +1 -1
  31. package/scripts/validate-railway-deployment.js +137 -0
  32. package/templates/claude-hooks/hook-config.json +59 -0
  33. package/templates/claude-hooks/pre-tool-use +189 -0
  34. package/dist/servers/railway/minimal.js +0 -91
  35. package/dist/servers/railway/minimal.js.map +0 -7
@@ -83,8 +83,9 @@ class RailwayMCPServer {
83
83
  await this.pgPool.query(`
84
84
  CREATE TABLE IF NOT EXISTS users (
85
85
  id TEXT PRIMARY KEY,
86
- email TEXT,
86
+ email TEXT UNIQUE,
87
87
  name TEXT,
88
+ password_hash TEXT,
88
89
  tier TEXT DEFAULT 'free',
89
90
  role TEXT DEFAULT 'user',
90
91
  created_at TIMESTAMPTZ DEFAULT NOW(),
@@ -95,6 +96,10 @@ class RailwayMCPServer {
95
96
  await this.pgPool.query(`ALTER TABLE users ADD COLUMN role TEXT DEFAULT 'user'`);
96
97
  } catch {
97
98
  }
99
+ try {
100
+ await this.pgPool.query(`ALTER TABLE users ADD COLUMN password_hash TEXT`);
101
+ } catch {
102
+ }
98
103
  try {
99
104
  await this.pgPool.query(`ALTER TABLE project_members ADD CONSTRAINT project_members_role_check CHECK (role IN ('admin','owner','editor','viewer'))`);
100
105
  } catch {
@@ -183,8 +188,9 @@ class RailwayMCPServer {
183
188
 
184
189
  CREATE TABLE IF NOT EXISTS users (
185
190
  id TEXT PRIMARY KEY,
186
- email TEXT,
191
+ email TEXT UNIQUE,
187
192
  name TEXT,
193
+ password_hash TEXT,
188
194
  tier TEXT DEFAULT 'free',
189
195
  role TEXT DEFAULT 'user',
190
196
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
@@ -242,7 +248,7 @@ class RailwayMCPServer {
242
248
  for (const q of queries) {
243
249
  try {
244
250
  await this.pgPool.query(q);
245
- } catch (e2) {
251
+ } catch (e) {
246
252
  }
247
253
  }
248
254
  await this.pgPool.query("INSERT INTO railway_schema_version (version, description) VALUES ($1, $2) ON CONFLICT (version) DO NOTHING", [version, description]);
@@ -271,6 +277,10 @@ class RailwayMCPServer {
271
277
  `ALTER TABLE project_members ADD CONSTRAINT project_members_role_check CHECK (role IN ('admin','owner','editor','viewer'))`,
272
278
  `ALTER TABLE users ADD CONSTRAINT users_role_check CHECK (role IN ('admin','user'))`
273
279
  ]);
280
+ await apply(4, "password authentication", [
281
+ `ALTER TABLE users ADD COLUMN IF NOT EXISTS password_hash TEXT`,
282
+ `ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE (email)`
283
+ ]);
274
284
  } else {
275
285
  this.db.exec(`CREATE TABLE IF NOT EXISTS railway_schema_version (version INTEGER PRIMARY KEY, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP, description TEXT)`);
276
286
  const row = this.db.prepare("SELECT COALESCE(MAX(version), 0) AS v FROM railway_schema_version").get();
@@ -307,6 +317,9 @@ class RailwayMCPServer {
307
317
  `CREATE TABLE IF NOT EXISTS admin_sessions (id TEXT PRIMARY KEY, user_id TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, expires_at DATETIME NOT NULL, user_agent TEXT, ip TEXT)`,
308
318
  `CREATE INDEX IF NOT EXISTS idx_admin_sessions_user ON admin_sessions(user_id)`
309
319
  ]);
320
+ apply(3, "password authentication", [
321
+ `ALTER TABLE users ADD COLUMN password_hash TEXT`
322
+ ]);
310
323
  }
311
324
  }
312
325
  // TTL cleanup for admin sessions
@@ -318,10 +331,13 @@ class RailwayMCPServer {
318
331
  if (this.pgPool) {
319
332
  await this.pgPool.query("DELETE FROM admin_sessions WHERE expires_at <= NOW()");
320
333
  } else if (this.db) {
321
- this.db.prepare('DELETE FROM admin_sessions WHERE datetime(expires_at) <= datetime("now")').run();
334
+ const tableExists = this.db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='admin_sessions'`).get();
335
+ if (tableExists) {
336
+ this.db.prepare('DELETE FROM admin_sessions WHERE datetime(expires_at) <= datetime("now")').run();
337
+ }
322
338
  }
323
- } catch {
324
- console.warn("Admin session cleanup failed:", e);
339
+ } catch (error) {
340
+ console.warn("Admin session cleanup failed:", error);
325
341
  }
326
342
  };
327
343
  run();
@@ -346,7 +362,7 @@ class RailwayMCPServer {
346
362
  }
347
363
  }
348
364
  async authenticate(req, res, next) {
349
- if (req.path === "/health" || req.path === "/health/db") {
365
+ if (req.path === "/health" || req.path === "/api/health" || req.path === "/health/db") {
350
366
  return next();
351
367
  }
352
368
  const authHeader = req.headers.authorization;
@@ -362,8 +378,8 @@ class RailwayMCPServer {
362
378
  }
363
379
  req.user = valid;
364
380
  return next();
365
- } catch (e2) {
366
- return res.status(500).json({ error: e2.message || "Auth error" });
381
+ } catch (e) {
382
+ return res.status(500).json({ error: e.message || "Auth error" });
367
383
  }
368
384
  }
369
385
  if (config.authMode === "jwt" && process.env["AUTH0_DOMAIN"]) {
@@ -435,7 +451,7 @@ class RailwayMCPServer {
435
451
  next();
436
452
  }
437
453
  setupRoutes() {
438
- this.app.get("/health", (req, res) => {
454
+ const healthHandler = (req, res) => {
439
455
  const health = {
440
456
  status: "healthy",
441
457
  version: "1.0.0",
@@ -444,7 +460,9 @@ class RailwayMCPServer {
444
460
  environment: config.environment
445
461
  };
446
462
  res.json(health);
447
- });
463
+ };
464
+ this.app.get("/health", healthHandler);
465
+ this.app.get("/api/health", healthHandler);
448
466
  this.app.get("/", (req, res) => {
449
467
  res.json({
450
468
  name: "StackMemory Railway Server",
@@ -457,6 +475,176 @@ class RailwayMCPServer {
457
475
  }
458
476
  });
459
477
  });
478
+ this.app.post("/auth/signup", async (req, res) => {
479
+ try {
480
+ const { email, password, name } = req.body;
481
+ if (!email || !password) {
482
+ return res.status(400).json({
483
+ success: false,
484
+ error: "Email and password are required"
485
+ });
486
+ }
487
+ if (this.pgPool) {
488
+ const existingUser = await this.pgPool.query(
489
+ "SELECT id FROM users WHERE email = $1",
490
+ [email]
491
+ );
492
+ if (existingUser.rowCount > 0) {
493
+ return res.status(409).json({
494
+ success: false,
495
+ error: "User already exists"
496
+ });
497
+ }
498
+ } else {
499
+ const existingUser = this.db.prepare("SELECT id FROM users WHERE email = ?").get(email);
500
+ if (existingUser) {
501
+ return res.status(409).json({
502
+ success: false,
503
+ error: "User already exists"
504
+ });
505
+ }
506
+ }
507
+ const passwordHash = await bcrypt.hash(password, 10);
508
+ const userId = `user_${Date.now()}_${Math.random().toString(36).substring(7)}`;
509
+ if (this.pgPool) {
510
+ await this.pgPool.query(
511
+ "INSERT INTO users (id, email, name, password_hash, tier, role) VALUES ($1, $2, $3, $4, $5, $6)",
512
+ [userId, email, name || email.split("@")[0], passwordHash, "free", "user"]
513
+ );
514
+ } else {
515
+ this.db.prepare(
516
+ "INSERT INTO users (id, email, name, password_hash, tier, role) VALUES (?, ?, ?, ?, ?, ?)"
517
+ ).run(userId, email, name || email.split("@")[0], passwordHash, "free", "user");
518
+ }
519
+ const apiKey = `sk_${Math.random().toString(36).substring(2)}${Math.random().toString(36).substring(2)}`;
520
+ const apiKeyHash = await bcrypt.hash(apiKey, 10);
521
+ if (this.pgPool) {
522
+ await this.pgPool.query(
523
+ "INSERT INTO api_keys (key_hash, user_id, name) VALUES ($1, $2, $3)",
524
+ [apiKeyHash, userId, "Default API Key"]
525
+ );
526
+ } else {
527
+ this.db.prepare(
528
+ "INSERT INTO api_keys (key_hash, user_id, name) VALUES (?, ?, ?)"
529
+ ).run(apiKeyHash, userId, "Default API Key");
530
+ }
531
+ const token = jwt.sign(
532
+ { sub: userId, email, role: "user" },
533
+ config.jwtSecret,
534
+ { expiresIn: "30d" }
535
+ );
536
+ res.json({
537
+ success: true,
538
+ apiKey,
539
+ token,
540
+ email,
541
+ userId,
542
+ message: "Account created successfully"
543
+ });
544
+ } catch (error) {
545
+ console.error("Signup error:", error);
546
+ res.status(500).json({
547
+ success: false,
548
+ error: "Failed to create account"
549
+ });
550
+ }
551
+ });
552
+ this.app.post("/auth/login", async (req, res) => {
553
+ try {
554
+ const { email, password } = req.body;
555
+ if (!email || !password) {
556
+ return res.status(400).json({
557
+ success: false,
558
+ error: "Email and password are required"
559
+ });
560
+ }
561
+ let user = null;
562
+ if (this.pgPool) {
563
+ const result = await this.pgPool.query(
564
+ "SELECT id, email, name, password_hash, tier, role FROM users WHERE email = $1",
565
+ [email]
566
+ );
567
+ user = result.rows[0];
568
+ } else {
569
+ user = this.db.prepare(
570
+ "SELECT id, email, name, password_hash, tier, role FROM users WHERE email = ?"
571
+ ).get(email);
572
+ }
573
+ if (!user) {
574
+ return res.status(401).json({
575
+ success: false,
576
+ error: "Invalid credentials"
577
+ });
578
+ }
579
+ const validPassword = await bcrypt.compare(password, user.password_hash);
580
+ if (!validPassword) {
581
+ return res.status(401).json({
582
+ success: false,
583
+ error: "Invalid credentials"
584
+ });
585
+ }
586
+ let apiKey = null;
587
+ if (this.pgPool) {
588
+ const keyResult = await this.pgPool.query(
589
+ "SELECT id FROM api_keys WHERE user_id = $1 AND revoked = false LIMIT 1",
590
+ [user.id]
591
+ );
592
+ if (keyResult.rowCount === 0) {
593
+ apiKey = `sk_${Math.random().toString(36).substring(2)}${Math.random().toString(36).substring(2)}`;
594
+ const apiKeyHash = await bcrypt.hash(apiKey, 10);
595
+ await this.pgPool.query(
596
+ "INSERT INTO api_keys (key_hash, user_id, name) VALUES ($1, $2, $3)",
597
+ [apiKeyHash, user.id, "Default API Key"]
598
+ );
599
+ } else {
600
+ apiKey = `sk_${Math.random().toString(36).substring(2)}${Math.random().toString(36).substring(2)}`;
601
+ const apiKeyHash = await bcrypt.hash(apiKey, 10);
602
+ await this.pgPool.query(
603
+ "UPDATE api_keys SET key_hash = $1, last_used = NOW() WHERE id = $2",
604
+ [apiKeyHash, keyResult.rows[0].id]
605
+ );
606
+ }
607
+ } else {
608
+ const keyRow = this.db.prepare(
609
+ "SELECT id FROM api_keys WHERE user_id = ? AND revoked = 0 LIMIT 1"
610
+ ).get(user.id);
611
+ if (!keyRow) {
612
+ apiKey = `sk_${Math.random().toString(36).substring(2)}${Math.random().toString(36).substring(2)}`;
613
+ const apiKeyHash = await bcrypt.hash(apiKey, 10);
614
+ this.db.prepare(
615
+ "INSERT INTO api_keys (key_hash, user_id, name) VALUES (?, ?, ?)"
616
+ ).run(apiKeyHash, user.id, "Default API Key");
617
+ } else {
618
+ apiKey = `sk_${Math.random().toString(36).substring(2)}${Math.random().toString(36).substring(2)}`;
619
+ const apiKeyHash = await bcrypt.hash(apiKey, 10);
620
+ this.db.prepare(
621
+ "UPDATE api_keys SET key_hash = ?, last_used = CURRENT_TIMESTAMP WHERE id = ?"
622
+ ).run(apiKeyHash, keyRow.id);
623
+ }
624
+ }
625
+ const token = jwt.sign(
626
+ { sub: user.id, email: user.email, role: user.role },
627
+ config.jwtSecret,
628
+ { expiresIn: "30d" }
629
+ );
630
+ res.json({
631
+ success: true,
632
+ apiKey,
633
+ token,
634
+ email: user.email,
635
+ userId: user.id,
636
+ databaseUrl: process.env.DATABASE_URL,
637
+ // For client configuration
638
+ message: "Login successful"
639
+ });
640
+ } catch (error) {
641
+ console.error("Login error:", error);
642
+ res.status(500).json({
643
+ success: false,
644
+ error: "Login failed"
645
+ });
646
+ }
647
+ });
460
648
  this.app.post("/api/context/save", async (req, res) => {
461
649
  try {
462
650
  const { projectId = "default", content, type = "general", metadata = {} } = req.body;
@@ -507,7 +695,7 @@ class RailwayMCPServer {
507
695
  res.status(500).json({ error: error.message });
508
696
  }
509
697
  });
510
- const parseCookies2 = (cookieHeader) => {
698
+ const parseCookies = (cookieHeader) => {
511
699
  const out = {};
512
700
  if (!cookieHeader) return out;
513
701
  cookieHeader.split(";").forEach((p) => {
@@ -516,15 +704,15 @@ class RailwayMCPServer {
516
704
  });
517
705
  return out;
518
706
  };
519
- const setJwtCookie2 = (res, token) => {
707
+ const setJwtCookie = (res, token) => {
520
708
  const flags = ["Path=/", "HttpOnly", "SameSite=Lax"];
521
709
  if (process.env["NODE_ENV"] === "production") flags.push("Secure");
522
710
  res.setHeader("Set-Cookie", `sm_admin_jwt=${encodeURIComponent(token)}; ${flags.join("; ")}`);
523
711
  };
524
- const clearJwtCookie2 = (res) => {
712
+ const clearJwtCookie = (res) => {
525
713
  res.setHeader("Set-Cookie", "sm_admin_jwt=; Path=/; HttpOnly; Max-Age=0; SameSite=Lax");
526
714
  };
527
- const verifyAdminJwt2 = (token) => {
715
+ const verifyAdminJwt = (token) => {
528
716
  try {
529
717
  const secret = process.env["ADMIN_JWT_SECRET"] || "dev-admin-secret";
530
718
  const payload = jwt.verify(token, secret);
@@ -544,10 +732,10 @@ class RailwayMCPServer {
544
732
  const requireAdmin = (req, res, next) => {
545
733
  const user = req.user || {};
546
734
  if (user.role === "admin") return next();
547
- const cookies = parseCookies2(req.headers.cookie);
735
+ const cookies = parseCookies(req.headers.cookie);
548
736
  const t = cookies["sm_admin_jwt"];
549
737
  if (t) {
550
- const verified = verifyAdminJwt2(t);
738
+ const verified = verifyAdminJwt(t);
551
739
  if (verified) {
552
740
  checkDbSession(verified.jti).then((ok) => {
553
741
  if (ok) return next();
@@ -580,8 +768,8 @@ class RailwayMCPServer {
580
768
  }
581
769
  const rows = this.db.prepare("SELECT id, name, is_public, created_at, updated_at FROM projects ORDER BY updated_at DESC").all();
582
770
  return res.json({ projects: rows });
583
- } catch (e2) {
584
- res.status(500).json({ error: e2.message });
771
+ } catch (e) {
772
+ res.status(500).json({ error: e.message });
585
773
  }
586
774
  });
587
775
  this.app.post("/admin/api/projects", requireAdmin, async (req, res) => {
@@ -594,8 +782,8 @@ class RailwayMCPServer {
594
782
  }
595
783
  this.db.prepare("INSERT OR IGNORE INTO projects (id, name, is_public) VALUES (?, ?, ?)").run(id, name || id, isPublic ? 1 : 0);
596
784
  return res.json({ success: true });
597
- } catch (e2) {
598
- res.status(500).json({ error: e2.message });
785
+ } catch (e) {
786
+ res.status(500).json({ error: e.message });
599
787
  }
600
788
  });
601
789
  this.app.patch("/admin/api/projects/:id/visibility", requireAdmin, async (req, res) => {
@@ -609,8 +797,8 @@ class RailwayMCPServer {
609
797
  }
610
798
  this.db.prepare("UPDATE projects SET is_public = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(isPublic ? 1 : 0, pid);
611
799
  return res.json({ success: true });
612
- } catch (e2) {
613
- res.status(500).json({ error: e2.message });
800
+ } catch (e) {
801
+ res.status(500).json({ error: e.message });
614
802
  }
615
803
  });
616
804
  this.app.get("/admin/api/projects/:id/members", requireAdmin, async (req, res) => {
@@ -625,8 +813,8 @@ class RailwayMCPServer {
625
813
  }
626
814
  const stmt = this.db.prepare("SELECT pm.user_id, pm.role, u.email, u.name FROM project_members pm LEFT JOIN users u ON u.id = pm.user_id WHERE pm.project_id = ? ORDER BY pm.role");
627
815
  return res.json({ members: stmt.all(pid) });
628
- } catch (e2) {
629
- res.status(500).json({ error: e2.message });
816
+ } catch (e) {
817
+ res.status(500).json({ error: e.message });
630
818
  }
631
819
  });
632
820
  this.app.put("/admin/api/projects/:id/members", requireAdmin, async (req, res) => {
@@ -645,8 +833,8 @@ class RailwayMCPServer {
645
833
  }
646
834
  this.db.prepare("INSERT INTO project_members (project_id, user_id, role) VALUES (?, ?, ?) ON CONFLICT(project_id, user_id) DO UPDATE SET role = ?").run(pid, userId, role, role);
647
835
  return res.json({ success: true });
648
- } catch (e2) {
649
- res.status(500).json({ error: e2.message });
836
+ } catch (e) {
837
+ res.status(500).json({ error: e.message });
650
838
  }
651
839
  });
652
840
  this.app.delete("/admin/api/projects/:id/members/:userId", requireAdmin, async (req, res) => {
@@ -659,8 +847,8 @@ class RailwayMCPServer {
659
847
  }
660
848
  this.db.prepare("DELETE FROM project_members WHERE project_id = ? AND user_id = ?").run(pid, uid);
661
849
  return res.json({ success: true });
662
- } catch (e2) {
663
- res.status(500).json({ error: e2.message });
850
+ } catch (e) {
851
+ res.status(500).json({ error: e.message });
664
852
  }
665
853
  });
666
854
  this.app.get("/admin/api/sessions", requireAdmin, async (_req, res) => {
@@ -671,8 +859,8 @@ class RailwayMCPServer {
671
859
  }
672
860
  const rows = this.db.prepare("SELECT id, user_id, created_at, expires_at, user_agent, ip FROM admin_sessions ORDER BY created_at DESC").all();
673
861
  return res.json({ sessions: rows });
674
- } catch (e2) {
675
- res.status(500).json({ error: e2.message });
862
+ } catch (e) {
863
+ res.status(500).json({ error: e.message });
676
864
  }
677
865
  });
678
866
  this.app.delete("/admin/api/sessions/:id", requireAdmin, async (req, res) => {
@@ -684,13 +872,13 @@ class RailwayMCPServer {
684
872
  this.db.prepare("DELETE FROM admin_sessions WHERE id = ?").run(id);
685
873
  }
686
874
  res.json({ success: true });
687
- } catch (e2) {
688
- res.status(500).json({ error: e2.message });
875
+ } catch (e) {
876
+ res.status(500).json({ error: e.message });
689
877
  }
690
878
  });
691
879
  this.app.post("/admin/api/sessions/refresh", requireAdmin, async (req, res) => {
692
880
  try {
693
- const cookies = parseCookies2(req.headers.cookie);
881
+ const cookies = parseCookies(req.headers.cookie);
694
882
  const t = cookies["sm_admin_jwt"];
695
883
  if (!t) return res.status(400).json({ error: "No session" });
696
884
  const secret = process.env["ADMIN_JWT_SECRET"] || "dev-admin-secret";
@@ -708,7 +896,8 @@ class RailwayMCPServer {
708
896
  } else {
709
897
  this.db.prepare("DELETE FROM admin_sessions WHERE id = ?").run(oldJti);
710
898
  }
711
- } catch {
899
+ } catch (error) {
900
+ console.warn("Failed to delete session during refresh:", error);
712
901
  }
713
902
  const jti = Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
714
903
  const hours = parseInt(process.env["ADMIN_SESSION_HOURS"] || "8", 10);
@@ -726,8 +915,8 @@ class RailwayMCPServer {
726
915
  if (process.env["NODE_ENV"] === "production") flags.push("Secure");
727
916
  res.setHeader("Set-Cookie", `sm_admin_jwt=${encodeURIComponent(token)}; ${flags.join("; ")}`);
728
917
  return res.json({ success: true });
729
- } catch (e2) {
730
- res.status(500).json({ error: e2.message });
918
+ } catch (e) {
919
+ res.status(500).json({ error: e.message });
731
920
  }
732
921
  });
733
922
  this.app.get("/admin", requireAdmin, (req, res) => {
@@ -879,6 +1068,63 @@ loadSessions();
879
1068
  }
880
1069
  });
881
1070
  }
1071
+ this.app.get("/admin/login", (_req, res) => {
1072
+ res.setHeader("Content-Type", "text/html");
1073
+ res.send(`<!doctype html><html><head><meta charset="utf-8"/><title>Admin Login</title>
1074
+ <style>body{font-family:system-ui;margin:40px} input{padding:8px;margin:4px} button{padding:8px}</style></head>
1075
+ <body><h3>Admin Login</h3>
1076
+ <p>Paste an admin API key to manage projects and members.</p>
1077
+ <form method="POST" action="/admin/login">
1078
+ <input type="password" name="apiKey" placeholder="sk-..." style="min-width:360px" required/>
1079
+ <div><button type="submit">Login</button></div>
1080
+ <p style="color:#666">Your key is validated server-side and not stored in the browser; a short-lived session cookie is created.</p>
1081
+ </form>
1082
+ </body></html>`);
1083
+ });
1084
+ this.app.post("/admin/login", express.urlencoded({ extended: false }), async (req, res) => {
1085
+ try {
1086
+ const apiKey = req.body?.apiKey || "";
1087
+ if (!apiKey) return res.status(400).send("Missing API key");
1088
+ const u = await this.validateApiKey(apiKey);
1089
+ if (!u || u.role !== "admin") return res.status(403).send("Not an admin API key");
1090
+ const jti = Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
1091
+ const hours = parseInt(process.env["ADMIN_SESSION_HOURS"] || "8", 10);
1092
+ const expMs = Date.now() + hours * 3600 * 1e3;
1093
+ const expDateIso = new Date(expMs).toISOString();
1094
+ const ua = req.headers["user-agent"] || "";
1095
+ const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress || "";
1096
+ if (this.pgPool) {
1097
+ await this.pgPool.query("INSERT INTO admin_sessions (id, user_id, expires_at, user_agent, ip) VALUES ($1, $2, $3, $4, $5)", [jti, u.id, expDateIso, ua, ip]);
1098
+ } else {
1099
+ this.db.prepare("INSERT INTO admin_sessions (id, user_id, expires_at, user_agent, ip) VALUES (?, ?, ?, ?, ?)").run(jti, u.id, expDateIso, ua, ip);
1100
+ }
1101
+ const token = jwt.sign({ sub: u.id, role: "admin", jti }, process.env["ADMIN_JWT_SECRET"] || "dev-admin-secret", { expiresIn: hours + "h" });
1102
+ setJwtCookie(res, token);
1103
+ res.redirect("/admin");
1104
+ } catch (e) {
1105
+ res.status(500).send("Login failed");
1106
+ }
1107
+ });
1108
+ this.app.get("/admin/logout", async (req, res) => {
1109
+ const cookies = parseCookies(req.headers.cookie);
1110
+ const t = cookies["sm_admin_jwt"];
1111
+ if (t) {
1112
+ const verified = verifyAdminJwt(t);
1113
+ if (verified) {
1114
+ try {
1115
+ if (this.pgPool) {
1116
+ await this.pgPool.query("DELETE FROM admin_sessions WHERE id = $1", [verified.jti]);
1117
+ } else {
1118
+ this.db.prepare("DELETE FROM admin_sessions WHERE id = ?").run(verified.jti);
1119
+ }
1120
+ } catch (error) {
1121
+ console.warn("Failed to delete session during logout:", error);
1122
+ }
1123
+ }
1124
+ }
1125
+ clearJwtCookie(res);
1126
+ res.redirect("/admin/login");
1127
+ });
882
1128
  }
883
1129
  setupWebSocket() {
884
1130
  this.wss = new WebSocketServer({
@@ -1096,60 +1342,4 @@ process.on("SIGINT", () => {
1096
1342
  console.log("Shutting down...");
1097
1343
  process.exit(0);
1098
1344
  });
1099
- (void 0).app.get("/admin/login", (_req, res) => {
1100
- res.setHeader("Content-Type", "text/html");
1101
- res.send(`<!doctype html><html><head><meta charset="utf-8"/><title>Admin Login</title>
1102
- <style>body{font-family:system-ui;margin:40px} input{padding:8px;margin:4px} button{padding:8px}</style></head>
1103
- <body><h3>Admin Login</h3>
1104
- <p>Paste an admin API key to manage projects and members.</p>
1105
- <form method="POST" action="/admin/login">
1106
- <input type="password" name="apiKey" placeholder="sk-..." style="min-width:360px" required/>
1107
- <div><button type="submit">Login</button></div>
1108
- <p style="color:#666">Your key is validated server-side and not stored in the browser; a short-lived session cookie is created.</p>
1109
- </form>
1110
- </body></html>`);
1111
- });
1112
- (void 0).app.post("/admin/login", express.urlencoded({ extended: false }), async (req, res) => {
1113
- try {
1114
- const apiKey = req.body?.apiKey || "";
1115
- if (!apiKey) return res.status(400).send("Missing API key");
1116
- const u = await (void 0).validateApiKey(apiKey);
1117
- if (!u || u.role !== "admin") return res.status(403).send("Not an admin API key");
1118
- const jti = Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
1119
- const hours = parseInt(process.env["ADMIN_SESSION_HOURS"] || "8", 10);
1120
- const expMs = Date.now() + hours * 3600 * 1e3;
1121
- const expDateIso = new Date(expMs).toISOString();
1122
- const ua = req.headers["user-agent"] || "";
1123
- const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress || "";
1124
- if ((void 0).pgPool) {
1125
- await (void 0).pgPool.query("INSERT INTO admin_sessions (id, user_id, expires_at, user_agent, ip) VALUES ($1, $2, $3, $4, $5)", [jti, u.id, expDateIso, ua, ip]);
1126
- } else {
1127
- (void 0).db.prepare("INSERT INTO admin_sessions (id, user_id, expires_at, user_agent, ip) VALUES (?, ?, ?, ?, ?)").run(jti, u.id, expDateIso, ua, ip);
1128
- }
1129
- const token = jwt.sign({ sub: u.id, role: "admin", jti }, process.env["ADMIN_JWT_SECRET"] || "dev-admin-secret", { expiresIn: hours + "h" });
1130
- setJwtCookie(res, token);
1131
- res.redirect("/admin");
1132
- } catch (e2) {
1133
- res.status(500).send("Login failed");
1134
- }
1135
- });
1136
- (void 0).app.get("/admin/logout", async (req, res) => {
1137
- const cookies = parseCookies(req.headers.cookie);
1138
- const t = cookies["sm_admin_jwt"];
1139
- if (t) {
1140
- const verified = verifyAdminJwt(t);
1141
- if (verified) {
1142
- try {
1143
- if ((void 0).pgPool) {
1144
- await (void 0).pgPool.query("DELETE FROM admin_sessions WHERE id = $1", [verified.jti]);
1145
- } else {
1146
- (void 0).db.prepare("DELETE FROM admin_sessions WHERE id = ?").run(verified.jti);
1147
- }
1148
- } catch {
1149
- }
1150
- }
1151
- }
1152
- clearJwtCookie(res);
1153
- res.redirect("/admin/login");
1154
- });
1155
1345
  //# sourceMappingURL=index.js.map