@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.
- package/dist/cli/claude-sm.js +1 -3
- package/dist/cli/claude-sm.js.map +2 -2
- package/dist/cli/codex-sm.js.map +1 -1
- package/dist/cli/commands/linear-unified.js +2 -3
- package/dist/cli/commands/linear-unified.js.map +2 -2
- package/dist/cli/commands/signup.js +46 -0
- package/dist/cli/commands/signup.js.map +7 -0
- package/dist/cli/commands/tasks.js +1 -1
- package/dist/cli/commands/tasks.js.map +2 -2
- package/dist/cli/index.js +3 -1
- package/dist/cli/index.js.map +2 -2
- package/dist/integrations/mcp/handlers/code-execution-handlers.js +262 -0
- package/dist/integrations/mcp/handlers/code-execution-handlers.js.map +7 -0
- package/dist/integrations/mcp/tool-definitions-code.js +121 -0
- package/dist/integrations/mcp/tool-definitions-code.js.map +7 -0
- package/dist/servers/railway/index.js +283 -93
- package/dist/servers/railway/index.js.map +3 -3
- package/package.json +2 -3
- package/scripts/claude-sm-autostart.js +1 -1
- package/scripts/clean-linear-backlog.js +2 -2
- package/scripts/debug-linear-update.js +1 -1
- package/scripts/debug-railway-build.js +87 -0
- package/scripts/delete-linear-tasks.js +2 -2
- package/scripts/install-code-execution-hooks.sh +96 -0
- package/scripts/linear-task-review.js +1 -1
- package/scripts/sync-and-clean-tasks.js +1 -1
- package/scripts/sync-linear-graphql.js +3 -3
- package/scripts/sync-linear-tasks.js +1 -1
- package/scripts/test-code-execution.js +143 -0
- package/scripts/update-linear-tasks-fixed.js +1 -1
- package/scripts/validate-railway-deployment.js +137 -0
- package/templates/claude-hooks/hook-config.json +59 -0
- package/templates/claude-hooks/pre-tool-use +189 -0
- package/dist/servers/railway/minimal.js +0 -91
- 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 (
|
|
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(
|
|
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:",
|
|
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 (
|
|
366
|
-
return res.status(500).json({ 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
|
-
|
|
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
|
|
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
|
|
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
|
|
712
|
+
const clearJwtCookie = (res) => {
|
|
525
713
|
res.setHeader("Set-Cookie", "sm_admin_jwt=; Path=/; HttpOnly; Max-Age=0; SameSite=Lax");
|
|
526
714
|
};
|
|
527
|
-
const
|
|
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 =
|
|
735
|
+
const cookies = parseCookies(req.headers.cookie);
|
|
548
736
|
const t = cookies["sm_admin_jwt"];
|
|
549
737
|
if (t) {
|
|
550
|
-
const verified =
|
|
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 (
|
|
584
|
-
res.status(500).json({ error:
|
|
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 (
|
|
598
|
-
res.status(500).json({ error:
|
|
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 (
|
|
613
|
-
res.status(500).json({ error:
|
|
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 (
|
|
629
|
-
res.status(500).json({ error:
|
|
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 (
|
|
649
|
-
res.status(500).json({ error:
|
|
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 (
|
|
663
|
-
res.status(500).json({ error:
|
|
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 (
|
|
675
|
-
res.status(500).json({ error:
|
|
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 (
|
|
688
|
-
res.status(500).json({ error:
|
|
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 =
|
|
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 (
|
|
730
|
-
res.status(500).json({ error:
|
|
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
|