89 lines
4.7 KiB
JavaScript
89 lines
4.7 KiB
JavaScript
// Shared pieces for the Igneum relay function. Zero dependencies (the Vercel Blob calls live in lib/blob.mjs, so
|
|
// lib/handler.mjs and its tests load without node_modules). 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 send it in x-relay-token; the relay key and the intake key go in
|
|
// x-igneum-key with the powers lib/guard.mjs gives them (5 October 2026, night: X23, X24, X27).
|
|
import { randomBytes } from 'node:crypto';
|
|
import { authVia } from './guard.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
|
|
// start-app (MF-11, 7 October 2026): a signed, tagged task like `run`, but the agent executes nothing from its body: it
|
|
// starts the installed Igneum Miner (hidden console, as the user) and reports whether an engine answered. The body
|
|
// carries relay/playbooks/start-app.ps1 so an agent from before this kind runs the same thing as a `run`.
|
|
export const KINDS = new Set(['text', 'file', 'task', 'result', 'run', 'start-app']);
|
|
/** The two kinds the agent executes: signed by the run key, tagged with the machine secret, one nonce each. */
|
|
export const AGENT_KINDS = ['run', 'start-app'];
|
|
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);
|
|
|
|
/** 'token', 'key', 'intake' or null (lib/guard.mjs). The intake tier may only upload and drop files. */
|
|
export const authed = (req, env = process.env) => authVia({ query: req.query || {}, headers: req.headers || {} }, env);
|
|
/** The same, with the intake tier excluded: the console and the wake POST take reports from nobody's package. */
|
|
export const authedNoIntake = (req, env = process.env) => { const v = authed(req, env); return v === 'intake' ? null : v; };
|
|
|
|
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 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 BLOB_URL_RE = /^https:\/\/[a-z0-9.-]+\.public\.blob\.vercel-storage\.com\//i;
|
|
|
|
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)]);
|
|
}
|