66 lines
2.9 KiB
JavaScript
66 lines
2.9 KiB
JavaScript
// Igneum miner log intake. POST /api/log with header x-igneum-key and a JSON body
|
|
// {label, machine, run_id, lines}. Stores one row in Neon table miner_logs.
|
|
// Zero dependencies: Neon's HTTP SQL endpoint over Node's built-in fetch.
|
|
// Read back on the Mac with `node tools/logs.mjs`.
|
|
|
|
const MAX_LINES = 256 * 1024;
|
|
|
|
function neon() {
|
|
const url = process.env.DATABASE_URL;
|
|
if (!url) throw new Error('DATABASE_URL is not set');
|
|
const host = new URL(url).hostname.replace('-pooler', '');
|
|
return async (query, params = []) => {
|
|
const r = await fetch(`https://${host}/sql`, {
|
|
method: 'POST',
|
|
headers: { 'Neon-Connection-String': url, 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ query, params }),
|
|
});
|
|
const j = await r.json();
|
|
if (!r.ok) throw new Error(j.message || JSON.stringify(j));
|
|
return j;
|
|
};
|
|
}
|
|
|
|
async function readBody(req) {
|
|
if (req.body !== undefined && req.body !== null) {
|
|
if (typeof req.body === 'string') return JSON.parse(req.body);
|
|
if (Buffer.isBuffer(req.body)) return JSON.parse(req.body.toString('utf8'));
|
|
return req.body;
|
|
}
|
|
const chunks = [];
|
|
for await (const c of req) chunks.push(c);
|
|
return JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
|
|
}
|
|
|
|
const str = (v, max) => (v === undefined || v === null ? '' : String(v)).slice(0, max);
|
|
|
|
export default async function handler(req, res) {
|
|
res.setHeader('Cache-Control', 'no-store');
|
|
if (req.method !== 'POST') {
|
|
res.setHeader('Allow', 'POST');
|
|
return res.status(405).json({ ok: false, error: 'method not allowed' });
|
|
}
|
|
// Two keys during a rotation (4 October 2026, round 4 X23): the current key and, while apps are moving to a new
|
|
// build, LOG_INTAKE_KEY_NEXT. Drop the old value from LOG_INTAKE_KEY once every machine reports with the new one.
|
|
const keys = [process.env.LOG_INTAKE_KEY, process.env.LOG_INTAKE_KEY_NEXT].filter(k => typeof k === 'string' && k.length >= 16);
|
|
const given = req.headers['x-igneum-key'];
|
|
if (!keys.length || typeof given !== 'string' || !keys.includes(given)) {
|
|
return res.status(401).json({ ok: false, error: 'bad key' });
|
|
}
|
|
let body;
|
|
try { body = await readBody(req); } catch { return res.status(400).json({ ok: false, error: 'bad json' }); }
|
|
let lines = str(body.lines, Infinity);
|
|
const buf = Buffer.from(lines, 'utf8');
|
|
if (buf.length > MAX_LINES) lines = buf.subarray(buf.length - MAX_LINES).toString('utf8');
|
|
const bytes = Buffer.byteLength(lines, 'utf8');
|
|
try {
|
|
const sql = neon();
|
|
const r = await sql(
|
|
'INSERT INTO miner_logs (label, machine, run_id, lines, bytes) VALUES ($1, $2, $3, $4, $5) RETURNING id',
|
|
[str(body.label, 200), str(body.machine, 200), str(body.run_id, 200), lines, bytes],
|
|
);
|
|
return res.status(200).json({ ok: true, id: Number(r.rows[0].id), bytes });
|
|
} catch (e) {
|
|
return res.status(500).json({ ok: false, error: String(e.message || e) });
|
|
}
|
|
}
|