@cybermem/mcp 0.6.10 → 0.8.1

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/src/auth.ts ADDED
@@ -0,0 +1,244 @@
1
+ /**
2
+ * CyberMem MCP Auth Module
3
+ *
4
+ * Token storage and browser-based OAuth login flow.
5
+ */
6
+
7
+ import * as fs from "fs";
8
+ import * as http from "http";
9
+ import * as os from "os";
10
+ import * as path from "path";
11
+
12
+ const AUTH_DIR = path.join(os.homedir(), ".cybermem");
13
+ const TOKEN_FILE = path.join(AUTH_DIR, "token.json");
14
+ const AUTH_URL = process.env.CYBERMEM_AUTH_URL || "https://cybermem.dev";
15
+
16
+ interface StoredToken {
17
+ access_token: string;
18
+ expires_at: string;
19
+ email?: string;
20
+ name?: string;
21
+ }
22
+
23
+ /**
24
+ * Ensure the .cybermem directory exists
25
+ */
26
+ function ensureAuthDir(): void {
27
+ if (!fs.existsSync(AUTH_DIR)) {
28
+ fs.mkdirSync(AUTH_DIR, { recursive: true, mode: 0o700 });
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Get stored token if valid
34
+ */
35
+ export function getToken(): string | null {
36
+ try {
37
+ if (!fs.existsSync(TOKEN_FILE)) {
38
+ return null;
39
+ }
40
+
41
+ const data: StoredToken = JSON.parse(fs.readFileSync(TOKEN_FILE, "utf-8"));
42
+
43
+ // Check expiration
44
+ if (new Date(data.expires_at) < new Date()) {
45
+ console.error("Token expired, please run --login");
46
+ return null;
47
+ }
48
+
49
+ return data.access_token;
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Check if user is logged in with valid token
57
+ */
58
+ export function isLoggedIn(): boolean {
59
+ return getToken() !== null;
60
+ }
61
+
62
+ /**
63
+ * Get user info from stored token
64
+ */
65
+ export function getUserInfo(): { email?: string; name?: string } | null {
66
+ try {
67
+ if (!fs.existsSync(TOKEN_FILE)) {
68
+ return null;
69
+ }
70
+ const data: StoredToken = JSON.parse(fs.readFileSync(TOKEN_FILE, "utf-8"));
71
+ return { email: data.email, name: data.name };
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Save token to disk
79
+ */
80
+ function saveToken(
81
+ token: string,
82
+ expiresIn: number,
83
+ email?: string,
84
+ name?: string,
85
+ ): void {
86
+ ensureAuthDir();
87
+
88
+ const expiresAt = new Date(Date.now() + expiresIn * 1000);
89
+ const data: StoredToken = {
90
+ access_token: token,
91
+ expires_at: expiresAt.toISOString(),
92
+ email,
93
+ name,
94
+ };
95
+
96
+ fs.writeFileSync(TOKEN_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
97
+ }
98
+
99
+ /**
100
+ * Remove stored token
101
+ */
102
+ export function logout(): void {
103
+ if (fs.existsSync(TOKEN_FILE)) {
104
+ fs.unlinkSync(TOKEN_FILE);
105
+ console.log("✅ Logged out successfully");
106
+ } else {
107
+ console.log("Already logged out");
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Show current auth status
113
+ */
114
+ export function showStatus(): void {
115
+ const token = getToken();
116
+ const userInfo = getUserInfo();
117
+
118
+ if (token && userInfo) {
119
+ console.log(
120
+ "✅ Logged in as:",
121
+ userInfo.email || userInfo.name || "Unknown",
122
+ );
123
+ if (userInfo.name) console.log(" Name:", userInfo.name);
124
+ } else {
125
+ console.log("❌ Not logged in");
126
+ console.log(" Run: npx @cybermem/mcp --login");
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Start OAuth login flow
132
+ * Opens browser and waits for callback with token
133
+ */
134
+ export async function login(): Promise<void> {
135
+ return new Promise((resolve, reject) => {
136
+ // Find available port
137
+ const server = http.createServer();
138
+
139
+ server.listen(0, "127.0.0.1", () => {
140
+ const address = server.address();
141
+ if (!address || typeof address === "string") {
142
+ reject(new Error("Failed to start callback server"));
143
+ return;
144
+ }
145
+
146
+ const port = address.port;
147
+ const callbackUrl = `http://localhost:${port}/callback`;
148
+ const authUrl = `${AUTH_URL}/api/auth/signin?callbackUrl=${encodeURIComponent(`${AUTH_URL}/api/auth/cli/callback?redirect=${encodeURIComponent(callbackUrl)}`)}`;
149
+
150
+ console.log("🔐 Opening browser for GitHub login...");
151
+ console.log(` If browser doesn't open, visit: ${authUrl}`);
152
+
153
+ // Open browser
154
+ const open = async (url: string) => {
155
+ const { exec } = await import("child_process");
156
+ const cmd =
157
+ process.platform === "darwin"
158
+ ? `open "${url}"`
159
+ : process.platform === "win32"
160
+ ? `start "${url}"`
161
+ : `xdg-open "${url}"`;
162
+ exec(cmd);
163
+ };
164
+ open(authUrl);
165
+
166
+ // Handle callback
167
+ server.on("request", async (req, res) => {
168
+ if (!req.url?.startsWith("/callback")) {
169
+ res.writeHead(404);
170
+ res.end("Not found");
171
+ return;
172
+ }
173
+
174
+ const url = new URL(req.url, `http://localhost:${port}`);
175
+ const token = url.searchParams.get("token");
176
+
177
+ if (!token) {
178
+ res.writeHead(400);
179
+ res.end("Missing token");
180
+ server.close();
181
+ reject(new Error("No token received"));
182
+ return;
183
+ }
184
+
185
+ // Decode token to get user info (JWT payload)
186
+ let email: string | undefined;
187
+ let name: string | undefined;
188
+ try {
189
+ const payload = JSON.parse(
190
+ Buffer.from(token.split(".")[1], "base64").toString(),
191
+ );
192
+ email = payload.email;
193
+ name = payload.name;
194
+ } catch {
195
+ // Ignore decode errors
196
+ }
197
+
198
+ // Save token (30 days expiry)
199
+ saveToken(token, 30 * 24 * 60 * 60, email, name);
200
+
201
+ // Send success page
202
+ res.writeHead(200, { "Content-Type": "text/html" });
203
+ res.end(`
204
+ <!DOCTYPE html>
205
+ <html>
206
+ <head>
207
+ <title>CyberMem - Logged In</title>
208
+ <style>
209
+ body { font-family: system-ui; text-align: center; padding: 50px; background: #0a0a0a; color: #fff; }
210
+ h1 { color: #22c55e; }
211
+ .logo { font-size: 48px; margin-bottom: 20px; }
212
+ </style>
213
+ </head>
214
+ <body>
215
+ <div class="logo">🧠</div>
216
+ <h1>Successfully Logged In!</h1>
217
+ <p>You can close this window and return to your terminal.</p>
218
+ <p style="color: #888;">Logged in as: ${email || name || "Unknown"}</p>
219
+ </body>
220
+ </html>
221
+ `);
222
+
223
+ console.log("");
224
+ console.log(
225
+ "✅ Successfully logged in as:",
226
+ email || name || "Unknown",
227
+ );
228
+ console.log(" Token saved to:", TOKEN_FILE);
229
+
230
+ server.close();
231
+ resolve();
232
+ });
233
+
234
+ // Timeout after 5 minutes
235
+ setTimeout(
236
+ () => {
237
+ server.close();
238
+ reject(new Error("Login timeout - no callback received"));
239
+ },
240
+ 5 * 60 * 1000,
241
+ );
242
+ });
243
+ });
244
+ }