Files
wehub-resource-sync bb5c75ce05
Component Security Validation / Security Audit (push) Has been cancelled
Deploy to Cloudflare Pages / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:58 +08:00

226 lines
8.5 KiB
JavaScript

#!/usr/bin/env node
const http = require('http');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const PORT = 3456;
const PID_FILE = path.join(__dirname, 'server.pid');
const SECRET_FILE = path.join(__dirname, '.secret');
const sseClients = new Set();
function getSecret() {
try {
return fs.readFileSync(SECRET_FILE, 'utf8').trim();
} catch {
const secret = crypto.randomBytes(32).toString('hex');
fs.writeFileSync(SECRET_FILE, secret, { mode: 0o600 });
return secret;
}
}
const hmacSecret = getSecret();
const server = http.createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
res.writeHead(204);
return res.end();
}
// Serve the game
if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
const htmlPath = path.join(__dirname, 'index.html');
fs.readFile(htmlPath, (err, data) => {
if (err) { res.writeHead(500); return res.end('Error loading game'); }
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(data);
});
return;
}
// SSE endpoint
if (req.method === 'GET' && req.url === '/events') {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
res.write('data: connected\n\n');
sseClients.add(res);
req.on('close', () => sseClients.delete(res));
return;
}
// Trigger announcement
if (req.method === 'POST' && req.url === '/done') {
let body = '';
req.on('data', chunk => { body += chunk; if (body.length > 1024) req.destroy(); });
req.on('end', () => {
const message = body || 'Claude Termino';
for (const client of sseClients) {
client.write(`event: done\ndata: ${message}\n\n`);
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true, clients: sseClients.size }));
});
return;
}
// Activity feed from hooks
if (req.method === 'POST' && req.url === '/activity') {
let body = '';
req.on('data', chunk => { body += chunk; if (body.length > 8192) req.destroy(); });
req.on('end', () => {
for (const client of sseClients) {
client.write(`event: activity\ndata: ${body}\n\n`);
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
});
return;
}
// Pause - Claude needs user input
if (req.method === 'POST' && req.url === '/pause') {
for (const client of sseClients) {
client.write(`event: pause\ndata: Claude needs you\n\n`);
}
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ ok: true }));
}
// Status - check if plan mode is active
if (req.method === 'GET' && req.url === '/status') {
const planActive = fs.existsSync(path.join(__dirname, '.plan-active'));
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ planActive }));
}
// Health check
if (req.method === 'GET' && req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ status: 'ok', clients: sseClients.size }));
}
// Share scores - generate signed URL
if (req.method === 'POST' && req.url === '/share') {
let body = '';
req.on('data', chunk => { body += chunk; if (body.length > 1024) req.destroy(); });
req.on('end', () => {
try {
const { dino, snake, flappy } = JSON.parse(body);
const payload = { d: dino || 0, s: snake || 0, f: flappy || 0, t: Date.now() };
const payloadB64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
const sig = crypto.createHmac('sha256', hmacSecret).update(payloadB64).digest('hex').slice(0, 16);
const token = `${payloadB64}.${sig}`;
const url = `http://localhost:${PORT}/v/${token}`;
const text = [
'Plan Mode Game Scores:',
`\u{1F995} Dino Runner: ${payload.d} pts`,
`\u{1F40D} Snake: ${payload.s} pts`,
`\u{1F426} Flappy Bird: ${payload.f} pts`,
'',
`Verify → ${url}`
].join('\n');
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ url, text }));
} catch {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid request' }));
}
});
return;
}
// Verify scores - render verification page
if (req.method === 'GET' && req.url.startsWith('/v/')) {
const token = req.url.slice(3);
const dotIdx = token.lastIndexOf('.');
let valid = false, payload = null;
if (dotIdx > 0) {
const payloadB64 = token.slice(0, dotIdx);
const sig = token.slice(dotIdx + 1);
const expected = crypto.createHmac('sha256', hmacSecret).update(payloadB64).digest('hex').slice(0, 16);
valid = sig === expected;
try { payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString()); } catch { valid = false; }
}
const ts = payload && payload.t ? new Date(payload.t).toLocaleString() : 'Unknown';
const html = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>${valid ? 'Verified' : 'Invalid'} - Plan Mode Game</title><style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#1a1a2e;color:#e5e7eb;font-family:'Courier New',monospace;display:flex;align-items:center;justify-content:center;min-height:100vh}
.card{text-align:center;max-width:420px;padding:40px;background:#12121f;border:1px solid #2d2d4e;border-radius:16px}
.icon{width:100px;height:100px;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 24px;font-size:48px}
.icon.valid{background:linear-gradient(135deg,#059669,#10b981);box-shadow:0 0 40px rgba(16,185,129,0.3)}
.icon.invalid{background:linear-gradient(135deg,#dc2626,#ef4444);box-shadow:0 0 40px rgba(239,68,68,0.3)}
h1{font-size:28px;margin-bottom:8px;letter-spacing:1px}
h1.valid{color:#10b981}
h1.invalid{color:#ef4444}
.subtitle{color:#6b7280;font-size:13px;margin-bottom:28px}
table{width:100%;border-collapse:collapse;margin-bottom:20px}
td{padding:10px 16px;border-bottom:1px solid #1e1e35;font-size:14px}
tr:last-child td{border-bottom:none}
.game{color:#d1d5db;text-align:left}
.pts{color:#a78bfa;font-weight:700;text-align:right;font-variant-numeric:tabular-nums}
.meta{color:#4b5563;font-size:11px;letter-spacing:0.5px}
.play-link{display:inline-block;margin-top:20px;color:#a78bfa;text-decoration:none;font-size:13px;letter-spacing:1px;padding:10px 24px;border:1px solid #7c3aed;border-radius:8px;transition:all 0.2s}
.play-link:hover{background:#7c3aed;color:#fff}
</style></head><body><div class="card">
<div class="icon ${valid ? 'valid' : 'invalid'}">${valid ? '✓' : '✗'}</div>
<h1 class="${valid ? 'valid' : 'invalid'}">${valid ? 'VERIFIED' : 'INVALID'}</h1>
<p class="subtitle">${valid ? 'These scores are authentic' : 'This score link has been tampered with'}</p>
${valid && payload ? `<table>
<tr><td class="game">\u{1F995} Dino Runner</td><td class="pts">${payload.d} pts</td></tr>
<tr><td class="game">\u{1F40D} Snake</td><td class="pts">${payload.s} pts</td></tr>
<tr><td class="game">\u{1F426} Flappy Bird</td><td class="pts">${payload.f} pts</td></tr>
</table>
<p class="meta">Shared on ${ts}</p>` : ''}
<a class="play-link" href="/">Play Plan Mode Game</a>
</div></body></html>`;
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(html);
return;
}
res.writeHead(404);
res.end('Not found');
});
// Handle port conflicts
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`Port ${PORT} already in use. Exiting.`);
process.exit(1);
}
throw err;
});
server.listen(PORT, () => {
fs.writeFileSync(PID_FILE, String(process.pid));
console.log(`Plan Mode Game server running on http://localhost:${PORT}`);
});
// SSE heartbeat - keeps connections alive and cleans dead clients
setInterval(() => {
for (const client of sseClients) {
try { client.write(': heartbeat\n\n'); }
catch { sseClients.delete(client); }
}
}, 30000);
// Cleanup on exit
process.on('SIGTERM', cleanup);
process.on('SIGINT', cleanup);
function cleanup() {
try { fs.unlinkSync(PID_FILE); } catch {}
server.close();
process.exit(0);
}