igneum/relay/lib/relay.mjs
igneum-labs 53f7f55796 Relay: text, files and runnable tasks between the Mac, the PCs and the phone (relay.igneum.network)
New Vercel project igneum-relay from relay/: one function (api/relay.mjs) over Neon tables relay_items and relay_machines,
files in Vercel Blob store igneum-relay (50 MB client uploads, 4 MB through the function), phone-first web page at /r/<token>/
with the site tokens. Mac CLI tools/relay.mjs (feed, read, drop, task, run, watch, inbox, machines, role, name).
Windows clients send.bat/send.ps1 and the igneum-agent (registers hostname, role, GPUs, WSL, nvcc; runs queued PowerShell
scripts, posts results, reboot-continue via scheduled task + RunOnce), bash twins send.sh and agent.sh (verified live),
playbooks for WSL setup, prover setup, prove-block, miner v4, one-click placeholder. make-clients.sh bakes the secrets
into a zip; the repo copies hold placeholders. Screenshots under docs/design/relay.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 10:04:30 +00:00

108 lines
4.6 KiB
JavaScript

// 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';
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 (token && typeof given === 'string' && given === token) return 'token';
const k = req.headers['x-igneum-key'];
if (key && typeof k === 'string' && k === key) 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)]);
}