🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@runinfra/cli

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@runinfra/cli - npm Package Compare versions

Comparing version
0.2.2
to
0.2.3
+604
dist/hf-weights.js
import { lstat, mkdir, open, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
import { isAbsolute, join, resolve as resolvePath } from "node:path";
import { sha256File } from "./checksum.js";
import { assertFreeSpace } from "./disk.js";
import { isLoopbackHostname } from "./endpoints.js";
import { CliError, localWriteFailure } from "./errors.js";
import { requestJson, streamRange, } from "./http.js";
import { formatBytes } from "./progress.js";
import { weightsTreeRelativePaths, weightsTreeSha256, } from "./weights-digest.js";
const HF_HUB_BASE = "https://huggingface.co";
const CHECKPOINT_BYTES = 32 * 1024 * 1024;
const TRANSFER_IDLE_TIMEOUT_MS = 60_000;
export const WEIGHTS_RESUME_FORMAT = 1;
export function serializeWeightsResume(manifest) {
return `${JSON.stringify(manifest, null, 2)}\n`;
}
function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function parseResumeFile(value) {
if (!isRecord(value))
return null;
const { path, sizeBytes, sha256, completedBytes } = value;
if (typeof path !== "string" || path.length === 0)
return null;
if (typeof sizeBytes !== "number" || !Number.isSafeInteger(sizeBytes) || sizeBytes < 0) {
return null;
}
if (sha256 !== null && typeof sha256 !== "string")
return null;
if (typeof completedBytes !== "number" ||
!Number.isSafeInteger(completedBytes) ||
completedBytes < 0 ||
completedBytes > sizeBytes) {
return null;
}
return { path, sizeBytes, sha256, completedBytes };
}
export function parseWeightsResume(raw) {
let value;
try {
value = JSON.parse(raw);
}
catch {
return null;
}
if (!isRecord(value) || value.format !== WEIGHTS_RESUME_FORMAT)
return null;
const { slug, version, hfId, revision, weightsSha256, updatedAt } = value;
if (typeof slug !== "string" || typeof hfId !== "string")
return null;
if (typeof revision !== "string" || typeof weightsSha256 !== "string")
return null;
if (typeof version !== "number" || !Number.isSafeInteger(version))
return null;
if (typeof updatedAt !== "string" || !Array.isArray(value.files))
return null;
const files = [];
for (const rawFile of value.files) {
const file = parseResumeFile(rawFile);
if (file === null)
return null;
files.push(file);
}
return {
format: WEIGHTS_RESUME_FORMAT,
slug,
version,
hfId,
revision,
weightsSha256,
files,
updatedAt,
};
}
function freshResume(plan) {
return {
format: WEIGHTS_RESUME_FORMAT,
slug: plan.slug,
version: plan.version,
hfId: plan.hfId,
revision: plan.revision,
weightsSha256: plan.weightsSha256,
files: plan.files.map((file) => ({ ...file, completedBytes: 0 })),
updatedAt: new Date().toISOString(),
};
}
function resumeMatchesPlan(resume, plan) {
if (resume.slug !== plan.slug ||
resume.version !== plan.version ||
resume.hfId !== plan.hfId ||
resume.revision !== plan.revision ||
resume.weightsSha256 !== plan.weightsSha256 ||
resume.files.length !== plan.files.length) {
return false;
}
return resume.files.every((file, index) => {
const expected = plan.files[index];
return (expected !== undefined &&
file.path === expected.path &&
file.sizeBytes === expected.sizeBytes &&
file.sha256 === expected.sha256);
});
}
function hubBase(value) {
if (value === undefined)
return HF_HUB_BASE;
const parsed = new URL(value);
if (!isLoopbackHostname(parsed.hostname) ||
(parsed.protocol !== "http:" && parsed.protocol !== "https:")) {
throw new CliError("usage", "The Hugging Face origin is fixed by the CLI.");
}
return parsed.origin;
}
function authorizationHeaders(env) {
const token = env.HF_TOKEN?.trim();
return token ? { authorization: `Bearer ${token}` } : {};
}
function encodeRepo(hfId) {
return hfId.split("/").map(encodeURIComponent).join("/");
}
function encodeRepoPath(path) {
return path.split("/").map(encodeURIComponent).join("/");
}
function revisionUrl(base, plan) {
return `${base}/api/models/${encodeRepo(plan.hfId)}/revision/${plan.revision}?expand=sha`;
}
function fileUrl(base, plan, file) {
return `${base}/${encodeRepo(plan.hfId)}/resolve/${plan.revision}/${encodeRepoPath(file.path)}`;
}
function hfAccessFailure(plan, tokenPresent) {
return new CliError("auth_denied", `Hugging Face did not allow access to ${plan.hfId}.`, tokenPresent
? `Accept the license for ${plan.hfId} on huggingface.co, then make sure HF_TOKEN is a read token for that account.`
: `Accept the license for ${plan.hfId} on huggingface.co, then set HF_TOKEN to a Hugging Face read token and run the command again.`);
}
function hfRateLimit(plan, headers) {
const parsed = Number.parseInt(headers["retry-after"] ?? "", 10);
const seconds = Number.isFinite(parsed) && parsed > 0 ? parsed : 60;
return new CliError("rate_limited", `Hugging Face rate limited the weight download for ${plan.hfId}.`, `Wait ${seconds} seconds, then run the same command again to resume.`);
}
function interrupted() {
return new CliError("interrupted", "Weight download interrupted.", "Run the same command again to resume from the durable checkpoints on disk.");
}
function storageFailure(path, error) {
if (error instanceof CliError)
return error;
const local = localWriteFailure(path, error);
if (local !== null)
return local;
return new CliError("storage_unwritable", `Could not access ${path}: ${error.message}`, "Check the destination permissions, or pass --out to a writable directory and run the command again.");
}
async function readResume(path) {
let raw;
try {
raw = await readFile(path, "utf8");
}
catch (error) {
if (error.code === "ENOENT")
return { kind: "missing" };
throw storageFailure(path, error);
}
const resume = parseWeightsResume(raw);
return resume === null ? { kind: "invalid" } : { kind: "valid", resume };
}
async function directoryHasEntries(path) {
try {
return (await readdir(path)).length > 0;
}
catch (error) {
throw storageFailure(path, error);
}
}
async function lstatOrNull(path) {
try {
return await lstat(path);
}
catch (error) {
if (error.code === "ENOENT")
return null;
throw storageFailure(path, error);
}
}
async function ensureSafeDirectory(base, segments) {
let current = base;
for (const segment of segments) {
current = join(current, segment);
const before = await lstatOrNull(current);
if (before === null) {
try {
await mkdir(current);
}
catch (error) {
if (error.code === "EEXIST") {
}
else {
throw storageFailure(current, error);
}
}
}
const after = await lstatOrNull(current);
if (after === null) {
throw new CliError("storage_unwritable", `${current} disappeared while the destination was being prepared.`, "Run the command again, or pass --out to a stable writable directory.");
}
if (after.isSymbolicLink() || !after.isDirectory()) {
throw new CliError("storage_unwritable", `${current} is not a real directory, so the CLI refused to write through it.`, "Move the kit to a directory without symbolic links or junctions and run the command again.");
}
}
return current;
}
async function assertSafeFinalPath(weightsRoot, relative) {
const segments = relative.split("/");
const name = segments.pop();
if (name === undefined)
throw new CliError("storage_unwritable", "A weights path was empty.");
const parent = await ensureSafeDirectory(weightsRoot, segments);
const finalPath = join(parent, name);
const existing = await lstatOrNull(finalPath);
if (existing?.isSymbolicLink()) {
throw new CliError("storage_unwritable", `${finalPath} is a symbolic link, so the CLI refused to replace its target.`, "Remove the link or pass --out to a clean directory.");
}
return finalPath;
}
async function assertSafeControlFile(path) {
const existing = await lstatOrNull(path);
if (existing !== null && (existing.isSymbolicLink() || !existing.isFile())) {
throw new CliError("storage_unwritable", `${path} is not a regular CLI state file, so it was not read or replaced.`, "Remove that entry or pass --out to a clean directory, then run the command again.");
}
}
async function writeResume(path, resume) {
const temp = `${path}.tmp`;
resume.updatedAt = new Date().toISOString();
try {
await assertSafeControlFile(path);
await assertSafeControlFile(temp);
await writeFile(temp, serializeWeightsResume(resume), "utf8");
await rename(temp, path);
}
catch (error) {
throw storageFailure(path, error);
}
}
async function verifyRevision(input, base, headers, request) {
const response = await (request ?? requestJson)({
method: "GET",
url: revisionUrl(base, input.plan),
headers,
signal: input.signal,
});
if (response.status === 401 || response.status === 403) {
throw hfAccessFailure(input.plan, headers.authorization !== undefined);
}
if (response.status === 429)
throw hfRateLimit(input.plan, response.headers);
if (response.status === 404) {
throw new CliError("artifact_changed", `The pinned Hugging Face revision for ${input.plan.hfId} is no longer available.`, "The CLI refused to substitute a branch, tag, or different commit. Contact support with the package slug.");
}
if (response.status !== 200 || !isRecord(response.body)) {
throw new CliError("server_error", `Hugging Face could not confirm the pinned revision for ${input.plan.hfId}.`, "Run the same command again. If it persists, check Hugging Face status before contacting support.");
}
const served = response.body.sha;
if (served !== input.plan.revision) {
throw new CliError("artifact_changed", `Hugging Face resolved ${input.plan.hfId} to a different revision than the package pinned.`, "The CLI stopped without retrying or accepting substitute weights. Contact support with the package slug.");
}
}
function validateServedRevision(plan, response) {
if (response.status !== 200 && response.status !== 206)
return;
const commits = [
...response.redirectHeaders.map((headers) => headers["x-repo-commit"]),
response.headers["x-repo-commit"],
].filter((value) => typeof value === "string" && value.length > 0);
if (commits.length === 0) {
throw new CliError("artifact_changed", `Hugging Face did not identify which revision served ${plan.hfId}.`, "The CLI refused bytes it could not bind to the package's immutable pin.");
}
if (commits.some((commit) => commit.trim().toLowerCase() !== plan.revision)) {
throw new CliError("artifact_changed", `Hugging Face served a different revision than the package pinned for ${plan.hfId}.`, "The CLI stopped before writing those bytes and will not retry a server that refused the pin.");
}
}
async function writeAll(handle, path, buffer, position) {
let offset = 0;
try {
while (offset < buffer.length) {
const result = await handle.write(buffer, offset, buffer.length - offset, position + offset);
if (result.bytesWritten === 0)
throw new Error("the filesystem accepted zero bytes");
offset += result.bytesWritten;
}
}
catch (error) {
throw storageFailure(path, error);
}
}
async function syncFile(handle, path) {
try {
await handle.datasync();
}
catch (error) {
throw storageFailure(path, error);
}
}
async function verifyPerFile(path, file, signal) {
if (file.sha256 === null)
return;
let actual;
try {
actual = await sha256File(path, undefined, signal);
}
catch (error) {
if (signal.aborted)
throw interrupted();
throw storageFailure(path, error);
}
if (actual !== file.sha256) {
throw new CliError("checksum_mismatch", `The fetched weights file ${file.path} does not match its signed sha256.`, "Delete that file and the .runinfra-weights-parts directory, then run the command again.");
}
}
async function downloadOneFile(values) {
const { file, input } = values;
const expected = input.plan.files[values.index];
if (expected === undefined)
throw new CliError("server_error", "The weights resume plan is inconsistent.");
const finalPath = await assertSafeFinalPath(values.root, file.path);
const final = await lstatOrNull(finalPath);
if (final !== null) {
if (!final.isFile() || final.isSymbolicLink() || final.size !== file.sizeBytes) {
throw new CliError("storage_unwritable", `${finalPath} already exists but is not the signed ${formatBytes(file.sizeBytes)} file.`, "Move or delete it, then run the same command again.");
}
await verifyPerFile(finalPath, expected, input.signal);
file.completedBytes = file.sizeBytes;
await values.persist();
return;
}
const partPath = join(values.partsRoot, `${values.index}.part`);
const priorPart = await lstatOrNull(partPath);
if (priorPart !== null && (priorPart.isSymbolicLink() || !priorPart.isFile())) {
throw new CliError("storage_unwritable", `${partPath} is not a regular partial file, so the CLI refused to write through it.`, "Remove that entry or pass --out to a clean directory, then run the command again.");
}
let handle = null;
try {
try {
handle = await open(partPath, "r+");
}
catch (error) {
if (error.code !== "ENOENT")
throw error;
handle = await open(partPath, "w+");
}
const part = await handle.stat();
if (part.size < file.completedBytes)
file.completedBytes = 0;
await handle.truncate(file.completedBytes);
}
catch (error) {
throw storageFailure(partPath, error);
}
if (handle === null)
throw new CliError("storage_unwritable", `Could not open ${partPath}.`);
const opened = handle;
try {
if (file.sizeBytes > file.completedBytes) {
let position = file.completedBytes;
let checkpoint = position;
input.output.status(` Fetching ${file.path} ${formatBytes(position)} of ${formatBytes(file.sizeBytes)}`);
const result = await values.stream({
url: fileUrl(values.base, input.plan, expected),
start: position,
endInclusive: file.sizeBytes - 1,
totalBytes: file.sizeBytes,
acceptWhole: position === 0,
headers: values.headers,
idleTimeoutMs: TRANSFER_IDLE_TIMEOUT_MS,
signal: input.signal,
validateResponse: (response) => validateServedRevision(input.plan, response),
onChunk: async (chunk) => {
if (chunk.length > file.sizeBytes - position) {
throw new CliError("range_mismatch", `Hugging Face sent more bytes than the pinned size for ${file.path}.`, "The download stopped before writing bytes outside the signed file boundary.");
}
await writeAll(opened, partPath, chunk, position);
position += chunk.length;
if (position - checkpoint >= CHECKPOINT_BYTES) {
await syncFile(opened, partPath);
file.completedBytes = position;
checkpoint = position;
await values.persist();
input.output.status(` Fetching ${file.path} ${formatBytes(position)} of ${formatBytes(file.sizeBytes)}`);
}
},
});
if (result.status === 401 || result.status === 403) {
throw hfAccessFailure(input.plan, values.headers.authorization !== undefined);
}
if (result.status === 429)
throw hfRateLimit(input.plan, result.headers);
if (result.status === 404) {
throw new CliError("artifact_changed", `The pinned file ${file.path} is missing from ${input.plan.hfId}@${input.plan.revision}.`, "The CLI refused to substitute another file. Contact support with the package slug.");
}
if (result.status !== 206 && result.status !== 200) {
throw new CliError("server_error", `Hugging Face could not serve ${file.path} from ${input.plan.hfId}.`, "Run the same command again to resume. If it persists, check Hugging Face status.");
}
if (position !== file.sizeBytes) {
throw new CliError("network", `Hugging Face sent ${formatBytes(position)} of the pinned ${formatBytes(file.sizeBytes)} file ${file.path}.`, "Run the same command again to resume from the last durable checkpoint.");
}
await syncFile(opened, partPath);
file.completedBytes = file.sizeBytes;
await values.persist();
}
}
finally {
await opened.close().catch(() => undefined);
}
await verifyPerFile(partPath, expected, input.signal);
try {
await rename(partPath, finalPath);
}
catch (error) {
throw storageFailure(finalPath, error);
}
}
async function existingCompletedBytes(weightsRoot, files) {
let total = 0;
for (const file of files) {
const path = await assertSafeFinalPath(weightsRoot, file.path);
const existing = await lstatOrNull(path);
if (existing === null)
continue;
if (existing.isSymbolicLink() || !existing.isFile() || existing.size !== file.sizeBytes) {
throw new CliError("storage_unwritable", `${path} already exists but is not the signed ${formatBytes(file.sizeBytes)} file.`, "Move or delete it, then run the same command again.");
}
total += file.sizeBytes;
}
return total;
}
async function stagedCompletedBytes(weightsRoot, partsRoot, files) {
let total = 0;
for (let index = 0; index < files.length; index += 1) {
const file = files[index];
if (file === undefined || file.completedBytes === 0)
continue;
const finalPath = await assertSafeFinalPath(weightsRoot, file.path);
if ((await lstatOrNull(finalPath)) !== null)
continue;
const partPath = join(partsRoot, `${index}.part`);
const part = await lstatOrNull(partPath);
if (part === null)
continue;
if (part.isSymbolicLink() || !part.isFile()) {
throw new CliError("storage_unwritable", `${partPath} is not a regular partial file, so it cannot be resumed.`, "Remove that entry or pass --out to a clean directory, then run the command again.");
}
total += Math.min(file.completedBytes, part.size);
}
return total;
}
export async function downloadPinnedWeights(input, dependencies = {}) {
if (input.signal.aborted)
throw interrupted();
const destination = isAbsolute(input.destination)
? resolvePath(input.destination)
: resolvePath(process.cwd(), input.destination);
const destinationStats = await lstatOrNull(destination);
if (destinationStats === null) {
throw new CliError("storage_unwritable", `${destination} does not exist.`);
}
if (destinationStats.isSymbolicLink() || !destinationStats.isDirectory()) {
throw new CliError("storage_unwritable", `${destination} is not a real directory.`);
}
const kitRoot = await ensureSafeDirectory(destination, [input.plan.kitRoot]);
const weightsRoot = await ensureSafeDirectory(kitRoot, ["weights"]);
const partsRoot = await ensureSafeDirectory(kitRoot, [".runinfra-weights-parts"]);
const resumePath = join(kitRoot, ".runinfra-weights.json");
await assertSafeControlFile(resumePath);
await assertSafeControlFile(`${resumePath}.tmp`);
const resumeRead = await readResume(resumePath);
let resume;
if (resumeRead.kind === "valid") {
if (!resumeMatchesPlan(resumeRead.resume, input.plan)) {
throw new CliError("artifact_changed", "The saved weights resume record belongs to a different package source.", `The CLI preserved the partial files. Move or delete ${resumePath} and ${partsRoot} before starting this package source.`);
}
resume = resumeRead.resume;
}
else {
const hasParts = await directoryHasEntries(partsRoot);
if (hasParts) {
throw new CliError("storage_unwritable", resumeRead.kind === "invalid"
? "The weights resume record is unreadable, so the saved partial files cannot be trusted or resumed safely."
: "Weight partial files exist without their resume record, so their durable byte ranges are unknown.", `The CLI preserved the partial files. Move or delete ${resumePath} and ${partsRoot}, then run the command again.`);
}
if (resumeRead.kind === "invalid") {
input.output.warn("The weights resume record was unreadable, but no partial weight files were present, so a new record was created.");
}
resume = freshResume(input.plan);
await writeResume(resumePath, resume);
}
const alreadyComplete = await existingCompletedBytes(weightsRoot, input.plan.files);
const alreadyStaged = await stagedCompletedBytes(weightsRoot, partsRoot, resume.files);
const remaining = Math.max(0, input.plan.sizeBytes - alreadyComplete - alreadyStaged);
await (dependencies.assertFreeSpace ?? assertFreeSpace)(kitRoot, remaining);
const base = hubBase(input.hubBase);
const headers = authorizationHeaders(input.env);
await verifyRevision(input, base, headers, dependencies.requestJson);
input.output.info(`Fetching ${input.plan.hfId}@${input.plan.revision} (${formatBytes(input.plan.sizeBytes)}) into ${weightsRoot}`);
let persistChain = Promise.resolve();
const persist = async () => {
persistChain = persistChain.then(() => writeResume(resumePath, resume));
await persistChain;
};
const stream = dependencies.streamRange ?? streamRange;
const transferController = new AbortController();
const abortTransfers = () => {
transferController.abort(input.signal.reason);
};
if (input.signal.aborted)
abortTransfers();
else
input.signal.addEventListener("abort", abortTransfers, { once: true });
const transferInput = {
...input,
signal: transferController.signal,
};
let next = 0;
let failed = false;
const failures = [];
const worker = async () => {
for (;;) {
if (failed)
return;
if (input.signal.aborted)
throw interrupted();
const index = next;
next += 1;
const file = resume.files[index];
if (file === undefined)
return;
await downloadOneFile({
root: weightsRoot,
partsRoot,
file,
index,
input: transferInput,
base,
headers,
persist,
stream,
});
}
};
const workers = Array.from({ length: Math.min(Math.max(1, input.concurrency), Math.max(1, resume.files.length)) }, () => worker().catch((error) => {
failed = true;
if (failures.length === 0) {
failures.push(error);
transferController.abort(error);
}
throw error;
}));
const settled = await Promise.allSettled(workers);
input.signal.removeEventListener("abort", abortTransfers);
await persistChain.catch(() => undefined);
input.output.endStatus();
if (settled.some((result) => result.status === "rejected")) {
if (input.signal.aborted)
throw interrupted();
const primary = failures[0];
if (primary !== undefined)
throw primary;
throw new CliError("server_error", "A weight download worker stopped without an error.");
}
input.output.info("Verifying the signed weights-tree checksum.");
let digest;
try {
digest = await weightsTreeSha256(weightsRoot);
}
catch (error) {
if (input.signal.aborted)
throw interrupted();
if (typeof error?.code === "string") {
throw storageFailure(weightsRoot, error);
}
throw new CliError("checksum_mismatch", `The fetched weights tree for ${input.plan.hfId} could not be verified: ${error.message}`, `Move or delete ${weightsRoot}, then run the same command again.`);
}
if (digest.digest !== input.plan.weightsSha256) {
const listed = new Set(input.plan.files.map((file) => file.path));
let unlisted = [];
try {
unlisted = (await weightsTreeRelativePaths(weightsRoot)).filter((path) => !listed.has(path));
}
catch {
unlisted = [];
}
if (unlisted.length > 0) {
const preview = unlisted.slice(0, 5).join(", ");
const rest = unlisted.length > 5 ? `, and ${unlisted.length - 5} more` : "";
throw new CliError("checksum_mismatch", `The weights directory for ${input.plan.hfId} holds ${unlisted.length} file(s) the package did not sign: ${preview}${rest}.`, `Every file this package pins was fetched. Remove the unsigned file(s) from ${weightsRoot}, or pull into an empty directory with --out, then run the same command again.`);
}
throw new CliError("checksum_mismatch", `The fetched weights tree for ${input.plan.hfId} does not match the package's signed digest.`, `Move or delete ${weightsRoot}, then run the same command again.`);
}
try {
await rm(resumePath, { force: true });
await rm(partsRoot, { recursive: true, force: true });
}
catch (error) {
throw storageFailure(kitRoot, error);
}
input.output.detail("Pinned weights tree checksum verified.");
input.output.info(`Saved pinned weights in ${weightsRoot}.`);
}
import { createHash, createPublicKey, verify } from "node:crypto";
import { open, stat } from "node:fs/promises";
import { CliError } from "./errors.js";
import { compareWeightsPaths } from "./weights-digest.js";
const EOCD_SIGNATURE = 0x06054b50;
const ZIP64_EOCD_SIGNATURE = 0x06064b50;
const ZIP64_LOCATOR_SIGNATURE = 0x07064b50;
const CENTRAL_SIGNATURE = 0x02014b50;
const LOCAL_SIGNATURE = 0x04034b50;
const ZIP64_EXTRA_ID = 0x0001;
const MAX_EOCD_SEARCH = 65_557;
const MAX_CENTRAL_DIRECTORY = 64 * 1024 * 1024;
const MAX_METADATA_ENTRY = 16 * 1024 * 1024;
function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function failKit(message) {
return new CliError("artifact_changed", `The downloaded kit cannot supply trusted weight instructions: ${message}.`, "Download the package again. If the fresh kit is refused too, contact support with the package slug.");
}
function failKitRead(path, error) {
return new CliError("storage_unwritable", `Could not read ${path}: ${error.message}`, "Check that the kit still exists and is readable, or pass --out to a readable local directory.");
}
function toSafeNumber(value, label) {
if (value < BigInt(0) || value > BigInt(Number.MAX_SAFE_INTEGER)) {
throw failKit(`${label} is outside the supported file-size range`);
}
return Number(value);
}
async function readExact(handle, position, length) {
const buffer = Buffer.alloc(length);
let offset = 0;
while (offset < length) {
let bytesRead;
try {
({ bytesRead } = await handle.read(buffer, offset, length - offset, position + offset));
}
catch (error) {
throw failKitRead("the downloaded kit", error);
}
if (bytesRead === 0)
throw failKit("the ZIP archive ends unexpectedly");
offset += bytesRead;
}
return buffer;
}
function findEocd(tail) {
for (let offset = tail.length - 22; offset >= 0; offset -= 1) {
if (tail.readUInt32LE(offset) !== EOCD_SIGNATURE)
continue;
const commentLength = tail.readUInt16LE(offset + 20);
if (offset + 22 + commentLength === tail.length)
return offset;
}
throw failKit("the ZIP end record is missing");
}
async function centralLocation(handle, archiveSize) {
const tailLength = Math.min(archiveSize, MAX_EOCD_SEARCH);
const tailStart = archiveSize - tailLength;
const tail = await readExact(handle, tailStart, tailLength);
const eocdInTail = findEocd(tail);
const eocdOffset = tailStart + eocdInTail;
const disk = tail.readUInt16LE(eocdInTail + 4);
const centralDisk = tail.readUInt16LE(eocdInTail + 6);
if (disk !== 0 || centralDisk !== 0)
throw failKit("multi-disk ZIP archives are not supported");
const entries = tail.readUInt16LE(eocdInTail + 10);
const size = tail.readUInt32LE(eocdInTail + 12);
const offset = tail.readUInt32LE(eocdInTail + 16);
if (entries !== 0xffff && size !== 0xffffffff && offset !== 0xffffffff) {
return { entries, size, offset };
}
if (eocdOffset < 20)
throw failKit("the ZIP64 locator is missing");
const locator = await readExact(handle, eocdOffset - 20, 20);
if (locator.readUInt32LE(0) !== ZIP64_LOCATOR_SIGNATURE) {
throw failKit("the ZIP64 locator is malformed");
}
if (locator.readUInt32LE(4) !== 0 || locator.readUInt32LE(16) !== 1) {
throw failKit("multi-disk ZIP64 archives are not supported");
}
const zip64Offset = toSafeNumber(locator.readBigUInt64LE(8), "ZIP64 end offset");
const zip64 = await readExact(handle, zip64Offset, 56);
if (zip64.readUInt32LE(0) !== ZIP64_EOCD_SIGNATURE) {
throw failKit("the ZIP64 end record is malformed");
}
if (zip64.readUInt32LE(16) !== 0 || zip64.readUInt32LE(20) !== 0) {
throw failKit("multi-disk ZIP64 archives are not supported");
}
return {
entries: toSafeNumber(zip64.readBigUInt64LE(32), "ZIP entry count"),
size: toSafeNumber(zip64.readBigUInt64LE(40), "ZIP central-directory size"),
offset: toSafeNumber(zip64.readBigUInt64LE(48), "ZIP central-directory offset"),
};
}
function zip64Values(extra, needs) {
let cursor = 0;
while (cursor + 4 <= extra.length) {
const id = extra.readUInt16LE(cursor);
const length = extra.readUInt16LE(cursor + 2);
const start = cursor + 4;
const end = start + length;
if (end > extra.length)
throw failKit("a ZIP extra field is truncated");
if (id === ZIP64_EXTRA_ID) {
let valueOffset = start;
const out = {};
const read = (label) => {
if (valueOffset + 8 > end)
throw failKit(`the ZIP64 ${label} is missing`);
const value = toSafeNumber(extra.readBigUInt64LE(valueOffset), `ZIP64 ${label}`);
valueOffset += 8;
return value;
};
if (needs.uncompressed)
out.uncompressed = read("uncompressed size");
if (needs.compressed)
out.compressed = read("compressed size");
if (needs.offset)
out.offset = read("local-header offset");
return out;
}
cursor = end;
}
throw failKit("a required ZIP64 extra field is missing");
}
async function readZipDirectory(handle, archiveSize) {
const location = await centralLocation(handle, archiveSize);
if (location.size > MAX_CENTRAL_DIRECTORY) {
throw failKit("the ZIP central directory is unexpectedly large");
}
if (location.offset + location.size > archiveSize) {
throw failKit("the ZIP central directory points outside the archive");
}
const bytes = await readExact(handle, location.offset, location.size);
const entries = [];
let cursor = 0;
for (let index = 0; index < location.entries; index += 1) {
if (cursor + 46 > bytes.length || bytes.readUInt32LE(cursor) !== CENTRAL_SIGNATURE) {
throw failKit("a ZIP central-directory entry is malformed");
}
const flags = bytes.readUInt16LE(cursor + 8);
const method = bytes.readUInt16LE(cursor + 10);
const crc32 = bytes.readUInt32LE(cursor + 16);
const compressed32 = bytes.readUInt32LE(cursor + 20);
const uncompressed32 = bytes.readUInt32LE(cursor + 24);
const nameLength = bytes.readUInt16LE(cursor + 28);
const extraLength = bytes.readUInt16LE(cursor + 30);
const commentLength = bytes.readUInt16LE(cursor + 32);
const diskStart = bytes.readUInt16LE(cursor + 34);
const offset32 = bytes.readUInt32LE(cursor + 42);
const end = cursor + 46 + nameLength + extraLength + commentLength;
if (end > bytes.length)
throw failKit("a ZIP central-directory entry is truncated");
if (diskStart !== 0)
throw failKit("a ZIP entry starts on another disk");
const name = bytes.subarray(cursor + 46, cursor + 46 + nameLength).toString("utf8");
const extra = bytes.subarray(cursor + 46 + nameLength, cursor + 46 + nameLength + extraLength);
const needs = {
uncompressed: uncompressed32 === 0xffffffff,
compressed: compressed32 === 0xffffffff,
offset: offset32 === 0xffffffff,
};
const zip64 = needs.uncompressed || needs.compressed || needs.offset
? zip64Values(extra, needs)
: {};
entries.push({
name,
flags,
method,
crc32,
uncompressedSize: zip64.uncompressed ?? uncompressed32,
compressedSize: zip64.compressed ?? compressed32,
localOffset: zip64.offset ?? offset32,
});
cursor = end;
}
return entries;
}
async function readStoredEntry(handle, entry) {
if ((entry.flags & 0x0001) !== 0)
throw failKit(`${entry.name} is encrypted`);
if (entry.method !== 0)
throw failKit(`${entry.name} is not stored without compression`);
if (entry.compressedSize !== entry.uncompressedSize) {
throw failKit(`${entry.name} has contradictory stored sizes`);
}
if (entry.uncompressedSize > MAX_METADATA_ENTRY) {
throw failKit(`${entry.name} is unexpectedly large`);
}
const local = await readExact(handle, entry.localOffset, 30);
if (local.readUInt32LE(0) !== LOCAL_SIGNATURE) {
throw failKit(`${entry.name} has no readable local header`);
}
const localFlags = local.readUInt16LE(6);
const localMethod = local.readUInt16LE(8);
const localCrc32 = local.readUInt32LE(14);
const localCompressed = local.readUInt32LE(18);
const localUncompressed = local.readUInt32LE(22);
const nameLength = local.readUInt16LE(26);
const extraLength = local.readUInt16LE(28);
if (localFlags !== entry.flags ||
localMethod !== entry.method ||
localCrc32 !== entry.crc32 ||
localCompressed !== entry.compressedSize ||
localUncompressed !== entry.uncompressedSize) {
throw failKit(`${entry.name} has a local header that disagrees with the central directory`);
}
const localName = (await readExact(handle, entry.localOffset + 30, nameLength)).toString("utf8");
if (localName !== entry.name) {
throw failKit(`${entry.name} points to a local header for a different file`);
}
const dataOffset = entry.localOffset + 30 + nameLength + extraLength;
const body = await readExact(handle, dataOffset, entry.compressedSize);
if (crc32(body) !== entry.crc32)
throw failKit(`${entry.name} does not match its ZIP CRC32`);
return body;
}
function crc32(bytes) {
let crc = 0xffffffff;
for (const byte of bytes) {
crc ^= byte;
for (let bit = 0; bit < 8; bit += 1) {
crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
}
}
return (crc ^ 0xffffffff) >>> 0;
}
function metadataEntry(entries, root, name) {
const expected = `${root}/${name}`;
const matches = entries.filter((entry) => entry.name === expected);
if (matches.length !== 1)
throw failKit(`${expected} is missing or duplicated`);
return matches[0];
}
function parseJson(bytes, label) {
try {
return JSON.parse(bytes.toString("utf8"));
}
catch {
throw failKit(`${label} is not valid JSON`);
}
}
function normalizeJson(value) {
if (Array.isArray(value))
return value.map(normalizeJson);
if (!isRecord(value))
return value;
const out = {};
for (const key of Object.keys(value).sort())
out[key] = normalizeJson(value[key]);
return out;
}
export function canonicalJson(value) {
const serialized = JSON.stringify(normalizeJson(value));
if (serialized === undefined)
throw failKit("a signed JSON value could not be serialized");
return serialized;
}
function requiredString(record, key, pattern) {
const value = record[key];
if (typeof value !== "string" || value.length === 0 || (pattern && !pattern.test(value))) {
throw failKit(`${key} is missing or malformed`);
}
return value;
}
function requiredInteger(record, key) {
const value = record[key];
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
throw failKit(`${key} is missing or malformed`);
}
return value;
}
function safeKitRoot(name) {
const parts = name.split("/");
if (parts.length !== 2 ||
parts[1] !== "manifest.json" ||
!/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(parts[0] ?? "")) {
throw failKit("manifest.json is not under one safe kit root");
}
return parts[0];
}
function safeWeightsPath(value, seen) {
if (typeof value !== "string" || value.length === 0) {
throw failKit("a weights file has no path");
}
const segments = value.split("/");
if (value.startsWith("/") ||
value.includes("\\") ||
value.includes("\0") ||
value.includes(":") ||
segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
throw failKit(`weights file path is unsafe: ${value}`);
}
if (seen.has(value))
throw failKit(`weights file path is listed twice: ${value}`);
seen.add(value);
return value;
}
function parseWeightsFiles(raw) {
if (!Array.isArray(raw) || raw.length === 0)
throw failKit("the signed weights file list is empty");
const seen = new Set();
const files = raw.map((entry) => {
if (!isRecord(entry))
throw failKit("a weights file entry is not an object");
const path = safeWeightsPath(entry.path, seen);
const sizeBytes = requiredInteger(entry, "sizeBytes");
const sha256 = entry.sha256;
if (sha256 !== undefined && sha256 !== null) {
if (typeof sha256 !== "string" || !/^[a-f0-9]{64}$/u.test(sha256)) {
throw failKit(`weights file ${path} has a malformed sha256`);
}
return { path, sizeBytes, sha256 };
}
return { path, sizeBytes, sha256: null };
});
const ordered = [...files].sort((a, b) => compareWeightsPaths(a.path, b.path));
if (files.some((file, index) => ordered[index]?.path !== file.path)) {
throw failKit("the signed weights file list is not in the verifier's canonical order");
}
return files;
}
function strictBase64(value, label) {
if (typeof value !== "string" || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(value)) {
throw failKit(`${label} is not valid base64`);
}
return Buffer.from(value, "base64");
}
function verifyReceipt(receiptRaw, publicKeyPem, manifest, source, filesDigest, fileCount, weightsSha256) {
if (!isRecord(receiptRaw))
throw failKit("benchmark-receipt.json is not an object");
if (receiptRaw.algorithm !== "Ed25519")
throw failKit("the receipt is not signed with Ed25519");
const signedBytes = strictBase64(receiptRaw.signedBytesB64, "receipt signed bytes");
const signature = strictBase64(receiptRaw.signature, "receipt signature");
let key;
try {
key = createPublicKey(publicKeyPem);
}
catch {
throw failKit("runinfra-factory.pub is not a readable public key");
}
let signatureValid = false;
try {
signatureValid = verify(null, signedBytes, key, signature);
}
catch {
throw failKit("the benchmark receipt signature could not be checked");
}
if (!signatureValid) {
throw failKit("the benchmark receipt signature is invalid");
}
const signedPayload = parseJson(signedBytes, "the signed receipt payload");
if (canonicalJson(signedPayload) !== canonicalJson(receiptRaw.payload)) {
throw failKit("the visible receipt payload is not the signed payload");
}
if (!isRecord(signedPayload) || !isRecord(signedPayload.package)) {
throw failKit("the signed receipt has no package block");
}
const signedPackage = signedPayload.package;
if (signedPackage.slug !== manifest.slug || signedPackage.version !== manifest.version) {
throw failKit("the signed receipt is for a different package or version");
}
if (signedPackage.weightsSha256 !== weightsSha256) {
throw failKit("the signed receipt and manifest weights digests disagree");
}
if (!isRecord(signedPackage.weightsSource)) {
throw failKit("the signed receipt has no weights source");
}
const signedSource = signedPackage.weightsSource;
if (signedSource.kind !== "huggingface" ||
signedSource.repo !== source.hfId ||
signedSource.revision !== source.revision ||
signedSource.fileCount !== fileCount ||
signedSource.filesDigest !== filesDigest) {
throw failKit("the manifest weights source is not the source signed by the receipt");
}
}
export async function readKitWeightsPlan(kitPath) {
const archive = await stat(kitPath).catch((error) => {
throw failKitRead(kitPath, error);
});
if (!archive.isFile() || !Number.isSafeInteger(archive.size) || archive.size < 22) {
throw failKit("the kit is not a readable ZIP file");
}
let handle;
try {
handle = await open(kitPath, "r");
}
catch (error) {
throw failKitRead(kitPath, error);
}
try {
const entries = await readZipDirectory(handle, archive.size);
const manifestEntries = entries.filter((entry) => entry.name.endsWith("/manifest.json"));
if (manifestEntries.length !== 1)
throw failKit("manifest.json is missing or duplicated");
const manifestEntry = manifestEntries[0];
const kitRoot = safeKitRoot(manifestEntry.name);
const manifestRaw = parseJson(await readStoredEntry(handle, manifestEntry), "manifest.json");
if (!isRecord(manifestRaw))
throw failKit("manifest.json is not an object");
const slug = requiredString(manifestRaw, "slug");
const version = requiredInteger(manifestRaw, "version");
if (version < 1)
throw failKit("version is missing or malformed");
if (slug !== kitRoot)
throw failKit("the ZIP root and manifest slug disagree");
const archivedWeightFiles = entries.filter((entry) => entry.name.startsWith(`${kitRoot}/weights/`) &&
!entry.name.endsWith("/"));
const weights = manifestRaw.weights;
if (archivedWeightFiles.length > 0) {
if (isRecord(weights) && weights.delivery === "fetch") {
throw failKit("a fetch-weights manifest also contains files under weights/");
}
return { kind: "optimized_weights", kitRoot, slug, version };
}
if (!isRecord(weights))
throw failKit("a kit with no archived weights has no weights block");
if (weights.delivery !== "fetch") {
throw failKit("a kit with no archived weights is not marked for fetch delivery");
}
const weightsSha256 = requiredString(weights, "sha256", /^[a-f0-9]{64}$/u);
if (!isRecord(manifestRaw.weightsSource)) {
throw failKit("a recipe kit has no weightsSource block");
}
const source = manifestRaw.weightsSource;
if (source.mode !== "huggingface")
throw failKit("the recipe source is not Hugging Face");
const hfId = requiredString(source, "hfId", /^[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._-]*$/u);
const revision = requiredString(source, "revision", /^[a-f0-9]{40}$/u);
const fileCount = requiredInteger(source, "fileCount");
const filesDigest = requiredString(source, "filesDigest", /^[a-f0-9]{64}$/u);
const rawFiles = source.files;
const files = parseWeightsFiles(rawFiles);
if (files.length !== fileCount)
throw failKit("the signed weights file count disagrees with the list");
const actualFilesDigest = createHash("sha256")
.update(canonicalJson(rawFiles), "utf8")
.digest("hex");
if (actualFilesDigest !== filesDigest) {
throw failKit("the weights file list does not match its signed digest");
}
let sizeBytes = 0;
for (const file of files) {
sizeBytes += file.sizeBytes;
if (!Number.isSafeInteger(sizeBytes))
throw failKit("the weights size exceeds the supported range");
}
const receipt = parseJson(await readStoredEntry(handle, metadataEntry(entries, kitRoot, "benchmark-receipt.json")), "benchmark-receipt.json");
const publicKey = await readStoredEntry(handle, metadataEntry(entries, kitRoot, "runinfra-factory.pub"));
verifyReceipt(receipt, publicKey, manifestRaw, source, filesDigest, fileCount, weightsSha256);
return {
kind: "recipe",
kitRoot,
slug,
version,
hfId,
revision,
weightsSha256,
sizeBytes,
files,
};
}
finally {
await handle.close().catch(() => undefined);
}
}
import { createHash } from "node:crypto";
import { createReadStream } from "node:fs";
import { lstat, readdir } from "node:fs/promises";
import { join } from "node:path";
function compareCodePoints(left, right) {
const a = [...left];
const b = [...right];
const length = Math.min(a.length, b.length);
for (let index = 0; index < length; index += 1) {
const aPoint = a[index]?.codePointAt(0) ?? 0;
const bPoint = b[index]?.codePointAt(0) ?? 0;
if (aPoint !== bPoint)
return aPoint < bPoint ? -1 : 1;
}
if (a.length === b.length)
return 0;
return a.length < b.length ? -1 : 1;
}
export function compareWeightsPaths(left, right) {
const a = left.split("/");
const b = right.split("/");
const length = Math.max(a.length, b.length);
for (let index = 0; index < length; index += 1) {
const aPart = a[index];
const bPart = b[index];
if (aPart === undefined)
return -1;
if (bPart === undefined)
return 1;
const aKind = index === a.length - 1 ? 0 : 1;
const bKind = index === b.length - 1 ? 0 : 1;
if (aKind !== bKind)
return aKind - bKind;
const names = compareCodePoints(aPart, bPart);
if (names !== 0)
return names;
}
return 0;
}
function sizeFrame(sizeBytes) {
const frame = Buffer.alloc(16);
frame.writeBigUInt64BE(BigInt(sizeBytes), 8);
return frame;
}
async function updateWithFile(hash, root, relative, expectedSize, signal) {
const path = join(root, ...relative.split("/"));
const stats = await lstat(path);
if (!stats.isFile() || stats.isSymbolicLink()) {
throw new Error(`weights entry is not a regular file: ${relative}`);
}
if (!Number.isSafeInteger(stats.size) || stats.size < 0) {
throw new Error(`weights file has an unreadable size: ${relative}`);
}
if (expectedSize !== undefined && stats.size !== expectedSize) {
throw new Error(`weights file ${relative} is ${stats.size} bytes, expected ${expectedSize}`);
}
const name = Buffer.from(relative, "utf8");
const nameLength = Buffer.alloc(8);
nameLength.writeBigUInt64BE(BigInt(name.length));
hash.update(nameLength);
hash.update(name);
hash.update(sizeFrame(stats.size));
await new Promise((resolve, reject) => {
const stream = createReadStream(path, signal ? { signal } : {});
stream.on("data", (chunk) => {
hash.update(typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk);
});
stream.on("end", resolve);
stream.on("error", reject);
});
return stats.size;
}
export async function weightsTreeSha256(root) {
const hash = createHash("sha256");
let fileCount = 0;
let totalBytes = 0;
const walk = async (directory, prefix) => {
const entries = await readdir(directory, { withFileTypes: true });
const files = entries
.filter((entry) => entry.isFile())
.sort((a, b) => compareCodePoints(a.name, b.name));
const directories = entries
.filter((entry) => entry.isDirectory())
.sort((a, b) => compareCodePoints(a.name, b.name));
const unsupported = entries.find((entry) => !entry.isFile() && !entry.isDirectory());
if (unsupported !== undefined) {
throw new Error(`weights entry is not a regular file: ${unsupported.name}`);
}
for (const file of files) {
const relative = prefix.length === 0 ? file.name : `${prefix}/${file.name}`;
totalBytes += await updateWithFile(hash, root, relative);
fileCount += 1;
}
for (const child of directories) {
const relative = prefix.length === 0 ? child.name : `${prefix}/${child.name}`;
await walk(join(directory, child.name), relative);
}
};
await walk(root, "");
if (fileCount === 0)
throw new Error("weights directory contains no files");
return { digest: hash.digest("hex"), fileCount, totalBytes };
}
export async function weightsTreeRelativePaths(root) {
const found = [];
const walk = async (directory, prefix) => {
const entries = await readdir(directory, { withFileTypes: true });
const files = entries
.filter((entry) => entry.isFile())
.sort((a, b) => compareCodePoints(a.name, b.name));
const directories = entries
.filter((entry) => entry.isDirectory())
.sort((a, b) => compareCodePoints(a.name, b.name));
for (const file of files) {
found.push(prefix.length === 0 ? file.name : `${prefix}/${file.name}`);
}
for (const child of directories) {
const relative = prefix.length === 0 ? child.name : `${prefix}/${child.name}`;
await walk(join(directory, child.name), relative);
}
};
await walk(root, "");
return found;
}
export async function weightsTreeSha256FromFiles(root, files, signal) {
const hash = createHash("sha256");
let totalBytes = 0;
for (const file of files) {
totalBytes += await updateWithFile(hash, root, file.path, file.sizeBytes, signal);
}
if (files.length === 0)
throw new Error("weights file list is empty");
return { digest: hash.digest("hex"), fileCount: files.length, totalBytes };
}
import { CliError } from "./errors.js";
import { downloadPinnedWeights, } from "./hf-weights.js";
import { readKitWeightsPlan } from "./kit-weights.js";
export async function installWeightsForKit(context, input, dependencies = {}) {
const plan = await (dependencies.readKitWeightsPlan ?? readKitWeightsPlan)(input.kitPath);
if (plan.slug !== input.expectedSlug || plan.version !== input.packageVersion) {
throw new CliError("artifact_changed", "The downloaded kit identifies a different package or version than the requested download.", "Download the package again. If the fresh kit is refused too, contact support with the package slug.");
}
if (plan.kind === "optimized_weights") {
context.output.info("The weights are already inside the kit. Nothing else was downloaded for --weights.");
return;
}
if (!input.kitChecksumVerified) {
throw new CliError("checksum_mismatch", "The kit publishes no readable checksum, so its weight source instructions cannot be trusted.", "Contact support with the package slug. The CLI did not contact Hugging Face.");
}
await (dependencies.downloadPinnedWeights ?? downloadPinnedWeights)({
output: context.output,
env: context.env,
plan,
destination: input.destination,
concurrency: input.concurrency,
signal: input.signal,
});
}
+16
-0

@@ -8,5 +8,7 @@ export const COMMANDS = ["login", "logout", "whoami", "pull"];

device: false,
local: false,
slug: null,
out: null,
concurrency: null,
weights: false,
};

@@ -73,2 +75,9 @@ }

}
case "--local": {
if (command !== "logout") {
return { ok: false, message: `--local applies to \`runinfra logout\` only.` };
}
args.local = true;
break;
}
case "--out":

@@ -114,2 +123,9 @@ case "-o": {

}
case "--weights": {
if (command !== "pull") {
return { ok: false, message: `--weights applies to \`runinfra pull\` only.` };
}
args.weights = true;
break;
}
default:

@@ -116,0 +132,0 @@ return {

+68
-1

@@ -1,2 +0,2 @@

import { CliError } from "./errors.js";
import { CliError, describeError } from "./errors.js";
import { requestJson } from "./http.js";

@@ -170,2 +170,69 @@ const TOKEN_REFUSALS = new Set([

}
export async function revokeCliKey(input) {
const controller = new AbortController();
const deadline = setTimeout(() => controller.abort(), input.timeoutMs);
try {
const response = await requestJson({
method: "POST",
url: input.endpoints.revoke,
headers: { authorization: `Bearer ${input.apiKey}` },
json: {},
idleTimeoutMs: input.timeoutMs,
signal: controller.signal,
});
if (response.status === 200) {
const body = response.body;
if (isRecord(body) && body.ok === true)
return { ok: true };
return {
ok: false,
reason: `${hostOf(input.endpoints.revoke)} answered 200, but not with a RunInfra revoke confirmation.`,
};
}
return {
ok: false,
reason: revokeRefusalReason(response.status, response.body, hostOf(input.endpoints.revoke)),
};
}
catch (error) {
if (controller.signal.aborted) {
return {
ok: false,
reason: `${hostOf(input.endpoints.revoke)} did not answer within ${input.timeoutMs}ms.`,
};
}
return { ok: false, reason: describeError(error).message };
}
finally {
clearTimeout(deadline);
}
}
function revokeRefusalReason(status, body, host) {
if (status === 404 || status === 405 || status === 501) {
return `${host} does not offer terminal revocation yet, so the key was left alone.`;
}
const code = isRecord(body) && typeof body.code === "string" ? body.code : null;
switch (code) {
case "invalid_key":
case "missing_credentials":
return `${host} did not accept this machine's key, so it could not revoke it.`;
case "workspace_access_revoked":
return `This account is no longer a member of that workspace, so ${host} would not act on the key.`;
case "rate_limited":
return `${host} is rate limiting revoke requests right now.`;
case "auth_unavailable":
case "server_error":
return `${host} could not process the revoke (HTTP ${status}).`;
default:
return `${host} answered HTTP ${status}.`;
}
}
function hostOf(url) {
try {
return new URL(url).host;
}
catch {
return url;
}
}
export async function pollDeviceCode(input) {

@@ -172,0 +239,0 @@ const response = await requestJson({

+25
-9
import { probeArtifact, requestLease } from "./catalog-api.js";
import { CliError } from "./errors.js";
import { CliError, localWriteFailure } from "./errors.js";
import { streamRange, streamWhole } from "./http.js";

@@ -14,2 +14,9 @@ import { leaseChangeReason, leaseExpiredStatus, leaseNeedsRefresh, } from "./lease.js";

const TRANSFER_IDLE_TIMEOUT_MS = 60_000;
const TERMINAL_CHUNK_FAILURES = new Set([
"artifact_changed",
"ranges_unsupported",
"range_mismatch",
"disk_space",
"storage_unwritable",
]);
class LeaseHolder {

@@ -186,2 +193,3 @@ inputs;

endInclusive: chunk.endInclusive,
totalBytes: inputs.sidecar.sizeBytes,
idleTimeoutMs: TRANSFER_IDLE_TIMEOUT_MS,

@@ -192,3 +200,3 @@ signal: inputs.signal,

received += buffer.length;
await writeAt(inputs.handle, buffer, chunk.start + offset);
await writeAt(inputs.handle, inputs.partPath, buffer, chunk.start + offset);
input.onBytes(buffer.length);

@@ -211,3 +219,3 @@ },

else if (result.status === 200) {
throw new CliError("ranges_unsupported", "The storage origin stopped honouring byte ranges mid-download.", "Run the same command again; it will fall back to a single stream.");
throw new CliError("ranges_unsupported", `The storage origin advertised byte ranges but answered the request for bytes ${chunk.start}-${chunk.endInclusive} with the whole object.`, "Nothing on disk is wrong. Run the same command again in case one bad edge node was in the way, and if it keeps happening contact support with the package slug, because this origin cannot serve the resumable download the CLI is asking for.");
}

@@ -221,4 +229,3 @@ else {

throw abortedError();
if (error instanceof CliError &&
(error.code === "artifact_changed" || error.code === "ranges_unsupported")) {
if (error instanceof CliError && TERMINAL_CHUNK_FAILURES.has(error.code)) {
throw error;

@@ -249,3 +256,3 @@ }

written += buffer.length;
await writeAt(inputs.handle, buffer, offset);
await writeAt(inputs.handle, inputs.partPath, buffer, offset);
onBytes(buffer.length);

@@ -262,8 +269,17 @@ },

}
async function writeAt(handle, buffer, position) {
async function writeAt(handle, path, buffer, position) {
let offset = 0;
while (offset < buffer.length) {
const { bytesWritten } = await handle.write(buffer, offset, buffer.length - offset, position + offset);
let bytesWritten;
try {
({ bytesWritten } = await handle.write(buffer, offset, buffer.length - offset, position + offset));
}
catch (error) {
const local = localWriteFailure(path, error);
if (local !== null)
throw local;
throw error;
}
if (bytesWritten <= 0) {
throw new CliError("storage_unwritable", "The filesystem stopped accepting writes for this download.", "Check free space and permissions on the destination, then run the command again.");
throw new CliError("storage_unwritable", `The filesystem stopped accepting writes to ${path}.`, "Check free space and permissions on the destination, then run the command again.");
}

@@ -270,0 +286,0 @@ offset += bytesWritten;

@@ -46,2 +46,3 @@ export const DEFAULT_API_BASE = "https://runinfra.ai";

devicePoll: `${base}/api/cli/device/poll`,
revoke: `${base}/api/cli/revoke`,
download: (slug) => `${base}/api/catalog/${encodeURIComponent(slug)}/download`,

@@ -48,0 +49,0 @@ };

@@ -15,5 +15,6 @@ export const EXIT_CODES = {

rate_limited: 5,
ranges_unsupported: 5,
checksum_mismatch: 6,
artifact_changed: 6,
ranges_unsupported: 6,
range_mismatch: 6,
disk_space: 7,

@@ -36,2 +37,18 @@ storage_unwritable: 7,

}
export function localWriteFailure(path, error) {
const code = error?.code;
switch (code) {
case "ENOSPC":
return new CliError("disk_space", `The volume holding ${path} ran out of space.`, "Free some space, or pass --out to a larger volume, then run the same command again to resume from the bytes already on disk.");
case "EDQUOT":
return new CliError("disk_space", `Writing ${path} exceeded your disk quota on that volume.`, "Raise the quota or pass --out to a volume you have room on, then run the same command again to resume.");
case "EROFS":
return new CliError("storage_unwritable", `${path} is on a read-only filesystem.`, "Pass --out to a directory you can write to, then run the command again.");
case "EACCES":
case "EPERM":
return new CliError("storage_unwritable", `The filesystem refused a write to ${path} (${code}).`, "Check the ownership and permissions on that directory, or pass --out somewhere you can write, then run the command again.");
default:
return null;
}
}
export function exitCodeFor(error) {

@@ -38,0 +55,0 @@ return isCliError(error) ? EXIT_CODES[error.code] : 1;

@@ -18,9 +18,16 @@ import { API_BASE_ENV } from "./endpoints.js";

" which is what a headless GPU host needs.",
" logout Delete this machine's stored key. To end its access",
" everywhere, revoke it in Settings, API keys.",
" logout [--local] Revoke this machine's key and delete the stored copy.",
" If the revoke cannot be made, the local key is still",
" deleted and you are told the key is still live.",
" whoami Show the workspace and key this machine has stored,",
" and when that access expires.",
" pull <slug> Download a package this workspace owns. Resumable:",
" run the same command again to continue.",
" run the same command again to continue. Add --weights",
" to fetch a recipe package's pinned source weights.",
"",
"LOGOUT OPTIONS",
" --local Delete the stored key without calling the server.",
" For an offline machine, or a teardown that must make",
" no network call. The key stays live until its expiry.",
"",
"PULL OPTIONS",

@@ -30,2 +37,5 @@ " --out DIR Where to write the file. Defaults to the current",

" --concurrency N Parallel connections, 1 to 16. Defaults to 8.",
" --weights For a recipe package, fetch its exact pinned Hugging",
" Face files into the kit's weights directory and verify",
" the signed weights-tree digest.",
"",

@@ -36,2 +46,3 @@ "ENVIRONMENT",

` ${CONFIG_DIR_ENV} Where credentials are stored.`,
" HF_TOKEN Hugging Face read token for a gated source model.",
"",

@@ -41,11 +52,16 @@ "EXIT CODES",

" 2 bad usage or unsupported runtime",
" 3 not signed in, denied, or expired",
" 3 not signed in, denied, expired, or refused by a gated source",
" 4 the workspace does not own it",
" 5 network or server failure",
" 6 integrity failure (checksum, or the artifact changed mid-download)",
" 5 network, server, or rate-limit failure, including an origin that will",
" not serve ranges",
" 6 integrity failure (checksum, pinned revision, a mis-served byte range,",
" or the artifact changed mid-download)",
" 7 no space, or the destination is not writable",
" 130 interrupted",
"",
"Access can be revoked at any time from Settings, API keys on runinfra.ai.",
"logout exits 0 whenever the local key is gone, including when the revoke",
"could not be made. The warning carries that, not the exit code.",
"",
"Access can also be revoked at any time from Settings, API keys on runinfra.ai.",
].join("\n");
}

@@ -20,12 +20,14 @@ import { request as httpRequest } from "node:http";

}
function networkError(url, cause) {
const detail = cause instanceof Error ? cause.message : String(cause);
let host = url;
function hostOf(url) {
try {
host = new URL(url).host;
return new URL(url).host;
}
catch {
return url;
}
return new CliError("network", `Could not reach ${host}: ${detail}`, "Check your connection or proxy settings, then run the command again.");
}
function networkError(url, cause) {
const detail = cause instanceof Error ? cause.message : String(cause);
return new CliError("network", `Could not reach ${hostOf(url)}: ${detail}`, "Check your connection or proxy settings, then run the command again.");
}
const CREDENTIAL_HEADERS = new Set([

@@ -54,5 +56,18 @@ "authorization",

: Buffer.from(JSON.stringify(options.json), "utf8");
const originalOrigin = new URL(options.url).origin;
let originalOrigin;
try {
originalOrigin = new URL(options.url).origin;
}
catch (cause) {
throw networkError(options.url, cause);
}
const redirectHeaders = [];
for (let hop = 0; hop <= maxRedirects; hop += 1) {
const parsed = new URL(currentUrl);
let parsed;
try {
parsed = new URL(currentUrl);
}
catch (cause) {
throw networkError(currentUrl, cause);
}
const secure = parsed.protocol === "https:";

@@ -98,6 +113,13 @@ const send = secure ? httpsRequest : httpRequest;

if (!isRedirect) {
return { message, finalUrl: currentUrl };
return { message, finalUrl: currentUrl, redirectHeaders };
}
redirectHeaders.push(normalizeHeaders(message));
message.resume();
const next = new URL(location, currentUrl);
let next;
try {
next = new URL(location, currentUrl);
}
catch (cause) {
throw networkError(currentUrl, cause);
}
if (parsed.protocol === "https:" && next.protocol !== "https:") {

@@ -165,2 +187,3 @@ throw new CliError("network", `${parsed.host} redirected to an insecure URL (${next.protocol}//${next.host}).`, "This is not something the CLI can safely follow. Report it to support.");

url,
...(options.headers ? { headers: options.headers } : {}),
...(options.idleTimeoutMs === undefined

@@ -177,7 +200,54 @@ ? {}

}
const CONTENT_RANGE_PATTERN = /^bytes\s+(\d+)-(\d+)\/(\d+|\*)$/iu;
export function parseContentRange(value) {
if (value === undefined)
return null;
const match = CONTENT_RANGE_PATTERN.exec(value.trim());
if (match === null)
return null;
const start = Number.parseInt(match[1] ?? "", 10);
const endInclusive = Number.parseInt(match[2] ?? "", 10);
const rawTotal = match[3] ?? "";
const totalBytes = rawTotal === "*" ? null : Number.parseInt(rawTotal, 10);
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(endInclusive)) {
return null;
}
if (totalBytes !== null && !Number.isSafeInteger(totalBytes))
return null;
if (endInclusive < start)
return null;
return { start, endInclusive, totalBytes };
}
export function contentRangeRefusal(headerValue, expected) {
const asked = `bytes ${expected.start}-${expected.endInclusive}`;
if (headerValue === undefined || headerValue.trim() === "") {
return `it answered 206 for ${asked} with no Content-Range header, so which bytes it sent cannot be established`;
}
const served = parseContentRange(headerValue);
if (served === null) {
return `it answered 206 for ${asked} with an unreadable Content-Range (${clipHeader(headerValue)})`;
}
if (served.start !== expected.start ||
served.endInclusive !== expected.endInclusive) {
return `it answered 206 for ${asked} with bytes ${served.start}-${served.endInclusive}`;
}
if (expected.totalBytes !== undefined &&
served.totalBytes !== null &&
served.totalBytes !== expected.totalBytes) {
return `it answered 206 for ${asked} but declared the object to be ${served.totalBytes} bytes rather than ${expected.totalBytes}`;
}
return null;
}
function clipHeader(value) {
const flat = value.replace(/\s+/gu, " ").trim();
return flat.length <= 80 ? flat : `${flat.slice(0, 80)}...`;
}
export async function streamRange(request) {
const { message, finalUrl } = await dispatch({
const { message, finalUrl, redirectHeaders } = await dispatch({
method: "GET",
url: request.url,
headers: { range: `bytes=${request.start}-${request.endInclusive}` },
headers: {
...(request.headers ?? {}),
range: `bytes=${request.start}-${request.endInclusive}`,
},
...(request.idleTimeoutMs === undefined

@@ -190,6 +260,44 @@ ? {}

const headers = normalizeHeaders(message);
if (request.validateResponse) {
try {
request.validateResponse({ status, headers, redirectHeaders, finalUrl });
}
catch (error) {
message.on("error", () => undefined);
message.destroy();
throw error;
}
}
if (status !== 206) {
await drain(message);
if (status === 200 &&
request.acceptWhole === true &&
request.start === 0 &&
request.totalBytes !== undefined &&
request.endInclusive === request.totalBytes - 1) {
const statedLength = headers["content-length"];
if (statedLength !== undefined &&
(!/^\d+$/u.test(statedLength) || Number(statedLength) !== request.totalBytes)) {
message.on("error", () => undefined);
message.destroy();
throw new CliError("artifact_changed", `${hostOf(finalUrl)} served ${statedLength} bytes for an object pinned at ${request.totalBytes} bytes.`, "The download stopped before accepting the changed file.");
}
const bytes = await consumeBody(message, finalUrl, request.onChunk);
return { status, headers, bytes };
}
message.on("error", () => undefined);
message.destroy();
return { status, headers, bytes: 0 };
}
const refusal = contentRangeRefusal(headers["content-range"], {
start: request.start,
endInclusive: request.endInclusive,
...(request.totalBytes === undefined
? {}
: { totalBytes: request.totalBytes }),
});
if (refusal !== null) {
message.on("error", () => undefined);
message.destroy();
throw new CliError("range_mismatch", `${hostOf(finalUrl)} did not serve the byte range that was asked for: ${refusal}.`, "The download stopped rather than write bytes it cannot place. This is a broken or hostile origin, not a connection problem, so report it to support with the package slug rather than retrying.");
}
const bytes = await consumeBody(message, finalUrl, request.onChunk);

@@ -199,5 +307,6 @@ return { status, headers, bytes };

export async function streamWhole(request) {
const { message, finalUrl } = await dispatch({
const { message, finalUrl, redirectHeaders } = await dispatch({
method: "GET",
url: request.url,
...(request.headers ? { headers: request.headers } : {}),
...(request.idleTimeoutMs === undefined

@@ -210,2 +319,12 @@ ? {}

const headers = normalizeHeaders(message);
if (request.validateResponse) {
try {
request.validateResponse({ status, headers, redirectHeaders, finalUrl });
}
catch (error) {
message.on("error", () => undefined);
message.destroy();
throw error;
}
}
if (status !== 200) {

@@ -212,0 +331,0 @@ await drain(message);

@@ -1,4 +0,7 @@

import { deleteCredentials, readCredentials } from "./credentials.js";
import { revokeCliKey } from "./auth-api.js";
import { credentialsExpired, deleteCredentials, readCredentials, } from "./credentials.js";
import { endpointsFor } from "./endpoints.js";
import { redact } from "./redact.js";
export async function logout(context) {
export const LOGOUT_REVOKE_TIMEOUT_MS = 5_000;
export async function logout(context, options) {
const credentials = await readCredentials(context.location);

@@ -12,7 +15,35 @@ if (credentials === null) {

}
const step = options.local
? { kind: "skipped" }
: await runRevoke(credentials.apiKey, credentials.apiBase, options);
await deleteCredentials(context.location);
const identity = `${credentials.keyPrefix} (${redact(credentials.apiKey)})`;
if (step.kind === "revoked") {
context.output.info("Revoked this machine's key and removed the local credentials.");
context.output.detail(`The key ${identity} no longer works anywhere.`);
return 0;
}
context.output.info("Removed this machine's credentials.");
context.output.detail(`The key ${credentials.keyPrefix} (${redact(credentials.apiKey)}) is still valid until ${credentials.expiresAt}.`);
if (step.kind === "skipped") {
context.output.info("Skipped the revoke because --local was passed. No network call was made.");
}
else {
context.output.warn("The key was NOT revoked, so it is still live.");
context.output.detail(step.reason);
}
if (credentialsExpired(credentials)) {
context.output.detail(`The key ${identity} had already expired on ${credentials.expiresAt}, so nothing is still granted.`);
return 0;
}
context.output.detail(`The key ${identity} is STILL VALID until ${credentials.expiresAt}.`);
context.output.detail("To end its access now, revoke it in Settings, API keys on runinfra.ai.");
return 0;
}
async function runRevoke(apiKey, apiBase, options) {
const outcome = await revokeCliKey({
endpoints: endpointsFor(apiBase),
apiKey,
timeoutMs: options.timeoutMs ?? LOGOUT_REVOKE_TIMEOUT_MS,
});
return outcome.ok ? { kind: "revoked" } : { kind: "failed", reason: outcome.reason };
}

@@ -45,3 +45,3 @@ import { parseArgs } from "./args.js";

case "logout":
exitCode = await logout(context);
exitCode = await logout(context, { local: parsed.args.local });
break;

@@ -57,3 +57,8 @@ case "whoami":

}
exitCode = await pull(context, { slug, out: parsed.args.out, concurrency: parsed.args.concurrency }, signal);
exitCode = await pull(context, {
slug,
out: parsed.args.out,
concurrency: parsed.args.concurrency,
weights: parsed.args.weights,
}, signal);
break;

@@ -60,0 +65,0 @@ }

@@ -9,6 +9,7 @@ import { open, mkdir, rename, rm, stat, writeFile } from "node:fs/promises";

import { runDownload } from "./downloader.js";
import { CliError } from "./errors.js";
import { CliError, localWriteFailure } from "./errors.js";
import { formatBytes, formatDuration } from "./progress.js";
import { clampConcurrency, normalizeRanges, totalBytes, } from "./range-plan.js";
import { describeMismatch, parseSidecar, serializeSidecar, sidecarMismatch, SIDECAR_FORMAT, } from "./sidecar.js";
import { installWeightsForKit } from "./weights.js";
export async function pull(context, options, signal) {

@@ -41,2 +42,3 @@ const { output } = context;

if (existing.size === artifact.sizeBytes) {
let checksumVerified = false;
if (lease.checksumSha256) {

@@ -53,5 +55,7 @@ let lastTickMs = 0;

output.endStatus();
if (judgeChecksum(lease.checksumSha256, actual) === "mismatch") {
const verdict = judgeChecksum(lease.checksumSha256, actual);
if (verdict === "mismatch") {
throw new CliError("checksum_mismatch", `${finalPath} is the published size but does not match the published checksum.`, "Move or delete that file, then run the same command again to download it fresh.");
}
checksumVerified = verdict === "match";
output.info(`${fileName} is already downloaded, and its checksum matches.`);

@@ -62,4 +66,12 @@ }

}
output.result(finalPath);
return 0;
return await finishPull({
context,
options,
signal,
concurrency,
directory,
finalPath,
packageVersion: lease.packageVersion,
checksumVerified,
});
}

@@ -101,2 +113,3 @@ throw new CliError("storage_unwritable", `${finalPath} already exists and is a different size (${formatBytes(existing.size)} on disk, ${formatBytes(artifact.sizeBytes)} published).`, "Move or delete that file, or pass --out to a different directory.");

handle,
partPath,
concurrency,

@@ -119,2 +132,3 @@ rangesSupported: artifact.rangesSupported,

output.info(`Transferred ${formatBytes(sessionBytes)} in ${formatDuration(elapsedSeconds)}.`);
let checksumVerified = false;
if (sidecar.checksumSha256 !== null) {

@@ -136,2 +150,3 @@ let lastTickMs = 0;

if (verdict === "match") {
checksumVerified = true;
output.detail("Checksum verified.");

@@ -149,3 +164,26 @@ }

output.info(`Saved ${fileName}.`);
output.result(finalPath);
return await finishPull({
context,
options,
signal,
concurrency,
directory,
finalPath,
packageVersion: lease.packageVersion,
checksumVerified,
});
}
async function finishPull(input) {
if (input.options.weights === true) {
await installWeightsForKit(input.context, {
kitPath: input.finalPath,
destination: input.directory,
concurrency: input.concurrency,
signal: input.signal,
expectedSlug: input.options.slug,
packageVersion: input.packageVersion,
kitChecksumVerified: input.checksumVerified,
});
}
input.context.output.result(input.finalPath);
return 0;

@@ -260,4 +298,12 @@ }

const temp = `${path}.tmp`;
await writeFile(temp, serializeSidecar(sidecar), "utf8");
await rename(temp, path);
try {
await writeFile(temp, serializeSidecar(sidecar), "utf8");
await rename(temp, path);
}
catch (error) {
const local = localWriteFailure(path, error);
if (local !== null)
throw local;
throw error;
}
}
export const CLI_NAME = "runinfra";
export const CLI_PACKAGE = "@runinfra/cli";
export const CLI_VERSION = "0.2.2";
export const CLI_VERSION = "0.2.3";
export const CLI_CLIENT_ID = "runinfra-cli";

@@ -5,0 +5,0 @@ export const MINIMUM_NODE_MAJOR = 20;

{
"name": "@runinfra/cli",
"version": "0.2.2",
"version": "0.2.3",
"description": "RunInfra CLI: browser-approved sign-in and resumable downloads for optimized model packages",

@@ -5,0 +5,0 @@ "license": "SEE LICENSE IN LICENSE",

+246
-64

@@ -10,2 +10,4 @@ # @runinfra/cli

$ runinfra pull <slug> --out ./models
# Recipe package only: also fetch its exact pinned source weights
$ runinfra pull <slug> --out ./models --weights
```

@@ -21,30 +23,33 @@

<!-- Release note for the next editor: when the installer and the wheel are
actually published, the standalone line becomes the lead command in the
block above and its Status cell becomes "Live". Do not promote a row
before fetching its artifact and installing from it: the Status column
is the only thing standing between a reader and a command that 404s. -->
<!-- For the next editor: never add a channel row before you have fetched its
artifact and installed from it. That rule used to be enforced by a Status
column, which is gone because it rotted: it went on saying "Not published
yet" for months after both channels were live, on the npm page, which is
the first thing a buyer reads. A row present here is a promise the command
works, so earn it by running the command, not by editing a cell. -->
Run the npm line above if the machine already has Node 20 or newer. It is the
only channel published today.
Three ways in, all live. These kits get pulled onto GPU hosts, and a GPU host
often has Python but no Node, so pick whichever matches the machine you are
standing on:
Two more channels are on the way, because these kits get pulled onto GPU hosts
and a GPU host often has Python but no Node:
| Channel | Command | Reach for it when |
| --- | --- | --- |
| Standalone | `curl -fsSL https://raw.githubusercontent.com/RightNow-AI/runinfra-cli/main/install.sh \| sh` | The box is bare. No Node, no Python, the download brings its own runtime. |
| Python | `pip install runinfra-cli` | The machine already lives in Python. |
| Node | `npm install -g @runinfra/cli` | The machine already lives in Node 20 or newer. |
| Channel | Command | Reach for it when | Status |
| --- | --- | --- | --- |
| Standalone | `curl -fsSL https://raw.githubusercontent.com/RightNow-AI/runinfra-cli/main/install.sh \| sh` | The box is bare. No Node, no Python, the download brings its own runtime. | Not published yet |
| Python | `pip install runinfra-cli` | The machine already lives in Python. | Not published yet |
| Node | `npm install -g @runinfra/cli` | The machine already lives in Node 20 or newer. | **Live, 0.1.1** |
On Windows the standalone line is
`irm https://raw.githubusercontent.com/RightNow-AI/runinfra-cli/main/install.ps1 | iex`.
Only the npm row works right now. Checked on 2026-07-27:
`pip index versions runinfra-cli` answers "No matching distribution found",
and the installer URL returns 404 because the script is not on the release
repository's main branch yet. Both are written down with their real commands
rather than left out, so the release that publishes them only has to flip a
Status cell, and so a reader can tell today what works from what does not.
This table deliberately carries **no version and no status column**. It used to
carry both, and both rotted: after the release that took every channel live, a
reader on npm was still being told that two of the three did not exist and that
the client was two versions behind. A number written into prose is a number
nobody updates. For what is actually published right now, ask the registries:
`npm view @runinfra/cli version` and `pip index versions runinfra-cli`.
All three publish at the same version, **0.1.1**, and put the same `runinfra`
command on PATH: the two new channels repackage the client that is already on
npm rather than changing it. So `runinfra --version` reads the same however it
All three channels publish the same version at the same time and put the same
`runinfra` command on PATH: the standalone and Python channels repackage the
client that is already on npm rather than changing it. So `runinfra --version`
reads the same however it
arrived, and every command in this document behaves identically. What differs

@@ -164,3 +169,4 @@ is only what has to be on the machine first:

key, or store a key by hand. Approving in the browser mints one, the CLI
writes it to a file only you can read, and revoking it in Settings kills it.
writes it to a file only you can read, and either `runinfra logout` or
Settings kills it.
- **It never prints the key.** `whoami` and `logout` show a redacted form

@@ -180,4 +186,4 @@ (`rp_k...8fq2`). No other code path renders it.

| `runinfra login [--device]` | Connects this terminal by having you approve it in a browser. |
| `runinfra pull <slug> [--out DIR] [--concurrency N]` | Downloads a package this workspace owns. Resumable. |
| `runinfra logout` | Deletes the local credential and tells you where to revoke the key. |
| `runinfra pull <slug> [--out DIR] [--concurrency N] [--weights]` | Downloads a package this workspace owns. `--weights` also fetches a recipe package's pinned source weights. Resumable. |
| `runinfra logout [--local]` | Revokes this machine's key and deletes the stored copy. |
| `runinfra whoami` | Shows what this machine has stored, and when its access expires. |

@@ -337,24 +343,84 @@

## Revoking access
## `runinfra logout`
Two different actions, and the CLI is careful not to confuse them.
Signing out means "this machine no longer has access", so `logout` revokes the
key it is holding and then deletes the local copy.
**`runinfra logout` removes the local file. It does not revoke the key.**
```console
$ runinfra logout
Revoked this machine's key and removed the local credentials.
The key rp_k3n9 (rp_k...8fq2) no longer works anywhere.
```
### The order, and why it is that order
1. **The revoke goes first, while the key is still on disk.** The key is what
authenticates the revoke, so deleting first would throw away the only
credential that can prove the request.
2. **The local file is then deleted no matter what the revoke did.** A logout
on a plane, behind a corporate proxy, or during an outage still signs this
machine out. A network step is never allowed to veto that.
3. **You are told which of the two happened**, in those words, every time.
### When the revoke does not go through
The local key is gone, the remote key is not, and the CLI says so rather than
letting you walk away from a lost laptop thinking it is handled:
```console
$ runinfra logout
Removed this machine's credentials.
The key rp_k3n9 (rp_k...8fq2) is still valid until 2026-10-24T09:14:02.118Z.
warning: The key was NOT revoked, so it is still live.
runinfra.ai did not answer within 5000ms.
The key rp_k3n9 (rp_k...8fq2) is STILL VALID until 2026-10-24T09:14:02.118Z.
To end its access now, revoke it in Settings, API keys on runinfra.ai.
```
That wording is deliberate. A lost laptop is not handled by deleting a file you
no longer have access to.
The revoke call has a **five second ceiling** covering connect, headers and
body together, so `logout` cannot hang. A timeout, a refused connection, a 5xx
and a deployment too old to have the route all land here, each naming its own
cause.
**To actually end a terminal's access:** open **Settings, API keys** on
runinfra.ai. Every terminal you connected is listed there as
`RunInfra CLI: user@host`, with its prefix, when it was created, and when it was
last used. Revoke the row. The next command that terminal runs is refused:
**The exit code is `0` in both cases**, matching `npm logout` and
`gh auth logout`. The file this machine had is gone, which is what the command
is named for, and `logout || true` in a CI teardown keeps working. The
incompleteness is carried by the warning, not by the exit status.
### `--local`
Deletes the stored key and makes **no network call at all**. For an offline
machine, and for teardown scripts that must not reach the network:
```console
$ runinfra logout --local
Removed this machine's credentials.
Skipped the revoke because --local was passed. No network call was made.
The key rp_k3n9 (rp_k...8fq2) is STILL VALID until 2026-10-24T09:14:02.118Z.
To end its access now, revoke it in Settings, API keys on runinfra.ai.
```
Logging out a machine that was never signed in prints
`This machine was not signed in.`, exits `0`, and also makes no network call:
there is no key, so there is nothing to authenticate one with.
### What the revoke endpoint can and cannot do
`POST /api/cli/revoke` revokes **the key that called it, and nothing else**.
There is no id in the path, none in the query, and the body is never read, so
there is nothing for a caller to name. It cannot list keys, cannot see another
terminal, and cannot touch any other resource. A leaked credentials file
therefore gains exactly one new power: destroying the credential it already is.
It is idempotent. Revoking a key that is already revoked or already expired
answers success, because the thing you asked for ("this key is dead") is
already true.
### Revoking someone else's terminal
Open **Settings, API keys** on runinfra.ai. Every terminal you connected is
listed there as `RunInfra CLI: user@host`, with its prefix, when it was created,
and when it was last used. Revoke the row. The next command that terminal runs
is refused:
```console
$ runinfra pull qwen3-6-27b-fp8cd-v3-mlponly-h100-vllm

@@ -365,5 +431,6 @@ error: This terminal's access was revoked.

Revocation takes effect on the next request, and your role in the workspace is
re-read on every request too. Someone removed from a workspace, or demoted below
deploy, loses CLI access immediately rather than at their next key rotation.
Revocation takes effect on the next request, whichever way it was done, and your
role in the workspace is re-read on every request too. Someone removed from a
workspace, or demoted below deploy, loses CLI access immediately rather than at
their next key rotation.

@@ -391,5 +458,44 @@ One honest limit: a download URL already minted stays valid for its 30 minute

Options: `--out DIR` (defaults to the working directory) and `--concurrency N`
(1 to 16, defaults to 8).
Options: `--out DIR` (defaults to the working directory), `--concurrency N`
(1 to 16, defaults to 8), and `--weights`.
### Fetching recipe weights
A recipe package does not redistribute model weights. Its signed kit records
the Hugging Face repository, a full immutable commit, the authoritative file
list, and the expected weights-tree sha256. Add `--weights` to fetch and verify
those files:
```console
$ runinfra pull recipe-model --out /data/models --weights
Saved kit.zip.
Fetching acme/model@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa (12.4 GB) into /data/models/recipe-model/weights
Verifying the signed weights-tree checksum.
Pinned weights tree checksum verified.
Saved pinned weights in /data/models/recipe-model/weights.
/data/models/kit.zip
```
The final kit path remains the only stdout line. Weight progress and verification
stay on stderr. Partial files and the resume record live beside `weights/`, not
inside it, so the kit's own verifier sees only completed signed files.
The final check walks the whole `weights/` directory, not only the files the
package signed, so a file the package did not sign fails the pull and is named in
the error. That matters when you pull into a directory that still holds an older
version of the same package: every pinned file arrives correctly, but the leftover
is still there, and a serving engine that globs `*.safetensors` would load it
alongside the pinned ones. Pull into an empty directory, or remove what the error
names.
Extract `kit.zip` into the same `--out` directory and its archive root joins the
already downloaded `weights/` directory.
For a package that already contains optimized weights, `--weights` exits 0 and
prints that nothing additional was downloaded.
Gated models require two things: accept the model license on huggingface.co, and
set a Hugging Face read token in `HF_TOKEN`. The token is sent only to the
Hugging Face origin and is removed before following a cross-origin CDN redirect.
### How resume works

@@ -449,2 +555,23 @@

Every chunk is also checked as it arrives, which matters because the checksum
is a verdict that lands hours later and only for packages that publish one. A
`206 Partial Content` is a **claim** about which bytes follow it, and the CLI
now reads the `Content-Range` header that carries that claim before a single
byte is written. The response has to state exactly the range that was asked
for, and if it names a total, that total has to be the size of the object being
downloaded. A missing, unreadable or disagreeing `Content-Range` stops the
download at exit `6`, and, unlike a dropped connection, it is not retried:
```console
error: storage.runinfra.ai did not serve the byte range that was asked for: it answered 206 for bytes 33554432-67108863 with bytes 0-33554431.
The download stopped rather than write bytes it cannot place. This is a broken or hostile origin, not a connection problem, so report it to support with the package slug rather than retrying.
```
The failure this closes is specific. A proxy, CDN or mirror that returns the
right **number** of bytes from the **wrong** offset used to have them written
at the offset the CLI asked for. On a resumed multi-gigabyte download the file
still reaches its full length, every range is still recorded as durable, and
nothing looks wrong until a whole-file hash disagrees at the end, if there is
one to disagree.
### Before it writes anything

@@ -461,2 +588,17 @@

A preflight cannot see a volume that fills up later, from something else on the
machine, while the transfer is running. When that happens the write is what
fails, so the CLI names the file it was writing rather than the host it was
reading from, and exits `7` like the preflight above:
```console
error: The volume holding /data/models/kit.zip.part ran out of space.
Free some space, or pass --out to a larger volume, then run the same command again to resume from the bytes already on disk.
```
An exhausted quota, a read-only mount and a permission refusal are reported the
same way, each naming its own cause. None of them is retried: the volume is not
going to change between the first attempt and the fourth, and retrying only
delays the one sentence the user can act on.
---

@@ -473,9 +615,27 @@

| 2 | Bad usage, or an unsupported Node runtime | No |
| 3 | Not signed in, denied, expired, or revoked | After `runinfra login` |
| 3 | Not signed in, denied, expired, revoked, or refused by a gated Hugging Face source | After fixing authentication or source access |
| 4 | The workspace does not own the package, or its files are not published yet | No |
| 5 | Network or server failure | Yes |
| 6 | Integrity failure: checksum mismatch, or the artifact changed mid-download | Start over |
| 7 | No space, or the destination is not writable | After freeing space |
| 5 | Network, server, or rate-limit failure, including an origin that advertises byte ranges and then will not serve them | Yes |
| 6 | Integrity failure: checksum mismatch, wrong pinned revision, a 206 that did not describe the requested range, or the artifact changed mid-download | Start over |
| 7 | No space, quota exhausted, or the destination is not writable | After freeing space |
| 130 | Interrupted | Yes, it resumes |
`runinfra logout` is `0` whenever the local key is gone, including when the
revoke could not be made. That is deliberate and matches `npm logout` and
`gh auth logout`: the command did the thing it is named for on this machine, so
a non-zero would break `logout || true` and every CI teardown. What the revoke
did is carried by the message, which states plainly whether the key is dead or
still live.
Two of these rows were wrong until they were measured against what the client
actually does, and both errors pointed a script at the wrong recovery. An
origin that ignores `Range` was reported as `6`, which tells a caller the bytes
on disk are corrupt and the download must start over, when the bytes are fine
and the origin simply cannot do resumable transfers. A disk that filled up
mid-download was reported as `5`, naming the storage host, which sent people to
check a connection that was working. Ranges are a **capability** and belong in
`5`; a full disk, an exhausted quota, a read-only mount and a permission
refusal are the **local machine** and belong in `7`, with the path in the
message rather than the host.
---

@@ -489,2 +649,3 @@

| `RUNINFRA_CONFIG_DIR` | Where credentials are stored. Overrides the platform default. |
| `HF_TOKEN` | Hugging Face read token used only for a gated recipe package's pinned source files. |

@@ -509,11 +670,13 @@ The base a key was minted against travels with the key. If you sign in against a

| `POST /api/cli/token` | Loopback authorization code exchange, the browser flow's final step |
| `POST /api/cli/revoke` | Revokes the calling key, and only the calling key |
| `GET /cli/authorize` | The approval page, browser only |
| `GET /api/catalog/<slug>/download` | Dual auth, accepts a `purpose='cli'` bearer key |
Path choices are forced by the CSRF origin check: `/api/cli/token` and
`/api/cli/device/` are exempt from it, because no non-browser HTTP client sends
`Origin`. `/api/cli/authorize` is **not** exempt (that one is the browser
approving with session cookies), so the CLI never calls it. **Any new
CLI-called POST route must live under one of the two exempt prefixes or it will
403 on every platform.**
Path choices are forced by the CSRF origin check: `/api/cli/token`,
`/api/cli/device/` and `/api/cli/revoke` are exempt from it, because no
non-browser HTTP client sends `Origin`. `/api/cli/authorize` is **not** exempt
(that one is the browser approving with session cookies), so the CLI never
calls it. **Any new CLI-called POST route must be added to
`EXEMPT_PATH_PREFIXES` in `lib/csrf/origin-check.ts` or it will 403 on every
platform**, and `app/api/cli/route-surface.test.ts` fails until it is.

@@ -543,2 +706,9 @@ ### Refusal envelope

`POST /api/cli/revoke` shares that envelope, because it authenticates the same
way. It answers `{ "ok": true }` on success and refuses with
`missing_credentials`, `invalid_key`, `workspace_access_revoked`,
`auth_unavailable`, `rate_limited` or `server_error`. `key_revoked` and
`key_expired` are deliberately absent: both mean the key is already dead, which
is the outcome that was asked for, so both answer `200 { "ok": true }`.
### Sign-in success body

@@ -568,4 +738,4 @@

Two things this CLI wants still do not exist. It degrades honestly rather than
pretending, and each becomes a small change here when it lands.
One thing this CLI wants still does not exist. It degrades honestly rather than
pretending, and it becomes a small change here when it lands.

@@ -575,9 +745,7 @@ 1. **Identity.** There is no `GET /api/cli/whoami`, so `whoami` reports the

access it reports may already have been revoked from Settings.
2. **Revocation from the terminal.** `logout` removes the local credential and
points you at Settings; it does not claim to have revoked the key, which
stays valid until its expiry. Ending a key from the terminal itself is not
offered yet: revoke it from **Settings, API keys** on runinfra.ai, which
takes effect on that terminal's next request.
The browser sign-in flow that this document once listed as unbuilt now exists.
Terminal revocation, which this document listed here as unbuilt, now exists:
`POST /api/cli/revoke` kills the calling key, and `runinfra logout` calls it
before it deletes the local file. The browser sign-in flow it also once listed
as unbuilt exists too.
It is created by `POST /api/cli/device/loopback-init`, which takes

@@ -600,6 +768,20 @@ `{ codeChallenge, codeChallengeMethod, scope, redirectUri, deviceLabel }` and

Tests cover the pure logic, deliberately: range planning and resume arithmetic,
sidecar validation, the lease-refresh decision, per-platform browser command
selection, redirect construction for IPv4 and IPv6, credential and path
resolution, and every response parser. Nothing in the test suite opens a socket
to the internet.
Tests cover the pure logic first: range planning and resume arithmetic, sidecar
validation, the lease-refresh decision, per-platform browser command selection,
redirect construction for IPv4 and IPv6, credential and path resolution, and
every response parser.
The rest run against a **real HTTP server on a loopback port**, because the
defects that reached users did not live in the pure logic. Credential leakage
across a redirect, a `Content-Range` nobody read, and an errno raised inside a
response handler are all properties of how `node:http` is called and what is
done with what comes back, and a mock of that call is exactly the thing that
would not have caught them. So `http-redirect-credentials.test.ts`,
`range-integrity.test.ts` and `logout.test.ts` stand up an origin, misbehave in
a specific way, and assert on what lands on disk. `logout.test.ts` in
particular needs a real socket: a mocked transport cannot express a server that
accepts a connection and then never answers, or a port with nothing behind it,
and those are two of the three ways the revoke has to fail safely.
Nothing in the test suite opens a socket to the internet: every server it talks
to is one it started on `127.0.0.1`.