Relay (X23, X27): the intake key is its own tier (upload and file drops only, RELAY_INTAKE_COMPAT=0 closes it);
a run task needs an Ed25519 signature by the Mac run key over {to, nonce, body sha256, flags} (RELAY_RUN_PUB,
401 without) and an HMAC tag with the target's machine secret that the agent verifies before anything runs;
results and registration are bound to the machine the secret proves (403 on a forged from).
X24: every client and Mac tool sends x-relay-token as a header to /api/relay?fn=; the path token stays for the
phone page only. X25: the agent arms the logon task only for a restart a task asked for and disarms on start
and exit. X26: 30-day retention with blob deletion, feed capped at 100, the dl base as RELAY_DL_BASE held by the
agent, never in a body. X28: GET inbox never acks (POST inbox does), RELAY-REBOOT on its own line and only with a
reboot flag, 120/min and 10 failed auths/min per IP, no username or folder on register, WSL sudo scoped to
apt-get and dpkg with SETENV, no password on a command line. X29: the intake key reaches curl through -K in
upload.sh and both upload-log.bat; tools/ci/curl-header-check.sh fails the class. G14: TZ=UTC in ship-app.mjs
and publish-jobs.sh; tools/ci/commit-tz-check.sh fails the class; history-rewrite.md names the .old-2026-10-05
files as the values in the history. The handler moved to relay/lib/handler.mjs with injected sql and blobs
(relay/lib/blob.mjs holds @vercel/blob) so relay/test/handler.test.mjs drives it without a database:
47 tests across 6 suites, all green.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
31 lines
1.4 KiB
JavaScript
31 lines
1.4 KiB
JavaScript
// The Vercel Blob side of the relay (store igneum-relay): the only module that imports @vercel/blob, so everything
|
|
// else loads without node_modules. api/relay.mjs hands these three to lib/handler.mjs; the tests hand it fakes.
|
|
import { put, del } from '@vercel/blob';
|
|
import { generateClientTokenFromReadWriteToken } from '@vercel/blob/client';
|
|
import { blobPath, MAX_BLOB } from './relay.mjs';
|
|
|
|
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 };
|
|
}
|
|
|
|
/** Deletes the blobs behind the given URLs (X26: a deleted or expired row takes its file with it). Never throws. */
|
|
export async function deleteBlobs(urls) {
|
|
const list = [...new Set(urls.filter(Boolean))];
|
|
if (!list.length) return 0;
|
|
try { await del(list); return list.length; } catch { return 0; }
|
|
}
|