#!/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
${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 ? `
| Recipient | Time | Size |
${rows.join('')}
` : '
📭 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); });