import express from 'express'; import { createServer } from 'http'; import { WebSocketServer } from 'ws'; import { createClient } from '@supabase/supabase-js'; import crypto from 'crypto'; import path from 'path'; import { fileURLToPath } from 'url'; import fetch from 'node-fetch'; import rateLimit from 'express-rate-limit'; import fs from 'fs'; import { registerFeedbackRoutes } from './handleFeedback.js'; import { SUPABASE_URL, SUPABASE_ANON_KEY } from './config.js'; import { safeSend } from './helpers.js'; import { getPostgresBackendType, initializePostgresStorage } from './postgres.js'; import { POSTGRES_STORAGE_DB_DIR, POSTGRES_STORAGE_DIR, refreshStorageMode } from './dataPaths.js'; import { materializeEmbeddedPostgresStorage, migrateLegacyDataToPostgres } from './postgresMigration.js'; import { loadStoredSHA, saveStoredSHA } from './versionStore.js'; export { SUPABASE_URL, SUPABASE_ANON_KEY }; export { LIGHTNING_BASE, PUBLIC_URL } from './config.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); export const supabaseAnon = createClient(SUPABASE_URL, SUPABASE_ANON_KEY); const PORT = process.env.PORT || 7860; const app = express(); const GITHUB_REPO = 'incognitolm/InferencePort-Pages'; const CDN_BASE = `https://cdn.jsdelivr.net/gh/${GITHUB_REPO}`; let latestSHA = null; const ADMIN_TOKEN = process.env.ADMIN_TOKEN || 'supersecret'; const TURNSTILE_SITE_KEY = process.env.TURNSTILE_SITE_KEY || '0x4AAAAAAC1ZXKIhZ9Kdz8j9'; const MAX_TEXT_UPLOAD_BYTES = 100 * 1024; const LOCAL_UI_DIR = [ process.env.UI_LOCAL_PATH, path.resolve(__dirname, '..', '..', 'InferencePort-Pages'), path.resolve(process.cwd(), '..', 'InferencePort-Pages'), ].filter(Boolean).find((dir) => { try { return fs.existsSync(path.join(dir, 'index.html')); } catch { return false; } }) || null; // Rate limiter for admin endpoints (5 attempts per IP per minute) const verifyLimiter = rateLimit({ windowMs: 60 * 1000, max: 5, standardHeaders: true, legacyHeaders: false, handler: (req, res) => { logAdminEvent(req, 'rate_limited'); res.status(429).json({ error: 'rate_limited' }); }, }); const PUBLIC_URL = process.env.PUBLIC_URL || 'default'; async function ensureStorageBackendReady() { let mode = refreshStorageMode(); const autoBootstrap = process.env.POSTGRES_AUTO_BOOTSTRAP !== '0'; if (mode !== 'postgres' && autoBootstrap) { console.log(`[storage] PostgreSQL storage folder missing at ${POSTGRES_STORAGE_DIR}; starting automatic bootstrap.`); const report = await migrateLegacyDataToPostgres({ skipIfFolderExists: true }); mode = refreshStorageMode(); if (report?.skipped) { console.log(`[storage] PostgreSQL storage folder became available during bootstrap check.`); } else { console.log(`[storage] automatic PostgreSQL bootstrap ${report.status} at ${POSTGRES_STORAGE_DIR}`); } } if (mode === 'postgres') { const materialized = await materializeEmbeddedPostgresStorage(); if (materialized?.upgraded && materialized?.report) { console.log(`[storage] materialized embedded PostgreSQL database at ${POSTGRES_STORAGE_DB_DIR}`); } await initializePostgresStorage(); } const backend = mode === 'postgres' ? getPostgresBackendType() : 'legacy'; const target = mode === 'postgres' ? (backend === 'embedded' ? POSTGRES_STORAGE_DB_DIR : POSTGRES_STORAGE_DIR) : '/data'; console.log(`[storage] mode=${mode}${mode === 'postgres' ? ` backend=${backend}` : ''} target=${target}`); return mode; } await ensureStorageBackendReady(); const { sessionStore, initStoreConfig } = await import('./sessionStore.js'); const { abortActiveStream, handleWsMessage } = await import('./wsHandler.js'); const { verifySupabaseToken } = await import('./auth.js'); const { mediaStore } = await import('./mediaStore.js'); initStoreConfig(SUPABASE_URL, SUPABASE_ANON_KEY); function getRequestIp(req) { return (req.headers['x-forwarded-for'] || '').split(',')[0].trim() || req.socket?.remoteAddress || req.ip || 'unknown'; } function truncateForLog(value, max = 180) { const text = String(value ?? '').replace(/\s+/g, ' ').trim(); if (!text) return ''; return text.length > max ? `${text.slice(0, max - 1)}…` : text; } function extensionFromName(name = '') { const ext = path.extname(String(name || '')).toLowerCase(); return ext.startsWith('.') ? ext.slice(1) : ext; } function isTextLikeUpload(name = '', mimeType = '', kind = '') { const normalizedKind = String(kind || '').toLowerCase(); const mime = String(mimeType || '').toLowerCase(); const ext = extensionFromName(name); if (normalizedKind === 'text' || normalizedKind === 'rich_text') return true; if (mime.startsWith('text/')) return true; if (['application/json', 'application/javascript', 'application/xml'].includes(mime)) return true; return ['txt', 'md', 'json', 'js', 'ts', 'css', 'py', 'html', 'htm', 'xml', 'csv', 'rtf'].includes(ext); } function getCookieMap(req) { const cookies = (req.headers.cookie || '') .split(';') .map((value) => value.trim()) .filter(Boolean) .map((entry) => { const idx = entry.indexOf('='); if (idx === -1) return null; return [entry.slice(0, idx), entry.slice(idx + 1)]; }) .filter(Boolean); return Object.fromEntries(cookies); } function hasAdminTurnstile(req) { return getCookieMap(req).admin_turnstile === '1'; } function requireAdminTurnstile(req, res, next) { if (hasAdminTurnstile(req)) return next(); logAdminEvent(req, 'blocked_missing_turnstile'); return res.status(403).json({ error: 'turnstile:required' }); } function logAdminEvent(req, action, detail = null) { const parts = [ `[ADMIN ${new Date().toISOString()}]`, `action=${action}`, `ip=${getRequestIp(req)}`, `method=${req.method}`, `path=${truncateForLog(req.originalUrl || req.url, 220)}`, ]; const userAgent = truncateForLog(req.headers['user-agent'] || 'unknown', 140); if (userAgent) parts.push(`ua="${userAgent}"`); if (typeof detail === 'string' && detail.trim()) { parts.push(detail.trim()); } else if (detail && typeof detail === 'object') { Object.entries(detail).forEach(([key, value]) => { if (value === undefined || value === null || value === '') return; parts.push(`${key}=${JSON.stringify(String(value))}`); }); } console.log(parts.join(' | ')); } function respondTextUploadTooLarge(res) { return res.status(413).json({ error: 'media:text_too_large', message: 'Text files must be 100 KB or smaller.', }); } async function verifyTurnstileToken(token, remoteIp) { const secret = process.env.TURNSTILE_SECRET_KEY; if (!token || !secret) throw new Error('Missing token or server not configured'); const params = new URLSearchParams(); params.append('secret', secret); params.append('response', token); if (remoteIp) params.append('remoteip', remoteIp); const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { method: 'POST', body: params, }); return response.json(); } app.use(express.json({ limit: '10mb' })); // --- API Turnstile Protection --- app.use('/api', (req, res, next) => { const exempt = ['/turnstile', '/health']; if (exempt.includes(req.path)) return next(); const cookieHeader = req.headers.cookie || ''; if (cookieHeader.includes('turnstile=1')) return next(); return res.status(403).json({ error: 'turnstile:required' }); }); registerFeedbackRoutes(app, { requireAdminTurnstile, verifyLimiter, logAdminEvent, ADMIN_TOKEN, getRequestIp, }); async function getRequestOwner(req) { const authHeader = req.headers.authorization || ''; const accessToken = authHeader.startsWith('Bearer ') ? authHeader.slice(7).trim() : ''; if (accessToken) { const user = await verifySupabaseToken(accessToken); if (user) return { owner: { type: 'user', id: user.id }, accessToken }; } const tempId = String(req.headers['x-temp-id'] || req.query.tempId || '').trim(); if (tempId) { sessionStore.initTemp(tempId); return { owner: { type: 'guest', id: tempId }, accessToken: null }; } return null; } async function requireRequestOwner(req, res) { const resolved = await getRequestOwner(req); if (!resolved?.owner) { res.status(401).json({ error: 'auth:required' }); return null; } return resolved; } function requireSignedInMediaOwner(resolved, res, message = 'Sign in to upload files.') { if (resolved?.owner?.type === 'user') return true; res.status(403).json({ error: 'media:auth_required', message }); return false; } app.get('/health', (_req,res) => res.json({ok:true})); app.get('/api/share/:token', async (req,res) => { try { const shared = await sessionStore.resolveShareToken(req.params.token); if (!shared) return res.status(404).json({error:'Not found'}); const snap = shared.session_snapshot; res.json({ name: snap.name, preview: (snap.history || []).slice(0,6).map(m=>({ role: m.role, content: (typeof m.content==='string'?m.content:JSON.stringify(m.content)).slice(0,400), })), }); } catch { res.status(500).json({error:'Server error'}); } }); app.get('/api/media', async (req, res) => { const resolved = await requireRequestOwner(req, res); if (!resolved) return; try { const result = await mediaStore.list(resolved.owner, { view: req.query.view === 'trash' ? 'trash' : 'active', parentId: req.query.parentId ? String(req.query.parentId) : null, }); res.json(result); } catch (err) { console.error('media list error', err); res.status(500).json({ error: 'media:list_failed' }); } }); app.post('/api/media/upload', express.raw({ type: () => true, limit: '100mb' }), async (req, res) => { const resolved = await requireRequestOwner(req, res); if (!resolved) return; if (!requireSignedInMediaOwner(resolved, res, 'Sign in to upload files.')) return; try { const name = decodeURIComponent(String(req.headers['x-file-name'] || 'upload.bin')); const mimeType = String(req.headers['x-mime-type'] || 'application/octet-stream'); const parentId = req.headers['x-parent-id'] ? String(req.headers['x-parent-id']) : null; const sessionId = req.headers['x-session-id'] ? String(req.headers['x-session-id']) : null; const kindHeader = req.headers['x-file-kind'] ? String(req.headers['x-file-kind']) : null; const buffer = Buffer.isBuffer(req.body) ? req.body : Buffer.from(req.body || []); if (isTextLikeUpload(name, mimeType, kindHeader) && buffer.byteLength > MAX_TEXT_UPLOAD_BYTES) { return respondTextUploadTooLarge(res); } const item = await mediaStore.storeBuffer(resolved.owner, { name, mimeType, buffer, parentId, sessionId, source: 'user_upload', kind: kindHeader || null, }); const usage = await mediaStore.getUsage(resolved.owner); res.json({ item, usage }); } catch (err) { console.error('media upload error', err); res.status(err.status || 500).json({ error: err.code || 'media:upload_failed', message: err.message || 'Upload failed', usage: err.usage || null, }); } }); app.post('/api/media/folders', async (req, res) => { const resolved = await requireRequestOwner(req, res); if (!resolved) return; if (!requireSignedInMediaOwner(resolved, res, 'Sign in to create folders.')) return; try { const item = await mediaStore.createFolder(resolved.owner, { name: req.body?.name || 'New Folder', parentId: req.body?.parentId || null, }); const usage = await mediaStore.getUsage(resolved.owner); res.json({ item, usage }); } catch (err) { console.error('media create folder error', err); res.status(500).json({ error: 'media:create_folder_failed' }); } }); app.post('/api/media/documents', async (req, res) => { const resolved = await requireRequestOwner(req, res); if (!resolved) return; if (!requireSignedInMediaOwner(resolved, res, 'Sign in to create documents.')) return; try { const richText = !!req.body?.richText; const content = String(req.body?.content || ''); if (Buffer.byteLength(content, 'utf8') > MAX_TEXT_UPLOAD_BYTES) { return respondTextUploadTooLarge(res); } const item = await mediaStore.createDocument(resolved.owner, { name: req.body?.name, parentId: req.body?.parentId || null, richText, content, source: 'user_upload', sessionId: req.body?.sessionId || null, }); const usage = await mediaStore.getUsage(resolved.owner); res.json({ item, usage }); } catch (err) { console.error('media create document error', err); res.status(err.status || 500).json({ error: err.code || 'media:create_document_failed', message: err.message || 'Document creation failed', usage: err.usage || null, }); } }); app.post('/api/media/move', async (req, res) => { const resolved = await requireRequestOwner(req, res); if (!resolved) return; if (!requireSignedInMediaOwner(resolved, res, 'Sign in to move files.')) return; try { const items = await mediaStore.move(resolved.owner, req.body?.ids || [], req.body?.parentId || null); const usage = await mediaStore.getUsage(resolved.owner); res.json({ items, usage }); } catch (err) { console.error('media move error', err); res.status(500).json({ error: 'media:move_failed' }); } }); app.post('/api/media/trash', async (req, res) => { const resolved = await requireRequestOwner(req, res); if (!resolved) return; try { const items = await mediaStore.moveToTrash(resolved.owner, req.body?.ids || []); const usage = await mediaStore.getUsage(resolved.owner); res.json({ items, usage }); } catch (err) { console.error('media trash error', err); res.status(500).json({ error: 'media:trash_failed' }); } }); app.post('/api/media/restore', async (req, res) => { const resolved = await requireRequestOwner(req, res); if (!resolved) return; if (!requireSignedInMediaOwner(resolved, res, 'Sign in to restore files.')) return; try { const items = await mediaStore.restore(resolved.owner, req.body?.ids || []); const usage = await mediaStore.getUsage(resolved.owner); res.json({ items, usage }); } catch (err) { console.error('media restore error', err); res.status(500).json({ error: 'media:restore_failed' }); } }); app.post('/api/media/deleteForever', async (req, res) => { const resolved = await requireRequestOwner(req, res); if (!resolved) return; try { const ids = await mediaStore.deleteForever(resolved.owner, req.body?.ids || []); const usage = await mediaStore.getUsage(resolved.owner); res.json({ ids, usage }); } catch (err) { console.error('media delete forever error', err); res.status(500).json({ error: 'media:delete_failed' }); } }); app.get('/api/media/:id/text', async (req, res) => { const resolved = await requireRequestOwner(req, res); if (!resolved) return; try { const loaded = await mediaStore.readText(resolved.owner, req.params.id); if (!loaded) return res.status(404).json({ error: 'media:not_found' }); res.json({ item: loaded.entry, content: loaded.text }); } catch (err) { console.error('media read text error', err); res.status(500).json({ error: 'media:read_failed' }); } }); app.put('/api/media/:id/text', async (req, res) => { const resolved = await requireRequestOwner(req, res); if (!resolved) return; if (!requireSignedInMediaOwner(resolved, res, 'Sign in to edit files.')) return; try { const buffer = Buffer.from(String(req.body?.content || ''), 'utf8'); if (buffer.byteLength > MAX_TEXT_UPLOAD_BYTES) { return respondTextUploadTooLarge(res); } const item = await mediaStore.updateContent(resolved.owner, req.params.id, { buffer, mimeType: req.body?.mimeType || null, kind: req.body?.richText ? 'rich_text' : null, name: req.body?.name || null, }); if (!item) return res.status(404).json({ error: 'media:not_found' }); const usage = await mediaStore.getUsage(resolved.owner); res.json({ item, usage }); } catch (err) { console.error('media update text error', err); res.status(err.status || 500).json({ error: err.code || 'media:update_failed', message: err.message || 'Update failed', usage: err.usage || null, }); } }); app.post('/api/media/:id/rename', async (req, res) => { const resolved = await requireRequestOwner(req, res); if (!resolved) return; if (!requireSignedInMediaOwner(resolved, res, 'Sign in to rename files.')) return; try { const name = String(req.body?.name || '').trim(); if (!name) return res.status(400).json({ error: 'media:name_required', message: 'A file name is required.' }); const item = await mediaStore.rename(resolved.owner, req.params.id, name); if (!item) return res.status(404).json({ error: 'media:not_found' }); const usage = await mediaStore.getUsage(resolved.owner); res.json({ item, usage }); } catch (err) { console.error('media rename error', err); res.status(500).json({ error: 'media:rename_failed', message: err.message || 'Rename failed' }); } }); app.get('/api/media/:id/content', async (req, res) => { const resolved = await requireRequestOwner(req, res); if (!resolved) return; try { const loaded = await mediaStore.readBuffer(resolved.owner, req.params.id); if (!loaded) return res.status(404).json({ error: 'media:not_found' }); res.setHeader('Content-Type', loaded.entry.mimeType || 'application/octet-stream'); res.setHeader('Cache-Control', 'private, max-age=60'); if (req.query.download === '1') { res.setHeader('Content-Disposition', `attachment; filename="${loaded.entry.name}"`); } res.send(loaded.buffer); } catch (err) { console.error('media read binary error', err); res.status(500).json({ error: 'media:read_failed' }); } }); app.post('/api/turnstile', async (req,res)=>{ try{ const token = req.body?.token; const j = await verifyTurnstileToken(token, getRequestIp(req)); if(j?.success){ res.cookie('turnstile','1',{ maxAge:24*3600*1000, path:'/', sameSite:'lax'}); return res.json({success:true}); } return res.status(403).json({error:'Verification failed'}); }catch(e){ console.error('turnstile verify',e); return res.status(500).json({error:'Server error'});} }); async function fetchLatestSHA() { try { const res = await fetch(`https://api.github.com/repos/${GITHUB_REPO}/commits/main`); const data = await res.json(); latestSHA = data.sha; console.log(`[${PUBLIC_URL}] Updated latest SHA: ${latestSHA}`); await saveStoredSHA(PUBLIC_URL, latestSHA); } catch (e) { console.error('Failed to fetch latest commit SHA', e); } } latestSHA = await loadStoredSHA(PUBLIC_URL); if (!latestSHA) { console.log(`[${PUBLIC_URL}] No stored SHA found.`); latestSHA = "latest"; await saveStoredSHA(PUBLIC_URL, latestSHA); } else { console.log(`[${PUBLIC_URL}] Using stored SHA: ${latestSHA}`); } // --- Admin endpoints --- app.get('/admin.html', async (req, res) => { logAdminEvent(req, 'page_view'); if (serveLocalUiFile('/admin.html', res)) return; if (!latestSHA) return res.status(500).send('Server not ready'); const url = `${CDN_BASE}@${latestSHA}/admin.html`; try { const response = await fetch(url); if (!response.ok) return res.status(404).send('File not found'); const text = await response.text(); res.setHeader('Content-Type', 'text/html'); res.send(text); } catch (e) { console.error('Error fetching admin.html from CDN:', e); res.status(500).send('Server error'); } }); app.get('/admin/config', (req, res) => { logAdminEvent(req, 'config_view', { verified: hasAdminTurnstile(req) }); res.json({ siteKey: TURNSTILE_SITE_KEY || null, verified: hasAdminTurnstile(req), }); }); app.post('/admin/turnstile', async (req, res) => { try { const token = req.body?.token; const result = await verifyTurnstileToken(token, getRequestIp(req)); if (!result?.success) { logAdminEvent(req, 'turnstile_failed', { errorCodes: Array.isArray(result?.['error-codes']) ? result['error-codes'].join(',') : '', }); return res.status(403).json({ error: 'Verification failed' }); } res.cookie('admin_turnstile', '1', { maxAge: 2 * 3600 * 1000, path: '/', sameSite: 'lax' }); logAdminEvent(req, 'turnstile_verified'); return res.json({ success: true }); } catch (err) { console.error('admin turnstile verify', err); logAdminEvent(req, 'turnstile_error', { message: err.message || 'unknown' }); return res.status(500).json({ error: err.message || 'Server error' }); } }); app.get('/admin/verify', requireAdminTurnstile, verifyLimiter, (req,res)=>{ const token = req.query.token; const success = token===ADMIN_TOKEN; logAdminEvent(req, success ? 'login_success' : 'login_failed', { turnstile: hasAdminTurnstile(req), tokenProvided: !!token, }); res.json({success}); }); app.get('/admin/refresh', requireAdminTurnstile, verifyLimiter, async (req, res) => { const token = req.query.token; if (token !== ADMIN_TOKEN) { logAdminEvent(req, 'refresh_denied', { reason: 'bad_token' }); return res.status(403).send('Forbidden'); } const sha = req.query.sha?.trim(); if (sha) { if (!/^[0-9a-f]{7,40}$/.test(sha)) { logAdminEvent(req, 'set_sha_invalid', { requestedSha: sha }); return res.status(400).send('Invalid SHA'); } latestSHA = sha; await saveStoredSHA(PUBLIC_URL, latestSHA); logAdminEvent(req, 'set_sha', { requestedSha: latestSHA }); console.log(`[${PUBLIC_URL}] Manual SHA set by admin: ${latestSHA}`); return res.send(`Version set to commit ${latestSHA}`); } await fetchLatestSHA(); logAdminEvent(req, 'refresh_latest', { resolvedSha: latestSHA }); res.send(`Latest version refreshed: ${latestSHA}`); }); app.get('/admin/status', requireAdminTurnstile, verifyLimiter, async (req, res) => { const token = req.query.token; if (token !== ADMIN_TOKEN) { logAdminEvent(req, 'status_denied', { reason: 'bad_token' }); return res.status(403).json({ error: 'Forbidden' }); } logAdminEvent(req, 'status_view', { currentSha: latestSHA, servingMode: LOCAL_UI_DIR ? 'local-ui' : 'cdn', }); res.json({ publicUrl: PUBLIC_URL, currentSha: latestSHA, servingMode: LOCAL_UI_DIR ? 'local-ui' : 'cdn', localUiDir: LOCAL_UI_DIR, repo: GITHUB_REPO, uiSource: LOCAL_UI_DIR ? 'Local InferencePort-Pages checkout is active' : 'Serving from CDN SHA', }); }); // --- MIME type helper --- function getMimeType(filePath){ const ext = path.extname(filePath).toLowerCase(); switch(ext){ case '.html': return 'text/html'; case '.js': return 'application/javascript'; case '.css': return 'text/css'; case '.json': return 'application/json'; case '.png': return 'image/png'; case '.jpg': case '.jpeg': return 'image/jpeg'; case '.gif': return 'image/gif'; case '.svg': return 'image/svg+xml'; case '.ico': return 'image/x-icon'; default: return 'application/octet-stream'; } } function resolveLocalUiFile(filePath) { if (!LOCAL_UI_DIR) return null; const resolvedRoot = path.resolve(LOCAL_UI_DIR); const relativePath = String(filePath || '/index.html').replace(/^[/\\]+/, ''); const resolvedFile = path.resolve(resolvedRoot, relativePath); if (resolvedFile !== resolvedRoot && !resolvedFile.startsWith(`${resolvedRoot}${path.sep}`)) return null; if (!fs.existsSync(resolvedFile) || fs.statSync(resolvedFile).isDirectory()) return null; return resolvedFile; } function serveLocalUiFile(filePath, res) { const resolvedFile = resolveLocalUiFile(filePath); if (!resolvedFile) return false; res.setHeader('Content-Type', getMimeType(resolvedFile)); res.send(fs.readFileSync(resolvedFile)); return true; } // Hybrid serving: everything via latest SHA (auto-refresh / manual refresh) app.get('*', async (req,res)=>{ if(req.path.startsWith('/api/')) return res.status(404).send('Not found'); // All client files are fetched from latest SHA const filePath = req.path === '/' ? '/index.html' : req.path; if (serveLocalUiFile(filePath, res)) return; const url = `${CDN_BASE}@${latestSHA}${filePath}`; try{ const response = await fetch(url); if(!response.ok) return res.status(404).send('File not found'); const mimeType = getMimeType(filePath); res.setHeader('Content-Type', mimeType); // Stream text files; redirect others (images/fonts) optional if(mimeType.startsWith('text') || mimeType==='application/javascript' || mimeType==='application/json'){ const text = await response.text(); res.send(text); } else { const buffer = await response.arrayBuffer(); res.send(Buffer.from(buffer)); } }catch(e){ console.error('Error fetching from CDN:',e); res.status(500).send('Server error'); } }); // --- WebSocket --- const httpServer = createServer(app); const wss = new WebSocketServer({server:httpServer,path:'/ws'}); export const wsClients = new Map(); wss.on('connection',(ws,req)=>{ const ip = (req.headers['x-forwarded-for']||'').split(',')[0].trim()||req.socket.remoteAddress||'unknown'; const userAgent = req.headers['user-agent']||'unknown'; const cookies = (req.headers.cookie||'').split(';').map(s=>s.trim()).filter(Boolean); const cookieMap = Object.fromEntries(cookies.map(c=>{ const i=c.indexOf('='); return [c.slice(0,i),c.slice(i+1)];})); const verified = cookieMap.turnstile==='1'; wsClients.set(ws,{ tempId:crypto.randomUUID(), ip, userAgent, userId:null, authenticated:false, verified }); ws.on('message',async raw=>{ try{ await handleWsMessage(ws,JSON.parse(raw.toString()),wsClients);} catch(ex){ console.error("Invalid message error:",ex.message,"\nStack:",ex.stack); safeSend(ws,{type:'error',message:'Invalid message: '+ex.message}); } }); ws.on('close',()=>{ abortActiveStream(ws); const c=wsClients.get(ws); if(c?.userId) sessionStore.markOffline(c.userId,ws); wsClients.delete(ws); }); ws.on('error',()=>{ abortActiveStream(ws); wsClients.delete(ws); }); safeSend(ws,{type:'connected', tempId:wsClients.get(ws)?.tempId}); }); httpServer.listen(PORT,'0.0.0.0',()=>console.log(`Running on port ${PORT}`));