openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
291 lines (290 loc) • 11.4 kB
JavaScript
import { t as hasErrnoCode } from "./errno-FR4CRyya.mjs";
import "./errors-BSdPMFao.mjs";
import { t as createSubsystemLogger } from "./subsystem-BlrgtxyM.mjs";
import { i as UPDATE_RUNNER_TIMEOUT_MS } from "./update-run-timeouts-Byb-PlTk.mjs";
import { n as awaitWithinDeadline, t as ABSOLUTE_DEADLINE_EXPIRED } from "./absolute-deadline-C4A1_R9J.mjs";
import { r as readPackageVersion } from "./package-json-7gGhB8Vf.mjs";
import { constants } from "node:fs";
import path from "node:path";
import fs$1 from "node:fs/promises";
import { createHash } from "node:crypto";
//#region src/infra/package-update-integrity.ts
const MAX_TREE_BYTES = 1073741824;
const MAX_TREE_ENTRIES = 5e4;
const MAX_MANIFEST_BYTES = 1048576;
const MAX_LAUNCHER_BYTES = 1048576;
const log = createSubsystemLogger("update/package-integrity");
let readerSequence = 0;
function packageLauncherDifferences(expected, actual, ownershipPreserved = true) {
const symlink = expected.type === "symlink" && actual.type === "symlink";
return [
"type",
"mode",
"uid",
"gid",
"contents"
].filter((field) => !(symlink && (field === "mode" || !ownershipPreserved && (field === "uid" || field === "gid"))) && expected[field] !== actual[field]).map((field) => field === "contents" && symlink ? "target" : field);
}
var PackageIntegrityTimeoutError = class extends Error {
constructor(budgetMs) {
super("Package rollback verification timed out");
this.budgetMs = budgetMs;
}
};
/** Resource exhaustion is distinct from a filesystem-integrity failure. */
var PackageIntegrityLimitError = class extends Error {
constructor(resource) {
super(`Package rollback verification ${resource} limit exceeded`);
this.resource = resource;
}
};
function isPackageIntegrityResourceError(error) {
return error instanceof PackageIntegrityTimeoutError || error instanceof PackageIntegrityLimitError;
}
async function readPackageVersionIfPresent(packageRoot) {
return packageRoot ? readPackageVersion(packageRoot) : null;
}
function identity(stat) {
return `${stat.dev}:${stat.ino}`;
}
function metadata(stat) {
return [
identity(stat),
stat.mode.toString(),
stat.uid.toString(),
stat.gid.toString(),
stat.nlink.toString(),
stat.size.toString(),
stat.mtimeNs.toString(),
stat.ctimeNs.toString()
];
}
function unchanged(left, right) {
return left.ino !== 0n && metadata(left).join("/") === metadata(right).join("/");
}
/** Read-only, bounded observations. These do not exclude writers or seal an inode. */
function createPackageIntegrityReader(timeoutMs = UPDATE_RUNNER_TIMEOUT_MS) {
const now = () => performance.now();
const startedAtMonotonicMs = now();
const budget = Number.isFinite(timeoutMs) ? Math.max(1, timeoutMs) : UPDATE_RUNNER_TIMEOUT_MS;
const deadlineAtMonotonicMs = startedAtMonotonicMs + budget;
const timing = {
readerId: `${process.pid}:${++readerSequence}`,
timeOriginUnixMs: performance.timeOrigin,
startedAtMonotonicMs,
budgetMs: budget,
deadlineClock: "monotonic",
deadlineAtMonotonicMs
};
let timeoutObservedAtMonotonicMs;
let pendingIo = 0;
async function trackIo(operation) {
pendingIo++;
try {
return await operation();
} finally {
pendingIo--;
}
}
async function observe(phase, operation) {
const emit = (event, facts) => {
try {
log.debug(event, {
...timing,
phase,
event,
...facts
});
} catch {}
};
emit("reader-started");
let outcome = "failed";
try {
const result = await operation();
outcome = "completed";
return result;
} finally {
const settledAtMonotonicMs = performance.now();
emit("reader-settled", {
settledAtMonotonicMs,
elapsedMs: settledAtMonotonicMs - startedAtMonotonicMs,
outcome: timeoutObservedAtMonotonicMs === void 0 ? outcome : "timed-out",
timeoutObservedAtMonotonicMs,
pendingIo
});
}
}
async function read(operation, closeLate) {
let pending;
const value = await awaitWithinDeadline(() => pending = trackIo(operation), deadlineAtMonotonicMs, now);
if (value === ABSOLUTE_DEADLINE_EXPIRED) {
timeoutObservedAtMonotonicMs ??= performance.now();
if (pending && closeLate) pending.then((late) => trackIo(() => closeLate(late)), () => {}).catch(() => {});
throw new PackageIntegrityTimeoutError(budget);
}
return value;
}
async function close(resource) {
const closing = trackIo(() => resource.close()).catch(() => {});
if (await awaitWithinDeadline(() => closing, deadlineAtMonotonicMs, now) === ABSOLUTE_DEADLINE_EXPIRED) timeoutObservedAtMonotonicMs ??= performance.now();
}
async function entries(directoryPath, limit = MAX_TREE_ENTRIES) {
const directory = await read(() => fs$1.opendir(directoryPath), (late) => late.close());
const children = [];
try {
while (true) {
const child = await read(() => directory.read());
if (!child) break;
if (children.length >= limit) throw new PackageIntegrityLimitError("entry");
children.push(child.name);
}
} finally {
await close(directory);
}
return children.toSorted();
}
async function hashFile(file, stat, remainingBytes) {
if (!stat.isFile()) throw new Error("Package rollback verification byte limit exceeded");
if (stat.size > BigInt(remainingBytes)) throw new PackageIntegrityLimitError("byte");
const handle = await read(() => fs$1.open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK), (late) => late.close());
try {
if (!unchanged(stat, await read(() => handle.stat({ bigint: true })))) throw new Error("Package rollback file changed before reading");
const hash = createHash("sha256");
const buffer = Buffer.allocUnsafe(65536);
const size = Number(stat.size);
let position = 0;
while (position < size) {
const { bytesRead } = await read(() => handle.read(buffer, 0, Math.min(buffer.length, size - position), position));
if (bytesRead === 0) throw new Error("Package rollback file changed while reading");
position += bytesRead;
hash.update(buffer.subarray(0, bytesRead));
}
if (!unchanged(stat, await read(() => handle.stat({ bigint: true })))) throw new Error("Package rollback file changed while reading");
return {
digest: hash.digest("hex"),
bytes: position
};
} finally {
await close(handle);
}
}
async function tree(root, originalRoot = root) {
const digest = createHash("sha256");
const observed = [];
const hardlinks = /* @__PURE__ */ new Map();
let bytes = 0;
let remainingEntries = 49999;
let device;
let rootIdentity = "";
async function visit(file, relative) {
const stat = await read(() => fs$1.lstat(file, { bigint: true }));
if (stat.ino === 0n || device !== void 0 && device !== stat.dev) throw new Error("Package rollback filesystem identity is unavailable");
if (!relative) {
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("Package rollback root is not a retained directory");
device = stat.dev;
rootIdentity = identity(stat);
}
observed.push({
file,
stat
});
const fields = metadata(stat);
if (!relative) fields.pop();
digest.update(JSON.stringify([relative, fields]));
if (stat.isSymbolicLink()) {
const target = await read(() => fs$1.readlink(file));
const resolved = path.relative(originalRoot, path.resolve(path.dirname(path.join(originalRoot, relative)), target));
let descended = false;
for (const segment of target.split(/[\\/]+/)) {
if (!segment || segment === ".") continue;
if (segment === ".." && descended) throw new Error("Package rollback symlink has ambiguous parent traversal");
descended ||= segment !== "..";
}
if (path.isAbsolute(resolved) || resolved === ".." || resolved.startsWith(`..${path.sep}`)) throw new Error("Package rollback symlink leaves the retained tree");
digest.update(JSON.stringify(["symlink", target]));
} else if (stat.isFile()) {
const contents = await hashFile(file, stat, MAX_TREE_BYTES - bytes);
bytes += contents.bytes;
const key = identity(stat);
const owner = stat.nlink > 1n ? hardlinks.get(key) ?? relative : null;
if (owner !== null) hardlinks.set(key, owner);
digest.update(JSON.stringify([
"file",
owner,
contents.digest
]));
} else if (stat.isDirectory()) {
const children = await entries(file, remainingEntries);
remainingEntries -= children.length;
for (const child of children) await visit(path.join(file, child), relative ? `${relative}/${child}` : child);
} else throw new Error("Package rollback contains a non-file entry");
}
await visit(root, "");
const version = await read(() => readPackageVersion(root, { maxBytes: MAX_MANIFEST_BYTES }));
if (!version) throw new Error("Package rollback version is unavailable");
for (const entry of observed) if (!unchanged(entry.stat, await read(() => fs$1.lstat(entry.file, { bigint: true })))) throw new Error("Package rollback tree changed during verification");
return {
digest: digest.digest("hex"),
identity: rootIdentity,
version
};
}
async function rootEntry(root, originalRoot = root, expectedKind) {
const stat = await read(() => fs$1.lstat(root, { bigint: true }));
if (expectedKind && expectedKind !== (stat.isSymbolicLink() ? "link" : "directory")) throw new Error("Package rollback root entry kind changed");
if (!stat.isSymbolicLink()) return {
kind: "directory",
tree: await tree(root, originalRoot)
};
const target = await read(() => fs$1.readlink(root));
if (!unchanged(stat, await read(() => fs$1.lstat(root, { bigint: true })))) throw new Error("Package rollback link changed while reading");
return {
kind: "link",
metadata: metadata(stat).slice(0, -1),
target
};
}
async function directoryIdentity(root) {
const stat = await read(() => fs$1.lstat(root, { bigint: true }));
if (stat.isSymbolicLink()) return null;
if (!stat.isDirectory() || stat.ino === 0n) throw new Error("Package rollback filesystem identity is unavailable");
const version = await read(() => readPackageVersion(root, { maxBytes: MAX_MANIFEST_BYTES }));
if (!version || !unchanged(stat, await read(() => fs$1.lstat(root, { bigint: true })))) throw new Error("Package rollback identity changed or version is unavailable");
return {
identity: identity(stat),
version
};
}
async function launcher(file) {
const stat = await read(() => fs$1.lstat(file, { bigint: true }));
const contents = stat.isSymbolicLink() ? await read(() => fs$1.readlink(file)) : (await hashFile(file, stat, MAX_LAUNCHER_BYTES)).digest;
if (!unchanged(stat, await read(() => fs$1.lstat(file, { bigint: true })))) throw new Error("Package rollback launcher changed during verification");
return {
type: stat.isSymbolicLink() ? "symlink" : "file",
mode: stat.mode.toString(),
uid: stat.uid.toString(),
gid: stat.gid.toString(),
contents
};
}
async function exists(file) {
try {
await read(() => fs$1.lstat(file));
return true;
} catch (error) {
if (hasErrnoCode(error, "ENOENT")) return false;
throw error;
}
}
return {
tree,
rootEntry,
directoryIdentity,
launcher,
exists,
entries,
observe
};
}
//#endregion
export { readPackageVersionIfPresent as a, packageLauncherDifferences as i, createPackageIntegrityReader as n, isPackageIntegrityResourceError as r, PackageIntegrityTimeoutError as t };