@jungjaehoon/mama-core 1.0.2 → 1.1.0

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.
@@ -1,424 +0,0 @@
1
- /**
2
- * MAMA Embedding HTTP Server
3
- *
4
- * HTTP API for embedding generation, integrated with MCP server.
5
- * Provides fast embedding access for Claude Code hooks.
6
- *
7
- * Features:
8
- * - Model stays loaded in memory (singleton)
9
- * - HTTP API at localhost:3847
10
- * - Health check endpoint
11
- * - Shared model with MCP server
12
- *
13
- * @module embedding-http-server
14
- */
15
-
16
- const http = require('http');
17
- const path = require('path');
18
- const fs = require('fs');
19
- const crypto = require('crypto');
20
-
21
- // SECURITY P1: Shutdown token for authenticated takeover
22
- const SHUTDOWN_TOKEN = process.env.MAMA_SHUTDOWN_TOKEN || crypto.randomBytes(16).toString('hex');
23
-
24
- // Import embedding functions from mama-core
25
- const { generateEmbedding } = require('../embeddings.js');
26
- const { getModelName, getEmbeddingDim } = require('../config-loader.js');
27
- const { initDB } = require('../db-manager.js');
28
-
29
- // Import Session API handler (Mobile Chat)
30
- const { createSessionHandler } = require('./mobile/session-api.js');
31
-
32
- // Import WebSocket handler (Mobile Chat - V2 with MessageRouter)
33
- const { createWebSocketHandler } = require('./mobile/websocket-handler.js');
34
-
35
- // Import Session Manager
36
- const { SessionManager } = require('./mobile/session-manager.js');
37
-
38
- // Create session manager (shared between REST API and WebSocket)
39
- const sessionManager = new SessionManager();
40
- const sessionHandler = createSessionHandler(sessionManager);
41
-
42
- // Configuration
43
- const DEFAULT_PORT = parseInt(process.env.MAMA_HTTP_PORT, 10) || 3847;
44
- const HOST = '127.0.0.1'; // localhost only for security
45
-
46
- // Port file for clients to discover the server
47
- const PORT_FILE = path.join(process.env.HOME || '/tmp', '.mama-embedding-port');
48
-
49
- // Track if model is loaded
50
- let modelLoaded = false;
51
-
52
- // Track server instance for shutdown
53
- let _serverInstance = null;
54
- let _hasChatCapability = false;
55
-
56
- // Graph handler (optional, passed from standalone)
57
- let graphHandler = null;
58
-
59
- /**
60
- * Read request body as JSON
61
- */
62
- function readBody(req) {
63
- return new Promise((resolve, reject) => {
64
- let data = '';
65
- req.on('data', (chunk) => (data += chunk));
66
- req.on('end', () => {
67
- try {
68
- resolve(JSON.parse(data));
69
- } catch (e) {
70
- reject(new Error('Invalid JSON'));
71
- }
72
- });
73
- req.on('error', reject);
74
- });
75
- }
76
-
77
- /**
78
- * HTTP request handler
79
- */
80
- async function handleRequest(req, res) {
81
- // CORS headers for local requests
82
- // Security Note: CORS '*' is safe here because:
83
- // 1. Server binds to localhost only (127.0.0.1)
84
- // 2. No sensitive data exposed (user's own decisions)
85
- // 3. Required for browser-based Graph Viewer
86
- res.setHeader('Content-Type', 'application/json');
87
- res.setHeader('Access-Control-Allow-Origin', '*');
88
-
89
- // Health check
90
- if (req.url === '/health' && req.method === 'GET') {
91
- res.writeHead(200);
92
- res.end(
93
- JSON.stringify({
94
- status: 'ok',
95
- modelLoaded,
96
- model: getModelName(),
97
- dim: getEmbeddingDim(),
98
- chatEnabled: _hasChatCapability,
99
- })
100
- );
101
- return;
102
- }
103
-
104
- // SECURITY P1: Shutdown endpoint with token authentication (for Standalone takeover)
105
- if (req.url === '/shutdown' && req.method === 'POST') {
106
- const providedToken = req.headers['x-shutdown-token'];
107
- if (providedToken !== SHUTDOWN_TOKEN) {
108
- console.error('[EmbeddingHTTP] SECURITY: Invalid shutdown token rejected');
109
- res.writeHead(401);
110
- res.end(JSON.stringify({ error: 'Invalid shutdown token' }));
111
- return;
112
- }
113
- console.error('[EmbeddingHTTP] Shutdown requested (Standalone takeover, token validated)');
114
- res.writeHead(200);
115
- res.end(JSON.stringify({ status: 'shutting_down' }));
116
- // Graceful shutdown after response - close WebSocket first, then HTTP
117
- setTimeout(() => {
118
- // Close WebSocket server first to release connections
119
- if (_wssInstance) {
120
- _wssInstance.clients?.forEach((client) => {
121
- try {
122
- client.terminate();
123
- } catch {
124
- // Ignore termination errors
125
- }
126
- });
127
- _wssInstance.close(() => {
128
- console.error('[EmbeddingHTTP] WebSocket server closed');
129
- });
130
- }
131
- // Then close HTTP server
132
- if (_serverInstance) {
133
- _serverInstance.close(() => {
134
- console.error('[EmbeddingHTTP] Server closed for Standalone takeover');
135
- cleanupPortFile();
136
- });
137
- }
138
- }, 100);
139
- return;
140
- }
141
-
142
- // Embedding endpoint
143
- if (req.url === '/embed' && req.method === 'POST') {
144
- try {
145
- const startTime = Date.now();
146
- const body = await readBody(req);
147
-
148
- if (!body.text || typeof body.text !== 'string') {
149
- res.writeHead(400);
150
- res.end(JSON.stringify({ error: 'Missing or invalid "text" field' }));
151
- return;
152
- }
153
-
154
- const embedding = await generateEmbedding(body.text);
155
- modelLoaded = true;
156
- const latency = Date.now() - startTime;
157
-
158
- res.writeHead(200);
159
- res.end(
160
- JSON.stringify({
161
- embedding: Array.from(embedding),
162
- dim: embedding.length,
163
- latency,
164
- })
165
- );
166
- } catch (error) {
167
- console.error(`[EmbeddingHTTP] Error: ${error.message}`);
168
- res.writeHead(500);
169
- res.end(JSON.stringify({ error: error.message }));
170
- }
171
- return;
172
- }
173
-
174
- // Batch embedding endpoint
175
- if (req.url === '/embed/batch' && req.method === 'POST') {
176
- try {
177
- const startTime = Date.now();
178
- const body = await readBody(req);
179
-
180
- if (!Array.isArray(body.texts)) {
181
- res.writeHead(400);
182
- res.end(JSON.stringify({ error: 'Missing or invalid "texts" array' }));
183
- return;
184
- }
185
-
186
- const embeddings = await Promise.all(
187
- body.texts.map((text) => generateEmbedding(text).then((e) => Array.from(e)))
188
- );
189
- modelLoaded = true;
190
- const latency = Date.now() - startTime;
191
-
192
- res.writeHead(200);
193
- res.end(
194
- JSON.stringify({
195
- embeddings,
196
- count: embeddings.length,
197
- latency,
198
- })
199
- );
200
- } catch (error) {
201
- console.error(`[EmbeddingHTTP] Batch error: ${error.message}`);
202
- res.writeHead(500);
203
- res.end(JSON.stringify({ error: error.message }));
204
- }
205
- return;
206
- }
207
-
208
- // Session API routes (Mobile Chat)
209
- try {
210
- const sessionHandled = await sessionHandler(req, res);
211
- if (sessionHandled) {
212
- return;
213
- }
214
- } catch (error) {
215
- console.error(`[EmbeddingHTTP] Session API error: ${error.message}`);
216
- res.writeHead(500);
217
- res.end(JSON.stringify({ error: error.message }));
218
- return;
219
- }
220
-
221
- // Graph API routes (Viewer, if graphHandler provided)
222
- if (graphHandler) {
223
- try {
224
- const graphHandled = await graphHandler(req, res);
225
- if (graphHandled) {
226
- return;
227
- }
228
- } catch (error) {
229
- console.error(`[EmbeddingHTTP] Graph API error: ${error.message}`);
230
- res.writeHead(500);
231
- res.end(JSON.stringify({ error: error.message }));
232
- return;
233
- }
234
- }
235
-
236
- // Viewer fallback when graphHandler not provided (MCP-only mode)
237
- const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
238
- const isViewerPath =
239
- url.pathname === '/' ||
240
- url.pathname === '/viewer' ||
241
- url.pathname.startsWith('/viewer/') ||
242
- url.pathname === '/graph' ||
243
- url.pathname.startsWith('/graph/');
244
-
245
- if (isViewerPath && !graphHandler) {
246
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
247
- res.end(`<!DOCTYPE html>
248
- <html>
249
- <head><title>MAMA - Standalone Required</title></head>
250
- <body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #1a1a2e; color: #eee;">
251
- <div style="text-align: center; padding: 2rem;">
252
- <h1>🧠 MAMA Viewer</h1>
253
- <p>Viewer와 Chat 기능은 <strong>Standalone</strong> 모드에서만 사용 가능합니다.</p>
254
- <p style="color: #888; margin-top: 1rem;">Start Standalone: <code style="background: #333; padding: 0.25rem 0.5rem; border-radius: 4px;">mama start</code></p>
255
- <p style="color: #666; font-size: 0.9rem; margin-top: 2rem;">현재: MCP Server (embedding only)</p>
256
- </div>
257
- </body>
258
- </html>`);
259
- return;
260
- }
261
-
262
- // 404 for unknown routes
263
- res.writeHead(404);
264
- res.end(JSON.stringify({ error: 'Not found' }));
265
- }
266
-
267
- /**
268
- * Write port file for client discovery
269
- */
270
- function writePortFile(port) {
271
- try {
272
- fs.writeFileSync(PORT_FILE, String(port));
273
- console.error(`[EmbeddingHTTP] Port file written: ${PORT_FILE}`);
274
- } catch (e) {
275
- console.error(`[EmbeddingHTTP] Failed to write port file: ${e.message}`);
276
- }
277
- }
278
-
279
- /**
280
- * Clean up port file on exit
281
- */
282
- function cleanupPortFile() {
283
- try {
284
- if (fs.existsSync(PORT_FILE)) {
285
- fs.unlinkSync(PORT_FILE);
286
- }
287
- } catch (e) {
288
- // Ignore cleanup errors
289
- }
290
- }
291
-
292
- // Track WebSocket server instance
293
- let _wssInstance = null;
294
-
295
- /**
296
- * Start the HTTP embedding server
297
- *
298
- * @param {number} port - Port to listen on (default: 3847)
299
- * @returns {Promise<http.Server>} HTTP server instance
300
- */
301
- async function startEmbeddingServer(port = DEFAULT_PORT, options = {}) {
302
- const { messageRouter, sessionStore, graphHandler: graphHandlerOption } = options;
303
-
304
- // Track chat capability (Standalone provides messageRouter)
305
- _hasChatCapability = !!(messageRouter && sessionStore);
306
-
307
- // Store graph handler if provided
308
- if (graphHandlerOption) {
309
- graphHandler = graphHandlerOption;
310
- console.error('[EmbeddingHTTP] Graph handler registered (Viewer routes on port 3847)');
311
- }
312
- // Check if HTTP server is disabled
313
- if (process.env.MAMA_DISABLE_HTTP_SERVER === 'true') {
314
- console.error('[EmbeddingHTTP] HTTP server disabled via MAMA_DISABLE_HTTP_SERVER');
315
- console.error('[EmbeddingHTTP] Graph Viewer and Mobile Chat will not be available');
316
- return null;
317
- }
318
-
319
- // Initialize database for graph API
320
- await initDB();
321
-
322
- // Initialize session manager
323
- await sessionManager.initDB();
324
-
325
- return new Promise((resolve, reject) => {
326
- const server = http.createServer(handleRequest);
327
-
328
- server.on('error', (error) => {
329
- if (error.code === 'EADDRINUSE') {
330
- console.error(
331
- `[EmbeddingHTTP] Port ${port} already in use, assuming another instance is running`
332
- );
333
- // Not a fatal error - another server instance may be running
334
- resolve(null);
335
- } else if (error.code === 'EPERM' || error.code === 'EACCES') {
336
- console.error(
337
- `[EmbeddingHTTP] Permission denied opening port ${port}, skipping HTTP embedding server (sandboxed environment)`
338
- );
339
- // Some environments block listening on localhost; keep MCP server running without HTTP embeddings
340
- resolve(null);
341
- } else {
342
- reject(error);
343
- }
344
- });
345
-
346
- server.listen(port, HOST, () => {
347
- console.error(`[EmbeddingHTTP] Running at http://${HOST}:${port}`);
348
- // Note: Shutdown token not logged for security (use MAMA_DEBUG=true to enable)
349
- if (process.env.MAMA_DEBUG === 'true') {
350
- console.error(`[EmbeddingHTTP] Shutdown token: ${SHUTDOWN_TOKEN}`);
351
- }
352
- writePortFile(port);
353
-
354
- // Initialize WebSocket server (unless disabled)
355
- const skipWebSocket =
356
- process.env.MAMA_DISABLE_WEBSOCKET === 'true' ||
357
- process.env.MAMA_DISABLE_MOBILE_CHAT === 'true';
358
-
359
- if (!skipWebSocket) {
360
- try {
361
- if (messageRouter && sessionStore) {
362
- _wssInstance = createWebSocketHandler({
363
- httpServer: server,
364
- messageRouter,
365
- sessionStore,
366
- authToken: process.env.MAMA_AUTH_TOKEN,
367
- });
368
- console.error(
369
- `[EmbeddingHTTP] WebSocket server initialized (MessageRouter mode) at ws://${HOST}:${port}/ws`
370
- );
371
- } else {
372
- _wssInstance = require('./mobile/websocket-handler.js').createWebSocketHandler(
373
- server,
374
- sessionManager
375
- );
376
- console.error(
377
- `[EmbeddingHTTP] WebSocket server initialized (legacy mode) at ws://${HOST}:${port}/ws`
378
- );
379
- }
380
- } catch (wsError) {
381
- console.error(`[EmbeddingHTTP] WebSocket initialization failed: ${wsError.message}`);
382
- }
383
- }
384
-
385
- // Track server instance for shutdown
386
- _serverInstance = server;
387
- resolve(server);
388
- });
389
- });
390
- }
391
-
392
- /**
393
- * Pre-warm the embedding model
394
- */
395
- async function warmModel() {
396
- console.error('[EmbeddingHTTP] Pre-warming embedding model...');
397
- const startTime = Date.now();
398
-
399
- try {
400
- await generateEmbedding('MAMA warmup initialization');
401
- modelLoaded = true;
402
- const latency = Date.now() - startTime;
403
- console.error(`[EmbeddingHTTP] Model warmed in ${latency}ms`);
404
- return { success: true, latency };
405
- } catch (error) {
406
- console.error(`[EmbeddingHTTP] Model warmup failed: ${error.message}`);
407
- return { success: false, error: error.message };
408
- }
409
- }
410
-
411
- // Cleanup on exit
412
- process.on('exit', cleanupPortFile);
413
- process.on('SIGTERM', cleanupPortFile);
414
- process.on('SIGINT', cleanupPortFile);
415
-
416
- module.exports = {
417
- startEmbeddingServer,
418
- warmModel,
419
- cleanupPortFile,
420
- PORT_FILE,
421
- DEFAULT_PORT,
422
- HOST,
423
- SHUTDOWN_TOKEN,
424
- };
@@ -1,160 +0,0 @@
1
- /**
2
- * @fileoverview Authentication module - token-based auth for external access
3
- * @module mobile/auth
4
- * @version 1.5.0
5
- *
6
- * Provides authentication for requests from outside localhost.
7
- * Uses MAMA_AUTH_TOKEN environment variable for simple token auth.
8
- *
9
- * @example
10
- * const { authenticate, isLocalhost } = require('./auth');
11
- * if (!authenticate(req)) {
12
- * res.writeHead(401);
13
- * res.end('Unauthorized');
14
- * }
15
- */
16
-
17
- /**
18
- * Environment variable for auth token
19
- * @type {string|undefined}
20
- */
21
- const AUTH_TOKEN = process.env.MAMA_AUTH_TOKEN;
22
-
23
- /**
24
- * Track if we've warned about missing auth token
25
- * @type {boolean}
26
- */
27
- let hasWarnedAboutToken = false;
28
-
29
- /**
30
- * Check if request is from localhost
31
- * @param {http.IncomingMessage} req - HTTP request
32
- * @returns {boolean} True if from localhost
33
- */
34
- function isLocalhost(req) {
35
- const remoteAddress = req.socket?.remoteAddress || req.connection?.remoteAddress;
36
- return (
37
- remoteAddress === '127.0.0.1' || remoteAddress === '::1' || remoteAddress === '::ffff:127.0.0.1'
38
- );
39
- }
40
-
41
- /**
42
- * Authenticate an HTTP request
43
- * @param {http.IncomingMessage} req - HTTP request
44
- * @returns {boolean} True if authenticated
45
- */
46
- function authenticate(req) {
47
- // Localhost always allowed
48
- if (isLocalhost(req)) {
49
- return true;
50
- }
51
-
52
- // External access detected - show security warning
53
- if (!hasWarnedAboutToken) {
54
- console.error('');
55
- console.error('⚠️ ========================================');
56
- console.error('⚠️ SECURITY WARNING: External access detected!');
57
- console.error('⚠️ ========================================');
58
- console.error('⚠️ ');
59
- console.error('⚠️ Your MAMA server is being accessed from outside localhost.');
60
- console.error('⚠️ This likely means you are using a tunnel (ngrok, Cloudflare, etc.)');
61
- console.error('⚠️ ');
62
-
63
- if (!AUTH_TOKEN) {
64
- console.error('⚠️ ❌ CRITICAL: MAMA_AUTH_TOKEN is NOT set!');
65
- console.error('⚠️ Anyone with your tunnel URL can access your:');
66
- console.error('⚠️ - Chat sessions with Claude Code');
67
- console.error('⚠️ - Decision database (~/.claude/mama-memory.db)');
68
- console.error('⚠️ - Local file system (via Claude Code)');
69
- console.error('⚠️ ');
70
- console.error('⚠️ To secure your server, set MAMA_AUTH_TOKEN:');
71
- console.error('⚠️ export MAMA_AUTH_TOKEN="your-secret-token"');
72
- console.error('⚠️ ');
73
- } else {
74
- console.error('⚠️ ✅ MAMA_AUTH_TOKEN is set (authentication enabled)');
75
- console.error('⚠️ External clients must provide token in:');
76
- console.error('⚠️ - Authorization: Bearer <token> header, OR');
77
- console.error('⚠️ - ?token=<token> query parameter');
78
- console.error('⚠️ ');
79
- }
80
-
81
- console.error('⚠️ To disable external access entirely:');
82
- console.error('⚠️ export MAMA_DISABLE_HTTP_SERVER=true');
83
- console.error('⚠️ export MAMA_DISABLE_MOBILE_CHAT=true');
84
- console.error('⚠️ ');
85
- console.error('⚠️ ========================================');
86
- console.error('');
87
-
88
- hasWarnedAboutToken = true;
89
- }
90
-
91
- // External access requires token
92
- if (!AUTH_TOKEN) {
93
- console.error(
94
- `[Auth] External access denied from ${req.socket?.remoteAddress} (no MAMA_AUTH_TOKEN)`
95
- );
96
- return false;
97
- }
98
-
99
- // Check Authorization header
100
- const authHeader = req.headers['authorization'];
101
- if (authHeader?.startsWith('Bearer ')) {
102
- const token = authHeader.slice(7);
103
- if (token === AUTH_TOKEN) {
104
- console.error(
105
- `[Auth] External access granted via Bearer token from ${req.socket?.remoteAddress}`
106
- );
107
- return true;
108
- }
109
- }
110
-
111
- // Check URL query parameter
112
- const url = new URL(req.url, `http://${req.headers.host}`);
113
- const queryToken = url.searchParams.get('token');
114
- if (queryToken === AUTH_TOKEN) {
115
- console.error(
116
- `[Auth] External access granted via query token from ${req.socket?.remoteAddress}`
117
- );
118
- return true;
119
- }
120
-
121
- console.error(`[Auth] External access denied from ${req.socket?.remoteAddress} (invalid token)`);
122
- return false;
123
- }
124
-
125
- /**
126
- * Authenticate a WebSocket upgrade request
127
- * @param {http.IncomingMessage} req - HTTP upgrade request
128
- * @param {WebSocket} ws - WebSocket connection
129
- * @returns {boolean} True if authenticated, closes ws if not
130
- */
131
- function authenticateWebSocket(req, ws) {
132
- if (!authenticate(req)) {
133
- ws.close(4001, 'Authentication required');
134
- return false;
135
- }
136
- return true;
137
- }
138
-
139
- /**
140
- * Create authentication middleware for HTTP routes
141
- * @returns {Function} Middleware function
142
- */
143
- function createAuthMiddleware() {
144
- return (req, res, next) => {
145
- if (!authenticate(req)) {
146
- res.writeHead(401, { 'Content-Type': 'application/json' });
147
- res.end(JSON.stringify({ error: 'Unauthorized' }));
148
- return;
149
- }
150
- next();
151
- };
152
- }
153
-
154
- module.exports = {
155
- authenticate,
156
- authenticateWebSocket,
157
- createAuthMiddleware,
158
- isLocalhost,
159
- AUTH_TOKEN,
160
- };