mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-10 03:08:10 +02:00
125 lines
8.1 KiB
JavaScript
125 lines
8.1 KiB
JavaScript
import { createHash } from "node:crypto";
|
|
import { spawnSync } from "node:child_process";
|
|
import { chmod, lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
import { dirname, join, resolve, sep } from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
|
import { applyCursorRuntimePatch, CURSOR_RUNTIME_PATCH_VERSION } from "./cursor-runtime-patch.mjs";
|
|
|
|
const MANIFEST_PATH = fileURLToPath(new URL("../cursor-distributions.json", import.meta.url));
|
|
const MAX_ARCHIVE_BYTES = 256 * 1024 * 1024;
|
|
const MAX_TREE_BYTES = 1024 * 1024 * 1024;
|
|
const TREE_MANIFEST = ".paperclip-cursor-closure.json";
|
|
const digest = bytes => createHash("sha256").update(bytes).digest("hex");
|
|
|
|
export async function cursorDistribution(platform = process.platform, architecture = process.arch) {
|
|
const manifest = JSON.parse(await readFile(MANIFEST_PATH, "utf8"));
|
|
const distribution = manifest.platforms[`${platform}-${architecture}`];
|
|
if (!distribution) throw new Error(`Cursor has no pinned distribution for ${platform}/${architecture}`);
|
|
if (manifest.patchVersion !== CURSOR_RUNTIME_PATCH_VERSION || !/^[a-f0-9]{64}$/.test(distribution.vendorClosureSha256)) throw new Error("Cursor distribution must pin its owned runtime patch and vendor closure");
|
|
return { ...distribution, version: manifest.version, patchVersion: manifest.patchVersion, platform, architecture };
|
|
}
|
|
|
|
/** Hash every runtime file, including bundled Node, native addons and workers. */
|
|
export async function cursorDistributionClosure(directory) {
|
|
const entries = [];
|
|
let bytes = 0;
|
|
async function walk(root, prefix = "") {
|
|
const stat = await lstat(root);
|
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("Cursor closure contains a linked or non-directory root");
|
|
for (const name of (await readdir(root)).sort()) {
|
|
if (prefix === "" && name === TREE_MANIFEST) continue;
|
|
const path = prefix ? `${prefix}/${name}` : name;
|
|
if (/[\u0000-\u001f\u007f\\]/.test(path)) throw new Error("Cursor closure contains an unsafe path");
|
|
const absolute = join(root, name);
|
|
const before = await lstat(absolute);
|
|
if (before.isDirectory()) await walk(absolute, path);
|
|
else {
|
|
if (!before.isFile() || before.nlink !== 1) throw new Error("Cursor closure contains links or special files");
|
|
bytes += before.size;
|
|
if (bytes > MAX_TREE_BYTES || entries.length >= 10_000) throw new Error("Cursor closure exceeds size limits");
|
|
const source = await readFile(absolute);
|
|
const after = await lstat(absolute);
|
|
if (before.ino !== after.ino || before.dev !== after.dev || before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || source.length !== after.size || !after.isFile()) throw new Error("Cursor closure changed while being hashed");
|
|
entries.push({ path, sha256: digest(source), size: source.length, executable: Boolean(after.mode & 0o111) });
|
|
}
|
|
}
|
|
}
|
|
await walk(directory);
|
|
entries.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
return { entries, sha256: digest(JSON.stringify(entries)) };
|
|
}
|
|
|
|
export async function verifyCursorDistribution(directory, distribution) {
|
|
const closure = await cursorDistributionClosure(directory);
|
|
if (closure.sha256 !== distribution.closureSha256) throw new Error("Cursor full execution closure digest mismatch");
|
|
for (const required of [distribution.executable, distribution.entrypoint]) {
|
|
if (!closure.entries.some(entry => entry.path === required)) throw new Error("Cursor execution closure is missing its entrypoint");
|
|
}
|
|
return closure;
|
|
}
|
|
|
|
/** A pinned archive only; never runs the vendor installer or discovers PATH. */
|
|
export async function materializePinnedCursorDistribution(options = {}) {
|
|
const distribution = await cursorDistribution(options.platform, options.architecture);
|
|
if (!options.destination) throw new Error("Cursor materialization requires an explicit destination");
|
|
const destination = resolve(options.destination);
|
|
await mkdir(dirname(destination), { recursive: true, mode: 0o700 });
|
|
try {
|
|
await lstat(destination);
|
|
throw new Error("Cursor destination already exists; refusing to overwrite it");
|
|
} catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
const temporary = await mkdtemp(join(dirname(destination), ".cursor-install-"));
|
|
try {
|
|
let archive;
|
|
if (options.archivePath) {
|
|
const stat = await lstat(options.archivePath);
|
|
if (!stat.isFile() || stat.size > MAX_ARCHIVE_BYTES) throw new Error("Cursor archive must be a bounded regular file");
|
|
archive = await readFile(options.archivePath);
|
|
} else {
|
|
const response = await fetch(distribution.url, { redirect: "error", signal: AbortSignal.timeout(180_000) });
|
|
if (!response.ok || !response.body) throw new Error(`Cursor archive download failed: ${response.status}`);
|
|
const chunks = [];
|
|
let length = 0;
|
|
for await (const chunk of response.body) {
|
|
length += chunk.length;
|
|
if (length > MAX_ARCHIVE_BYTES) throw new Error("Cursor archive download exceeds size limit");
|
|
chunks.push(chunk);
|
|
}
|
|
archive = Buffer.concat(chunks);
|
|
}
|
|
if (digest(archive) !== distribution.archiveSha256) throw new Error("Cursor archive digest mismatch");
|
|
const archivePath = join(temporary, "archive.tar.gz");
|
|
const unpacked = join(temporary, "unpacked");
|
|
await writeFile(archivePath, archive, { mode: 0o600 });
|
|
await mkdir(unpacked, { mode: 0o700 });
|
|
// Only authenticated vendor archive bytes reach tar. Inspect entries first;
|
|
// links/special files are forbidden, and every path shares one fixed root.
|
|
for (const flag of ["-tzf", "-tvzf"]) {
|
|
const listing = spawnSync("/usr/bin/tar", [flag, archivePath], { encoding: "utf8", timeout: 30_000, maxBuffer: 4 * 1024 * 1024 });
|
|
if (listing.status !== 0) throw new Error("Cursor archive listing failed");
|
|
const lines = listing.stdout.trim().split("\n");
|
|
if (flag === "-tzf" && lines.some(path => !path.startsWith("dist-package/") || path.split("/").includes("..") || /[\u0000-\u001f\u007f\\]/.test(path))) throw new Error("Cursor archive contains unsafe paths");
|
|
if (flag === "-tvzf" && lines.some(line => !["-", "d"].includes(line[0]))) throw new Error("Cursor archive contains links or special entries");
|
|
}
|
|
const extraction = spawnSync("/usr/bin/tar", ["-xzf", archivePath, "-C", unpacked, "--no-same-owner"], { encoding: "utf8", timeout: 120_000 });
|
|
if (extraction.status !== 0) throw new Error("Cursor archive extraction failed");
|
|
const packageRoot = join(unpacked, "dist-package");
|
|
await verifyCursorDistribution(packageRoot, { ...distribution, closureSha256: distribution.vendorClosureSha256 });
|
|
await applyCursorRuntimePatch(packageRoot, `${distribution.platform}-${distribution.architecture}`);
|
|
const closure = await verifyCursorDistribution(packageRoot, distribution);
|
|
// Record the pinned inventory for a later runtime lease. Its digest must be
|
|
// checked against the checked-in distribution pin before trusting entries.
|
|
await writeFile(join(packageRoot, TREE_MANIFEST), `${JSON.stringify({ schema: "paperclip.cursor_closure.v1", ...distribution, entries: closure.entries })}\n`, { mode: 0o600 });
|
|
for (const entry of closure.entries) await chmod(join(packageRoot, ...entry.path.split("/")), entry.executable ? 0o500 : 0o400);
|
|
await chmod(packageRoot, 0o700);
|
|
await rename(packageRoot, destination);
|
|
return { version: distribution.version, patchVersion: distribution.patchVersion, platform: distribution.platform, architecture: distribution.architecture, destination, executable: join(destination, distribution.executable), entrypoint: join(destination, distribution.entrypoint), archiveSha256: distribution.archiveSha256, closureSha256: closure.sha256 };
|
|
} finally { await rm(temporary, { recursive: true, force: true }); }
|
|
}
|
|
|
|
if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
|
|
const [destination, platform, architecture, archivePath] = process.argv.slice(2);
|
|
process.stdout.write(`${JSON.stringify(await materializePinnedCursorDistribution({ destination, platform, architecture, archivePath }))}\n`);
|
|
}
|