@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.
- package/package.json +33 -21
- package/src/config-loader.js +0 -218
- package/src/db-adapter/README.md +0 -110
- package/src/db-adapter/base-adapter.js +0 -91
- package/src/db-adapter/index.js +0 -31
- package/src/db-adapter/sqlite-adapter.js +0 -364
- package/src/db-adapter/statement.js +0 -127
- package/src/db-manager.js +0 -671
- package/src/debug-logger.js +0 -86
- package/src/decision-formatter.js +0 -1276
- package/src/decision-tracker.js +0 -621
- package/src/embedding-cache.js +0 -222
- package/src/embedding-client.js +0 -141
- package/src/embedding-server/index.js +0 -424
- package/src/embedding-server/mobile/auth.js +0 -160
- package/src/embedding-server/mobile/daemon.js +0 -313
- package/src/embedding-server/mobile/output-parser.js +0 -281
- package/src/embedding-server/mobile/session-api.js +0 -279
- package/src/embedding-server/mobile/session-manager.js +0 -377
- package/src/embedding-server/mobile/websocket-handler.js +0 -443
- package/src/embeddings.js +0 -305
- package/src/errors.js +0 -326
- package/src/index.js +0 -41
- package/src/mama-api.js +0 -2614
- package/src/memory-inject.js +0 -174
- package/src/memory-store.js +0 -89
- package/src/notification-manager.js +0 -3
- package/src/ollama-client.js +0 -391
- package/src/outcome-tracker.js +0 -351
- package/src/progress-indicator.js +0 -110
- package/src/query-intent.js +0 -237
- package/src/relevance-scorer.js +0 -286
- package/src/tier-validator.js +0 -269
- package/src/time-formatter.js +0 -98
|
@@ -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
|
-
};
|