TOUSE / server.js
Manus Agent
Persist and protect cross-browser answer viewing
5d4cb85
Raw History Blame Contribute Delete
11.2 kB
import http from 'node:http';
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import { fileURLToPath } from 'node:url';
import { createServer as createViteServer } from 'vite';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const port = Number(process.env.PORT || 3000);
const isProduction = process.env.NODE_ENV === 'production';
const model = 'cohere/north-mini-code:free';
const storageDir = process.env.DATA_DIR || (fs.existsSync('/data') ? '/data' : path.join(__dirname, 'data'));
const submissionsFile = path.join(storageDir, 'submissions.json');
function loadSubmissions() {
try {
const parsed = JSON.parse(fs.readFileSync(submissionsFile, 'utf8'));
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
let submissions = loadSubmissions();
function persistSubmissions() {
fs.mkdirSync(storageDir, { recursive: true });
const tempFile = `${submissionsFile}.tmp`;
fs.writeFileSync(tempFile, JSON.stringify(submissions, null, 2));
fs.renameSync(tempFile, submissionsFile);
}
const fallbackReading = {
score: 58,
label: 'offline reflection — not a model read',
read: 'The requested model could not be reached, so this is only a rough local reflection. There may be care here, but a conversation still has to do the work that a score cannot.',
careSignal: 'unverified until it shows up in action',
confidence: 'low',
source: 'offline-fallback',
};
function sendJson(res, status, body) {
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
res.end(JSON.stringify(body));
}
function cleanString(value, max = 800) {
return typeof value === 'string' ? value.trim().slice(0, max) : '';
}
function parseModelJson(content) {
const stripped = content.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim();
const start = stripped.indexOf('{');
const end = stripped.lastIndexOf('}');
if (start === -1 || end === -1) throw new Error('Model response was not JSON');
return JSON.parse(stripped.slice(start, end + 1));
}
function normalizeReading(raw) {
const score = Math.max(0, Math.min(100, Math.round(Number(raw.score) || fallbackReading.score)));
return {
score,
label: cleanString(raw.label, 80) || fallbackReading.label,
read: cleanString(raw.read, 320) || fallbackReading.read,
careSignal: cleanString(raw.careSignal, 100) || fallbackReading.careSignal,
confidence: cleanString(raw.confidence, 30) || fallbackReading.confidence,
source: raw.source === 'offline-fallback' ? 'offline-fallback' : 'model',
};
}
function localReflection(payload) {
const text = `${cleanString(payload.apology, 700)} ${Array.isArray(payload.promises) ? payload.promises.join(' ') : ''}`.toLowerCase();
let score = 46;
if (text.length > 160) score += 8;
if (/(hurt|impact|carried|understand|listen|need|trust|show up|ordinary|defend)/.test(text)) score += 12;
if (/(miss you|lonely|i need you|come back|my pain|my heart)/.test(text)) score -= 5;
if (/(sorry|change|respect|pace|choice|choose)/.test(text)) score += 6;
return { ...fallbackReading, score: Math.max(35, Math.min(74, score)) };
}
function findSubmission(id) {
return submissions.find((submission) => submission.id === id);
}
function latestSubmission() {
return [...submissions].sort((a, b) => b.createdAt - a.createdAt)[0];
}
function passwordMatches(supplied) {
const expected = process.env.OWNER_VIEW_PASSWORD || '';
const value = cleanString(supplied, 200);
return Boolean(expected && value && Buffer.byteLength(expected) === Buffer.byteLength(value)
&& crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(value)));
}
async function rateSincerity(payload) {
const apiKey = process.env.OPENROUTER_API_KEY;
if (!apiKey) return localReflection(payload);
const system = `You are a careful relationship reflection assistant. You cannot detect lies or know intent; never claim certainty. Given an apology, promises, consent statement, and name, produce an imperfect impression of how specific, accountable, and other-aware the answer sounds. Do not reward romantic intensity alone. Penalize vague promises, self-pity, pressure, blame shifting, or centering only the apologizer's relief. Reward naming impact, concrete behavior change, patience, and respect for the other person's choice. Return ONLY valid JSON with these keys: score (integer 0-100), label (short lowercase phrase), read (2-3 warm but direct sentences), careSignal (short phrase), confidence (one of low, medium, high). Use a nuanced score; do not give 90+ unless the text is unusually specific and accountable.`;
const user = JSON.stringify({
name: cleanString(payload.name, 80),
promises: Array.isArray(payload.promises) ? payload.promises.slice(0, 3).map((item) => cleanString(item, 140)) : [],
apology: cleanString(payload.apology, 700),
consent: Boolean(payload.consent),
});
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://secondchance.manus.app',
'X-Title': 'Second Chance sincerity reflection',
},
body: JSON.stringify({
model,
temperature: 0.45,
max_tokens: 260,
messages: [{ role: 'system', content: system }, { role: 'user', content: user }],
}),
});
if (!response.ok) throw new Error(`Model request failed (${response.status})`);
const data = await response.json();
const content = data?.choices?.[0]?.message?.content;
if (typeof content !== 'string') throw new Error('Model response was empty');
return normalizeReading({ ...parseModelJson(content), source: 'model' });
}
async function readBody(req) {
let body = '';
for await (const chunk of req) {
body += chunk;
if (body.length > 12000) throw new Error('Request too large');
}
return JSON.parse(body || '{}');
}
async function handleApi(req, res) {
if (req.method === 'GET' && req.url === '/api/latest-submission') {
const latest = latestSubmission();
sendJson(res, 200, latest ? { available: true, submissionId: latest.id } : { available: false });
return true;
}
if (req.method === 'POST' && req.url === '/api/owner/submissions') {
try {
const payload = await readBody(req);
if (!passwordMatches(payload.password)) {
sendJson(res, 401, { error: 'That key does not open this room.' });
return true;
}
sendJson(res, 200, { submissions: [...submissions].sort((a, b) => b.createdAt - a.createdAt) });
} catch (error) {
console.error('[owner-submissions]', error.message);
sendJson(res, 400, { error: 'The private key could not be checked.' });
}
return true;
}
if (req.method === 'GET' && req.url.startsWith('/api/owner/submissions/')) {
const id = cleanString(req.url.split('/').pop(), 100);
const submission = findSubmission(id);
if (!submission) {
sendJson(res, 404, { error: 'Submission not found.' });
return true;
}
sendJson(res, 200, { submission });
return true;
}
if (req.method === 'POST' && req.url === '/api/owner-unlock') {
try {
const payload = await readBody(req);
const submission = findSubmission(cleanString(payload.submissionId, 100)) || latestSubmission();
if (!passwordMatches(payload.password) || !submission) {
sendJson(res, 401, { error: 'That key does not open this room.' });
return true;
}
sendJson(res, 200, {
ok: true,
submission: {
name: submission.name,
promises: submission.promises,
apology: submission.apology,
reading: submission.reading,
},
});
return true;
} catch (error) {
console.error('[owner-unlock]', error.message);
sendJson(res, 400, { error: 'The private key could not be checked.' });
return true;
}
}
if (req.method !== 'POST' || req.url !== '/api/sincerity') return false;
try {
const payload = await readBody(req);
if (!cleanString(payload.name, 80) || !cleanString(payload.apology, 700) || !Array.isArray(payload.promises) || payload.promises.length < 3 || payload.consent !== true) {
sendJson(res, 400, { error: 'Complete the name, three promises, hard part, and consent first.' });
return true;
}
const normalizedPayload = {
name: cleanString(payload.name, 80),
promises: payload.promises.slice(0, 3).map((item) => cleanString(item, 140)),
apology: cleanString(payload.apology, 700),
consent: true,
};
let reading;
try {
reading = await rateSincerity(normalizedPayload);
} catch (error) {
console.error('[sincerity]', error.message);
reading = localReflection(normalizedPayload);
}
const submissionId = crypto.randomUUID();
submissions.push({
id: submissionId,
...normalizedPayload,
reading,
createdAt: Date.now(),
viewed: false,
decision: 'pending',
updatedAt: Date.now(),
});
persistSubmissions();
sendJson(res, 200, { sealed: true, submissionId });
} catch (error) {
console.error('[sincerity]', error.message);
sendJson(res, 200, localReflection({}));
}
return true;
}
function serveStatic(req, res) {
const requestPath = new URL(req.url, 'http://localhost').pathname;
const relative = requestPath === '/' ? 'index.html' : requestPath === '/owner' ? 'owner.html' : requestPath.replace(/^\//, '');
const filePath = path.resolve(__dirname, isProduction ? 'dist' : 'public', relative);
const basePath = path.resolve(__dirname, isProduction ? 'dist' : 'public');
if (!filePath.startsWith(basePath)) {
res.writeHead(403); res.end('Forbidden'); return;
}
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
const ext = path.extname(filePath);
const types = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.png': 'image/png', '.json': 'application/json', '.mp3': 'audio/mpeg' };
res.writeHead(200, { 'Content-Type': types[ext] || 'application/octet-stream' });
fs.createReadStream(filePath).pipe(res);
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not found');
}
}
const server = http.createServer(async (req, res) => {
if (await handleApi(req, res)) return;
if (!isProduction) {
const vite = server.vite;
vite.middlewares(req, res, () => serveStatic(req, res));
} else {
serveStatic(req, res);
}
});
if (!isProduction) {
server.vite = await createViteServer({
server: { middlewareMode: true, hmr: false },
appType: 'spa',
plugins: [{
name: 'remove-preview-hmr-client',
transformIndexHtml: {
order: 'post',
handler: (html) => html.replace(/\s*<script type="module" src="\/@vite\/client"><\/script>/g, ''),
},
}],
});
}
server.listen(port, '0.0.0.0', () => console.log(`Second Chance listening on 0.0.0.0:${port} (${isProduction ? 'production' : 'development'})`));