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 { handleWsMessage } from './wsHandler.js'; import { sessionStore, initStoreConfig } from './sessionStore.js'; import { SUPABASE_URL, SUPABASE_ANON_KEY } from './config.js'; import { safeSend } from './helpers.js'; export { SUPABASE_URL, SUPABASE_ANON_KEY }; export { LIGHTNING_BASE, PUBLIC_URL } from './config.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); initStoreConfig(SUPABASE_URL, SUPABASE_ANON_KEY); export const supabaseAnon = createClient(SUPABASE_URL, SUPABASE_ANON_KEY); const PORT = process.env.PORT || 7860; const app = express(); app.use(express.static(path.join(__dirname, '..', 'public'))); app.use(express.json({ limit: '10mb' })); // Require turnstile cookie for API routes (except the verification endpoint itself) 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' }); }); 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' }); } }); // Turnstile verification endpoint - accepts token and verifies with Cloudflare app.post('/api/turnstile', async (req, res) => { try { const token = req.body?.token; const secret = process.env.TURNSTILE_SECRET_KEY; if (!token || !secret) return res.status(400).json({ error: 'Missing token or server not configured' }); const params = new URLSearchParams(); params.append('secret', secret); params.append('response', token); if (req.ip) params.append('remoteip', req.ip); const r = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { method: 'POST', body: params, }); const j = await r.json(); if (j?.success) { // Set a short-lived cookie to allow access to other endpoints 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' }); } }); app.get('*', (req, res) => { if (!req.path.startsWith('/api/')) res.sendFile(path.join(__dirname, '..', 'public', 'index.html')); }); 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'; // Mark verified if request included a turnstile cookie 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', () => { const c = wsClients.get(ws); if (c?.userId) sessionStore.markOffline(c.userId, ws); wsClients.delete(ws); }); ws.on('error', () => wsClients.delete(ws)); safeSend(ws, { type: 'connected', tempId: wsClients.get(ws)?.tempId }); }); httpServer.listen(PORT, '0.0.0.0', () => console.log(`InferencePort Web on port ${PORT}`));