// Shared pieces for the Igneum relay function. Zero dependencies apart from @vercel/blob. // Storage: Neon (HTTP SQL driver) for every item, Vercel Blob (store igneum-relay, public URLs with a // random suffix) for files. The token in the URL path is the only secret the web page holds; scripts // may also present the intake key in x-igneum-key. import { put } from '@vercel/blob'; import { generateClientTokenFromReadWriteToken } from '@vercel/blob/client'; import { randomBytes } from 'node:crypto'; import { sameSecret } from './auth.mjs'; export const MAX_INLINE = 4 * 1024 * 1024; // raw upload through the function (Vercel body cap is 4.5 MB) export const MAX_BLOB = 50 * 1024 * 1024; // direct-to-Blob upload with a client token export const MAX_BODY = 1024 * 1024; // text body per item export const KINDS = new Set(['text', 'file', 'task', 'result', 'run']); export const ROLES = new Set(['miner', 'prover', 'bench', 'mac', 'phone', '']); export 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.rows; }; } export const str = (v, max) => (v === undefined || v === null ? '' : String(v)).slice(0, max); export function authed(req) { const q = req.query || {}; const token = process.env.RELAY_TOKEN; const key = process.env.RELAY_KEY; const given = q.token || req.headers['x-relay-token']; if (sameSecret(given, token)) return 'token'; const k = req.headers['x-igneum-key']; if (sameSecret(k, key)) return 'key'; // the key packaged into every miner app (LOG_INTAKE_KEY, and its successor during a rotation) reports too: the // build job uploads its binaries with it (5 October 2026: 0.3.5 apps got "no token" after the relay key split) for (const name of ['LOG_INTAKE_KEY', 'LOG_INTAKE_KEY_NEXT']) if (process.env[name] && sameSecret(k, process.env[name])) return 'key'; return null; } export async function readJson(req) { if (req.body !== undefined && req.body !== null) { if (typeof req.body === 'string') return req.body ? JSON.parse(req.body) : {}; if (Buffer.isBuffer(req.body)) return req.body.length ? JSON.parse(req.body.toString('utf8')) : {}; return req.body; } const chunks = []; for await (const c of req) chunks.push(c); const s = Buffer.concat(chunks).toString('utf8'); return s ? JSON.parse(s) : {}; } export async function readRaw(req) { if (Buffer.isBuffer(req.body)) return req.body; if (typeof req.body === 'string') return Buffer.from(req.body, 'utf8'); const chunks = []; for await (const c of req) chunks.push(c); return Buffer.concat(chunks); } export function safeName(name) { const n = str(name, 200).replace(/[\\/]+/g, '_').replace(/[^\w.\-+ ()\[\]]/g, '_').trim(); return n || 'file'; } export function blobPath(name) { return `relay/${randomBytes(6).toString('hex')}/${safeName(name)}`; } export async function storeBuffer(name, buf, contentType) { const r = await put(blobPath(name), buf, { access: 'public', addRandomSuffix: true, contentType: contentType || 'application/octet-stream', }); return { url: r.url, size: buf.length }; } export async function clientUploadToken(name, size) { const pathname = blobPath(name); const token = await generateClientTokenFromReadWriteToken({ pathname, addRandomSuffix: true, allowOverwrite: false, maximumSizeInBytes: MAX_BLOB, validUntil: Date.now() + 60 * 60 * 1000, }); return { token, pathname, put_url: `https://vercel.com/api/blob/?pathname=${encodeURIComponent(pathname)}`, api_version: '11', max: MAX_BLOB, size }; } export const ITEM_COLS = 'id, ts, from_machine, to_machine, kind, title, body, file_name, file_url, size, read, read_at, done, done_at, flags, task_id, (file_b64 IS NOT NULL) AS inline'; export const iso = v => { if (!v) return null; const d = new Date(String(v).replace(' ', 'T').replace(/([+-]\d\d)$/, '$1:00')); return isNaN(d) ? String(v) : d.toISOString(); }; export function rowOut(r) { return { id: Number(r.id), ts: iso(r.ts), from: r.from_machine, to: r.to_machine, kind: r.kind, title: r.title, body: r.body, file_name: r.file_name, has_file: !!(r.file_url || r.inline), size: Number(r.size || 0), read: !!r.read, read_at: iso(r.read_at), done: !!r.done, done_at: iso(r.done_at), flags: r.flags || {}, task_id: r.task_id == null ? null : Number(r.task_id), }; } export async function touch(sql, name) { if (!name) return; await sql(`INSERT INTO relay_machines (name, role, named, last_seen) VALUES ($1, '', false, now()) ON CONFLICT (name) DO UPDATE SET last_seen = now()`, [str(name, 80)]); }