@stackmemoryai/stackmemory 0.3.21 → 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/commands/linear-unified.js +2 -3
- package/dist/cli/commands/linear-unified.js.map +2 -2
- package/dist/cli/commands/tasks.js +1 -1
- package/dist/cli/commands/tasks.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 +98 -92
- package/dist/servers/railway/index.js.map +3 -3
- package/package.json +1 -2
- 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
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/integrations/mcp/tool-definitions-code.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Code Execution Tool Definitions for MCP Server\n */\n\nexport const codeExecutionTools = [\n {\n name: 'code.execute',\n description: 'Execute Python, JavaScript, or TypeScript code in a sandboxed environment',\n inputSchema: {\n type: 'object',\n properties: {\n language: {\n type: 'string',\n enum: ['python', 'javascript', 'typescript'],\n description: 'Programming language to execute',\n },\n code: {\n type: 'string',\n description: 'Code to execute',\n },\n workingDirectory: {\n type: 'string',\n description: 'Optional working directory for execution',\n },\n timeout: {\n type: 'number',\n description: 'Execution timeout in milliseconds (default: 30000)',\n },\n force: {\n type: 'boolean',\n description: 'Force execution even if code validation fails',\n },\n },\n required: ['language', 'code'],\n },\n },\n {\n name: 'code.validate',\n description: 'Validate code for potential security issues',\n inputSchema: {\n type: 'object',\n properties: {\n code: {\n type: 'string',\n description: 'Code to validate',\n },\n },\n required: ['code'],\n },\n },\n {\n name: 'code.sandbox_status',\n description: 'Get status of the code execution sandbox',\n inputSchema: {\n type: 'object',\n properties: {},\n },\n },\n {\n name: 'code.clean_sandbox',\n description: 'Clean temporary files from the sandbox',\n inputSchema: {\n type: 'object',\n properties: {},\n },\n },\n];\n\n/**\n * Example of using code execution in restricted mode\n */\nexport const codeOnlyModeExample = `\n# When in code_only mode, Claude can ONLY execute code\n\n## Example Python execution:\n\\`\\`\\`python\nimport numpy as np\nimport matplotlib.pyplot as plt\n\n# Generate data\nx = np.linspace(0, 10, 100)\ny = np.sin(x)\n\n# Create visualization (won't display but will execute)\nplt.plot(x, y)\nplt.title('Sine Wave')\nplt.xlabel('X')\nplt.ylabel('Y')\n\n# Calculate statistics\nmean_y = np.mean(y)\nstd_y = np.std(y)\nprint(f\"Mean: {mean_y:.4f}\")\nprint(f\"Std: {std_y:.4f}\")\n\\`\\`\\`\n\n## Example JavaScript execution:\n\\`\\`\\`javascript\n// Process data\nconst data = Array.from({length: 10}, (_, i) => i ** 2);\n\n// Calculate sum\nconst sum = data.reduce((a, b) => a + b, 0);\nconsole.log('Sum of squares:', sum);\n\n// Async operation\nasync function fetchData() {\n // Simulate API call\n await new Promise(resolve => setTimeout(resolve, 100));\n return { status: 'success', data: [1, 2, 3] };\n}\n\nfetchData().then(result => {\n console.log('Async result:', result);\n});\n\\`\\`\\`\n\n## Benefits of code_only mode:\n1. Safe computational environment\n2. No file system modifications\n3. No network access\n4. Pure problem-solving focus\n5. Ideal for algorithms, data analysis, and mathematical computations\n`;"],
|
|
5
|
+
"mappings": "AAIO,MAAM,qBAAqB;AAAA,EAChC;AAAA,IACE,MAAM;AAAA,IACN,aAAa;AAAA,IACb,aAAa;AAAA,MACX,MAAM;AAAA,MACN,YAAY;AAAA,QACV,UAAU;AAAA,UACR,MAAM;AAAA,UACN,MAAM,CAAC,UAAU,cAAc,YAAY;AAAA,UAC3C,aAAa;AAAA,QACf;AAAA,QACA,MAAM;AAAA,UACJ,MAAM;AAAA,UACN,aAAa;AAAA,QACf;AAAA,QACA,kBAAkB;AAAA,UAChB,MAAM;AAAA,UACN,aAAa;AAAA,QACf;AAAA,QACA,SAAS;AAAA,UACP,MAAM;AAAA,UACN,aAAa;AAAA,QACf;AAAA,QACA,OAAO;AAAA,UACL,MAAM;AAAA,UACN,aAAa;AAAA,QACf;AAAA,MACF;AAAA,MACA,UAAU,CAAC,YAAY,MAAM;AAAA,IAC/B;AAAA,EACF;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,aAAa;AAAA,IACb,aAAa;AAAA,MACX,MAAM;AAAA,MACN,YAAY;AAAA,QACV,MAAM;AAAA,UACJ,MAAM;AAAA,UACN,aAAa;AAAA,QACf;AAAA,MACF;AAAA,MACA,UAAU,CAAC,MAAM;AAAA,IACnB;AAAA,EACF;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,aAAa;AAAA,IACb,aAAa;AAAA,MACX,MAAM;AAAA,MACN,YAAY,CAAC;AAAA,IACf;AAAA,EACF;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,aAAa;AAAA,IACb,aAAa;AAAA,MACX,MAAM;AAAA,MACN,YAAY,CAAC;AAAA,IACf;AAAA,EACF;AACF;AAKO,MAAM,sBAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -248,7 +248,7 @@ class RailwayMCPServer {
|
|
|
248
248
|
for (const q of queries) {
|
|
249
249
|
try {
|
|
250
250
|
await this.pgPool.query(q);
|
|
251
|
-
} catch (
|
|
251
|
+
} catch (e) {
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
await this.pgPool.query("INSERT INTO railway_schema_version (version, description) VALUES ($1, $2) ON CONFLICT (version) DO NOTHING", [version, description]);
|
|
@@ -331,10 +331,13 @@ class RailwayMCPServer {
|
|
|
331
331
|
if (this.pgPool) {
|
|
332
332
|
await this.pgPool.query("DELETE FROM admin_sessions WHERE expires_at <= NOW()");
|
|
333
333
|
} else if (this.db) {
|
|
334
|
-
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
|
+
}
|
|
335
338
|
}
|
|
336
|
-
} catch {
|
|
337
|
-
console.warn("Admin session cleanup failed:",
|
|
339
|
+
} catch (error) {
|
|
340
|
+
console.warn("Admin session cleanup failed:", error);
|
|
338
341
|
}
|
|
339
342
|
};
|
|
340
343
|
run();
|
|
@@ -359,7 +362,7 @@ class RailwayMCPServer {
|
|
|
359
362
|
}
|
|
360
363
|
}
|
|
361
364
|
async authenticate(req, res, next) {
|
|
362
|
-
if (req.path === "/health" || req.path === "/health/db") {
|
|
365
|
+
if (req.path === "/health" || req.path === "/api/health" || req.path === "/health/db") {
|
|
363
366
|
return next();
|
|
364
367
|
}
|
|
365
368
|
const authHeader = req.headers.authorization;
|
|
@@ -375,8 +378,8 @@ class RailwayMCPServer {
|
|
|
375
378
|
}
|
|
376
379
|
req.user = valid;
|
|
377
380
|
return next();
|
|
378
|
-
} catch (
|
|
379
|
-
return res.status(500).json({ error:
|
|
381
|
+
} catch (e) {
|
|
382
|
+
return res.status(500).json({ error: e.message || "Auth error" });
|
|
380
383
|
}
|
|
381
384
|
}
|
|
382
385
|
if (config.authMode === "jwt" && process.env["AUTH0_DOMAIN"]) {
|
|
@@ -448,7 +451,7 @@ class RailwayMCPServer {
|
|
|
448
451
|
next();
|
|
449
452
|
}
|
|
450
453
|
setupRoutes() {
|
|
451
|
-
|
|
454
|
+
const healthHandler = (req, res) => {
|
|
452
455
|
const health = {
|
|
453
456
|
status: "healthy",
|
|
454
457
|
version: "1.0.0",
|
|
@@ -457,7 +460,9 @@ class RailwayMCPServer {
|
|
|
457
460
|
environment: config.environment
|
|
458
461
|
};
|
|
459
462
|
res.json(health);
|
|
460
|
-
}
|
|
463
|
+
};
|
|
464
|
+
this.app.get("/health", healthHandler);
|
|
465
|
+
this.app.get("/api/health", healthHandler);
|
|
461
466
|
this.app.get("/", (req, res) => {
|
|
462
467
|
res.json({
|
|
463
468
|
name: "StackMemory Railway Server",
|
|
@@ -599,7 +604,6 @@ class RailwayMCPServer {
|
|
|
599
604
|
[apiKeyHash, keyResult.rows[0].id]
|
|
600
605
|
);
|
|
601
606
|
}
|
|
602
|
-
const databaseUrl = process.env.DATABASE_URL;
|
|
603
607
|
} else {
|
|
604
608
|
const keyRow = this.db.prepare(
|
|
605
609
|
"SELECT id FROM api_keys WHERE user_id = ? AND revoked = 0 LIMIT 1"
|
|
@@ -691,7 +695,7 @@ class RailwayMCPServer {
|
|
|
691
695
|
res.status(500).json({ error: error.message });
|
|
692
696
|
}
|
|
693
697
|
});
|
|
694
|
-
const
|
|
698
|
+
const parseCookies = (cookieHeader) => {
|
|
695
699
|
const out = {};
|
|
696
700
|
if (!cookieHeader) return out;
|
|
697
701
|
cookieHeader.split(";").forEach((p) => {
|
|
@@ -700,15 +704,15 @@ class RailwayMCPServer {
|
|
|
700
704
|
});
|
|
701
705
|
return out;
|
|
702
706
|
};
|
|
703
|
-
const
|
|
707
|
+
const setJwtCookie = (res, token) => {
|
|
704
708
|
const flags = ["Path=/", "HttpOnly", "SameSite=Lax"];
|
|
705
709
|
if (process.env["NODE_ENV"] === "production") flags.push("Secure");
|
|
706
710
|
res.setHeader("Set-Cookie", `sm_admin_jwt=${encodeURIComponent(token)}; ${flags.join("; ")}`);
|
|
707
711
|
};
|
|
708
|
-
const
|
|
712
|
+
const clearJwtCookie = (res) => {
|
|
709
713
|
res.setHeader("Set-Cookie", "sm_admin_jwt=; Path=/; HttpOnly; Max-Age=0; SameSite=Lax");
|
|
710
714
|
};
|
|
711
|
-
const
|
|
715
|
+
const verifyAdminJwt = (token) => {
|
|
712
716
|
try {
|
|
713
717
|
const secret = process.env["ADMIN_JWT_SECRET"] || "dev-admin-secret";
|
|
714
718
|
const payload = jwt.verify(token, secret);
|
|
@@ -728,10 +732,10 @@ class RailwayMCPServer {
|
|
|
728
732
|
const requireAdmin = (req, res, next) => {
|
|
729
733
|
const user = req.user || {};
|
|
730
734
|
if (user.role === "admin") return next();
|
|
731
|
-
const cookies =
|
|
735
|
+
const cookies = parseCookies(req.headers.cookie);
|
|
732
736
|
const t = cookies["sm_admin_jwt"];
|
|
733
737
|
if (t) {
|
|
734
|
-
const verified =
|
|
738
|
+
const verified = verifyAdminJwt(t);
|
|
735
739
|
if (verified) {
|
|
736
740
|
checkDbSession(verified.jti).then((ok) => {
|
|
737
741
|
if (ok) return next();
|
|
@@ -764,8 +768,8 @@ class RailwayMCPServer {
|
|
|
764
768
|
}
|
|
765
769
|
const rows = this.db.prepare("SELECT id, name, is_public, created_at, updated_at FROM projects ORDER BY updated_at DESC").all();
|
|
766
770
|
return res.json({ projects: rows });
|
|
767
|
-
} catch (
|
|
768
|
-
res.status(500).json({ error:
|
|
771
|
+
} catch (e) {
|
|
772
|
+
res.status(500).json({ error: e.message });
|
|
769
773
|
}
|
|
770
774
|
});
|
|
771
775
|
this.app.post("/admin/api/projects", requireAdmin, async (req, res) => {
|
|
@@ -778,8 +782,8 @@ class RailwayMCPServer {
|
|
|
778
782
|
}
|
|
779
783
|
this.db.prepare("INSERT OR IGNORE INTO projects (id, name, is_public) VALUES (?, ?, ?)").run(id, name || id, isPublic ? 1 : 0);
|
|
780
784
|
return res.json({ success: true });
|
|
781
|
-
} catch (
|
|
782
|
-
res.status(500).json({ error:
|
|
785
|
+
} catch (e) {
|
|
786
|
+
res.status(500).json({ error: e.message });
|
|
783
787
|
}
|
|
784
788
|
});
|
|
785
789
|
this.app.patch("/admin/api/projects/:id/visibility", requireAdmin, async (req, res) => {
|
|
@@ -793,8 +797,8 @@ class RailwayMCPServer {
|
|
|
793
797
|
}
|
|
794
798
|
this.db.prepare("UPDATE projects SET is_public = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(isPublic ? 1 : 0, pid);
|
|
795
799
|
return res.json({ success: true });
|
|
796
|
-
} catch (
|
|
797
|
-
res.status(500).json({ error:
|
|
800
|
+
} catch (e) {
|
|
801
|
+
res.status(500).json({ error: e.message });
|
|
798
802
|
}
|
|
799
803
|
});
|
|
800
804
|
this.app.get("/admin/api/projects/:id/members", requireAdmin, async (req, res) => {
|
|
@@ -809,8 +813,8 @@ class RailwayMCPServer {
|
|
|
809
813
|
}
|
|
810
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");
|
|
811
815
|
return res.json({ members: stmt.all(pid) });
|
|
812
|
-
} catch (
|
|
813
|
-
res.status(500).json({ error:
|
|
816
|
+
} catch (e) {
|
|
817
|
+
res.status(500).json({ error: e.message });
|
|
814
818
|
}
|
|
815
819
|
});
|
|
816
820
|
this.app.put("/admin/api/projects/:id/members", requireAdmin, async (req, res) => {
|
|
@@ -829,8 +833,8 @@ class RailwayMCPServer {
|
|
|
829
833
|
}
|
|
830
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);
|
|
831
835
|
return res.json({ success: true });
|
|
832
|
-
} catch (
|
|
833
|
-
res.status(500).json({ error:
|
|
836
|
+
} catch (e) {
|
|
837
|
+
res.status(500).json({ error: e.message });
|
|
834
838
|
}
|
|
835
839
|
});
|
|
836
840
|
this.app.delete("/admin/api/projects/:id/members/:userId", requireAdmin, async (req, res) => {
|
|
@@ -843,8 +847,8 @@ class RailwayMCPServer {
|
|
|
843
847
|
}
|
|
844
848
|
this.db.prepare("DELETE FROM project_members WHERE project_id = ? AND user_id = ?").run(pid, uid);
|
|
845
849
|
return res.json({ success: true });
|
|
846
|
-
} catch (
|
|
847
|
-
res.status(500).json({ error:
|
|
850
|
+
} catch (e) {
|
|
851
|
+
res.status(500).json({ error: e.message });
|
|
848
852
|
}
|
|
849
853
|
});
|
|
850
854
|
this.app.get("/admin/api/sessions", requireAdmin, async (_req, res) => {
|
|
@@ -855,8 +859,8 @@ class RailwayMCPServer {
|
|
|
855
859
|
}
|
|
856
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();
|
|
857
861
|
return res.json({ sessions: rows });
|
|
858
|
-
} catch (
|
|
859
|
-
res.status(500).json({ error:
|
|
862
|
+
} catch (e) {
|
|
863
|
+
res.status(500).json({ error: e.message });
|
|
860
864
|
}
|
|
861
865
|
});
|
|
862
866
|
this.app.delete("/admin/api/sessions/:id", requireAdmin, async (req, res) => {
|
|
@@ -868,13 +872,13 @@ class RailwayMCPServer {
|
|
|
868
872
|
this.db.prepare("DELETE FROM admin_sessions WHERE id = ?").run(id);
|
|
869
873
|
}
|
|
870
874
|
res.json({ success: true });
|
|
871
|
-
} catch (
|
|
872
|
-
res.status(500).json({ error:
|
|
875
|
+
} catch (e) {
|
|
876
|
+
res.status(500).json({ error: e.message });
|
|
873
877
|
}
|
|
874
878
|
});
|
|
875
879
|
this.app.post("/admin/api/sessions/refresh", requireAdmin, async (req, res) => {
|
|
876
880
|
try {
|
|
877
|
-
const cookies =
|
|
881
|
+
const cookies = parseCookies(req.headers.cookie);
|
|
878
882
|
const t = cookies["sm_admin_jwt"];
|
|
879
883
|
if (!t) return res.status(400).json({ error: "No session" });
|
|
880
884
|
const secret = process.env["ADMIN_JWT_SECRET"] || "dev-admin-secret";
|
|
@@ -892,7 +896,8 @@ class RailwayMCPServer {
|
|
|
892
896
|
} else {
|
|
893
897
|
this.db.prepare("DELETE FROM admin_sessions WHERE id = ?").run(oldJti);
|
|
894
898
|
}
|
|
895
|
-
} catch {
|
|
899
|
+
} catch (error) {
|
|
900
|
+
console.warn("Failed to delete session during refresh:", error);
|
|
896
901
|
}
|
|
897
902
|
const jti = Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
|
|
898
903
|
const hours = parseInt(process.env["ADMIN_SESSION_HOURS"] || "8", 10);
|
|
@@ -910,8 +915,8 @@ class RailwayMCPServer {
|
|
|
910
915
|
if (process.env["NODE_ENV"] === "production") flags.push("Secure");
|
|
911
916
|
res.setHeader("Set-Cookie", `sm_admin_jwt=${encodeURIComponent(token)}; ${flags.join("; ")}`);
|
|
912
917
|
return res.json({ success: true });
|
|
913
|
-
} catch (
|
|
914
|
-
res.status(500).json({ error:
|
|
918
|
+
} catch (e) {
|
|
919
|
+
res.status(500).json({ error: e.message });
|
|
915
920
|
}
|
|
916
921
|
});
|
|
917
922
|
this.app.get("/admin", requireAdmin, (req, res) => {
|
|
@@ -1063,6 +1068,63 @@ loadSessions();
|
|
|
1063
1068
|
}
|
|
1064
1069
|
});
|
|
1065
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
|
+
});
|
|
1066
1128
|
}
|
|
1067
1129
|
setupWebSocket() {
|
|
1068
1130
|
this.wss = new WebSocketServer({
|
|
@@ -1280,60 +1342,4 @@ process.on("SIGINT", () => {
|
|
|
1280
1342
|
console.log("Shutting down...");
|
|
1281
1343
|
process.exit(0);
|
|
1282
1344
|
});
|
|
1283
|
-
(void 0).app.get("/admin/login", (_req, res) => {
|
|
1284
|
-
res.setHeader("Content-Type", "text/html");
|
|
1285
|
-
res.send(`<!doctype html><html><head><meta charset="utf-8"/><title>Admin Login</title>
|
|
1286
|
-
<style>body{font-family:system-ui;margin:40px} input{padding:8px;margin:4px} button{padding:8px}</style></head>
|
|
1287
|
-
<body><h3>Admin Login</h3>
|
|
1288
|
-
<p>Paste an admin API key to manage projects and members.</p>
|
|
1289
|
-
<form method="POST" action="/admin/login">
|
|
1290
|
-
<input type="password" name="apiKey" placeholder="sk-..." style="min-width:360px" required/>
|
|
1291
|
-
<div><button type="submit">Login</button></div>
|
|
1292
|
-
<p style="color:#666">Your key is validated server-side and not stored in the browser; a short-lived session cookie is created.</p>
|
|
1293
|
-
</form>
|
|
1294
|
-
</body></html>`);
|
|
1295
|
-
});
|
|
1296
|
-
(void 0).app.post("/admin/login", express.urlencoded({ extended: false }), async (req, res) => {
|
|
1297
|
-
try {
|
|
1298
|
-
const apiKey = req.body?.apiKey || "";
|
|
1299
|
-
if (!apiKey) return res.status(400).send("Missing API key");
|
|
1300
|
-
const u = await (void 0).validateApiKey(apiKey);
|
|
1301
|
-
if (!u || u.role !== "admin") return res.status(403).send("Not an admin API key");
|
|
1302
|
-
const jti = Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
|
|
1303
|
-
const hours = parseInt(process.env["ADMIN_SESSION_HOURS"] || "8", 10);
|
|
1304
|
-
const expMs = Date.now() + hours * 3600 * 1e3;
|
|
1305
|
-
const expDateIso = new Date(expMs).toISOString();
|
|
1306
|
-
const ua = req.headers["user-agent"] || "";
|
|
1307
|
-
const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress || "";
|
|
1308
|
-
if ((void 0).pgPool) {
|
|
1309
|
-
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]);
|
|
1310
|
-
} else {
|
|
1311
|
-
(void 0).db.prepare("INSERT INTO admin_sessions (id, user_id, expires_at, user_agent, ip) VALUES (?, ?, ?, ?, ?)").run(jti, u.id, expDateIso, ua, ip);
|
|
1312
|
-
}
|
|
1313
|
-
const token = jwt.sign({ sub: u.id, role: "admin", jti }, process.env["ADMIN_JWT_SECRET"] || "dev-admin-secret", { expiresIn: hours + "h" });
|
|
1314
|
-
setJwtCookie(res, token);
|
|
1315
|
-
res.redirect("/admin");
|
|
1316
|
-
} catch (e2) {
|
|
1317
|
-
res.status(500).send("Login failed");
|
|
1318
|
-
}
|
|
1319
|
-
});
|
|
1320
|
-
(void 0).app.get("/admin/logout", async (req, res) => {
|
|
1321
|
-
const cookies = parseCookies(req.headers.cookie);
|
|
1322
|
-
const t = cookies["sm_admin_jwt"];
|
|
1323
|
-
if (t) {
|
|
1324
|
-
const verified = verifyAdminJwt(t);
|
|
1325
|
-
if (verified) {
|
|
1326
|
-
try {
|
|
1327
|
-
if ((void 0).pgPool) {
|
|
1328
|
-
await (void 0).pgPool.query("DELETE FROM admin_sessions WHERE id = $1", [verified.jti]);
|
|
1329
|
-
} else {
|
|
1330
|
-
(void 0).db.prepare("DELETE FROM admin_sessions WHERE id = ?").run(verified.jti);
|
|
1331
|
-
}
|
|
1332
|
-
} catch {
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
clearJwtCookie(res);
|
|
1337
|
-
res.redirect("/admin/login");
|
|
1338
|
-
});
|
|
1339
1345
|
//# sourceMappingURL=index.js.map
|