#!/usr/bin/env node /** * SocialCrypt Server * =================== * * Dumb JSONL blob store for encrypted social messaging. * Works everywhere: bare metal, Docker, HuggingFace Spaces, * Railway, Fly.io, Render, Glitch, etc. * * GET / → Web dashboard * GET /blobs.jsonl → All blobs (JSONL format) * POST /submit → Append { id, blob } (server sets ts) * GET /stats → Message count & size * GET /health → Health check * * Storage: Uses /data on HuggingFace Spaces (persistent volume), * falls back to local ./data everywhere else. */ const http = require('http'); const fs = require('fs'); const path = require('path'); // ─── Configuration ─────────────────────────────────────────────────── const PORT = parseInt(process.env.PORT) || 7860; const DATA_DIR = fs.existsSync('/data') ? '/data' : path.join(__dirname, 'data'); const BLOBS_FILE = path.join(DATA_DIR, 'blobs.jsonl'); // ─── Initialize ────────────────────────────────────────────────────── if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true }); console.log(`📁 Data directory: ${DATA_DIR}`); // ─── Helper: get stats ────────────────────────────────────────────── function getStats() { if (!fs.existsSync(BLOBS_FILE)) return { messages: 0, size_bytes: 0, size_human: '0 B' }; const stat = fs.statSync(BLOBS_FILE); const content = fs.readFileSync(BLOBS_FILE, 'utf-8').trim(); const lines = content ? content.split('\n').filter(Boolean) : []; const sizeKB = (stat.size / 1024).toFixed(1); return { messages: lines.length, size_bytes: stat.size, size_human: stat.size > 1048576 ? (stat.size / 1048576).toFixed(1) + ' MB' : sizeKB + ' KB' }; } // ─── HTTP Server ───────────────────────────────────────────────────── const server = http.createServer((req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, HEAD'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(200); return res.end(); } const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`); const p = url.pathname; // GET / — Dashboard if (req.method === 'GET' && p === '/') { const s = getStats(); const rows = []; if (s.messages > 0) { const lines = fs.readFileSync(BLOBS_FILE, 'utf-8').trim().split('\n').filter(Boolean).reverse().slice(0, 50); for (const line of lines) { try { const e = JSON.parse(line); rows.push(`${(e.id||'?').substring(0,16)}...${e.ts ? new Date(e.ts).toLocaleString() : '?'}${e.blob ? (e.blob.length/1024).toFixed(1)+' KB' : '?'}`); } catch {} } } res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); return res.end(` 🔐 SocialCrypt Server

🔐 SocialCrypt

📊 Stats

${s.messages}
Messages
${s.size_human}
Size
${DATA_DIR === '/data' ? '/data' : './data'}
Storage

📡 Endpoints

GET /blobs.jsonl — Read all blobs

POST /submit — Submit {id, blob}

GET /stats — JSON stats

GET /health — Health check

📬 Messages ${s.messages > 0 ? '('+Math.min(s.messages,50)+' of '+s.messages+')' : ''}

${s.messages > 0 ? `
${rows.join('')}
RecipientTimeSize
` : '

📭 No messages yet

'}
`); } // GET /blobs.jsonl — Stream blobs if (req.method === 'GET' && p === '/blobs.jsonl') { const s = getStats(); res.writeHead(200, { 'Content-Type': 'application/x-ndjson', 'X-Total-Messages': String(s.messages) }); if (fs.existsSync(BLOBS_FILE)) return fs.createReadStream(BLOBS_FILE).on('error', () => res.end()).pipe(res); return res.end(''); } // POST /submit — Append blob if (req.method === 'POST' && p === '/submit') { let body = ''; req.on('data', c => body += c); req.on('end', () => { try { const data = JSON.parse(body); if (!data.id || !data.blob) { res.writeHead(400); return res.end(JSON.stringify({error:'Missing id or blob'})); } const line = JSON.stringify({ id: data.id, ts: Date.now(), blob: data.blob }) + '\n'; if (!fs.existsSync(path.dirname(BLOBS_FILE))) fs.mkdirSync(path.dirname(BLOBS_FILE), { recursive: true }); fs.appendFileSync(BLOBS_FILE, line, 'utf-8'); console.log(`📨 Stored for ${data.id.substring(0,16)}... | Total: ${getStats().messages}`); res.writeHead(201, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: true })); } catch (e) { res.writeHead(400); res.end(JSON.stringify({error:'Invalid JSON: '+e.message})); } }); return; } // GET /stats if (req.method === 'GET' && p === '/stats') { res.writeHead(200, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ ...getStats(), data_dir: DATA_DIR, port: PORT })); } // GET /health if (req.method === 'GET' && p === '/health') { let writable = false; try { fs.accessSync(path.dirname(BLOBS_FILE), fs.constants.W_OK); writable = true; } catch {} res.writeHead(writable ? 200 : 503, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ status: writable ? 'ok' : 'degraded', messages: getStats().messages })); } // 404 res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('SocialCrypt — try /blobs.jsonl, /submit, /stats, /health'); }); // ─── Startup ───────────────────────────────────────────────────────── server.listen(PORT, '0.0.0.0', () => { console.log(`┌──────────────────────────────────┐`); console.log(`│ 🔐 SocialCrypt Server │`); console.log(`│ 📡 http://0.0.0.0:${String(PORT).padEnd(5)} │`); console.log(`│ 💾 ${String(DATA_DIR).padEnd(25)}│`); console.log(`│ 📬 ${String(getStats().messages).padStart(5)} messages │`); console.log(`└──────────────────────────────────┘`); }); process.on('SIGTERM', () => { server.close(() => process.exit(0)); setTimeout(() => process.exit(1), 5000); }); process.on('SIGINT', () => { server.close(() => process.exit(0)); setTimeout(() => process.exit(1), 5000); });