Confine checked artifact loading and browser asset lookup

This commit is contained in:
Timothy J. Aveni
2026-09-19 23:38:06 -07:00
parent ac36edf62d
commit 0aaa80a4ba
6 changed files with 303 additions and 46 deletions
+155
View File
@@ -0,0 +1,155 @@
import { constants } from "node:fs";
import { lstat, open, realpath } from "node:fs/promises";
import path from "node:path";
import { createHash } from "node:crypto";
import {
artifactPath,
parsePackageArtifacts,
checkExecutionWorld,
checkedWorldDigest,
type ArtifactReader,
type ArtifactFile,
type PackageArtifacts,
type CheckedWorld,
} from "./artifacts.js";
import { type ExecutionPlan, ExecutionPlanError } from "./plan.js";
const maxFileBytes = 256 * 1024 * 1024;
const metadataLimit = 16 * 1024 * 1024;
const fail = (where: string, message: string): never => {
throw new ExecutionPlanError("ARTIFACT_FILE_INVALID", where, message);
};
/** Roots must be trusted immutable output directories. Refuse symlinks, including
* interior directory links. Hash verification is still required before serving bytes. */
export async function artifactFileReader(roots: Readonly<Record<string, string>>): Promise<ArtifactReader> {
const directories = new Map<string, string>();
for (const [revision, root] of Object.entries(roots)) {
if (!path.isAbsolute(root) || path.normalize(root) !== root || (await realpath(root)) !== root)
fail(revision, "Output root must be a canonical absolute directory");
const stat = await lstat(root);
if (!stat.isDirectory() || stat.isSymbolicLink()) fail(root, "Output root is not a directory");
directories.set(revision, root);
}
return async (revision, relative, limit) => {
const root = directories.get(revision);
if (!root) fail(revision, "Unknown package output");
artifactPath(relative);
if (!Number.isSafeInteger(limit) || limit < 0 || limit > maxFileBytes) fail(relative, "Invalid file byte limit");
const components = relative.split("/");
let resolved = root!;
for (let index = 0; index < components.length; index++) {
resolved = path.join(resolved, components[index]!);
const stat = await lstat(resolved);
if (stat.isSymbolicLink() || (index < components.length - 1 ? !stat.isDirectory() : !stat.isFile()))
fail(relative, "Only real directories and regular files are allowed");
}
const handle = await open(resolved, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
try {
const stat = await handle.stat();
if (!stat.isFile() || stat.size > limit) fail(relative, "File exceeds its declared bound");
// A fixed allocation and one extra byte detect growth without readFile's unbounded allocation.
const buffer = Buffer.alloc(stat.size + 1);
let offset = 0;
while (offset < buffer.length) {
const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
if (!bytesRead) break;
offset += bytesRead;
}
if (offset !== stat.size) fail(relative, "File changed while reading");
return buffer.subarray(0, offset);
} finally {
await handle.close();
}
};
}
export interface LoadedArtifactWorld {
world: CheckedWorld;
digest: string;
manifests: PackageArtifacts[];
read: ArtifactReader;
}
/** Immutable-output loader. No module code is imported into the coordinator. */
export async function loadArtifactWorld(
plan: ExecutionPlan,
roots: Readonly<Record<string, string>>,
): Promise<LoadedArtifactWorld> {
const read = await artifactFileReader(roots),
manifests: PackageArtifacts[] = [];
const expected = plan.definition.packages.map((p) => p.revision).sort();
if (JSON.stringify(Object.keys(roots).sort()) !== JSON.stringify(expected))
fail("world", "Output roots must exactly match the installed plan");
for (const revision of expected) {
const bytes = await read(revision, "share/quixos/package-artifacts.json", metadataLimit);
const manifest = parsePackageArtifacts(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)));
if (manifest.packageRevisionId !== revision) fail(revision, "Output belongs to another package revision");
manifests.push(manifest);
}
const world = await checkExecutionWorld(plan, manifests, read);
return { world, digest: checkedWorldDigest(world), manifests, read };
}
export interface BrowserArtifact {
url: string;
implementation: string;
packageRevisionId: string;
registryExport: string;
exports: readonly string[];
}
export interface ArtifactResponse {
status: 200 | 304;
headers: Record<string, string>;
body: Uint8Array;
}
/** This registry is placed behind the workspace gateway's existing authentication.
* Lookup uses an explicit URL table; request paths never become filesystem paths. */
export class BrowserArtifactGateway {
readonly entries: readonly BrowserArtifact[];
private readonly routes = new Map<string, { revision: string; file: ArtifactFile }>();
private readonly read: ArtifactReader;
constructor(loaded: LoadedArtifactWorld) {
this.read = loaded.read;
const entries: BrowserArtifact[] = [];
for (const manifest of loaded.manifests)
for (const implementation of manifest.implementations) {
if (implementation.target !== "browser" || implementation.compatibility !== "quixos-js-module-v1") continue;
const prefix = `/__quixos/artifacts/${loaded.digest}/${implementation.contentDigest}/`;
for (const file of implementation.files) {
if (!/^[a-zA-Z0-9.+-]+\/[a-zA-Z0-9.+-]+$/.test(file.mediaType)) fail(file.path, "Invalid asset MIME type");
const url = prefix + file.path.split("/").map(encodeURIComponent).join("/");
const previous = this.routes.get(url);
if (
previous &&
(previous.file.digest !== file.digest ||
previous.file.bytes !== file.bytes ||
previous.file.mediaType !== file.mediaType)
)
fail(url, "Conflicting browser asset route");
this.routes.set(url, { revision: manifest.packageRevisionId, file: Object.freeze({ ...file }) });
}
entries.push({
url: prefix + implementation.path.split("/").map(encodeURIComponent).join("/"),
implementation: implementation.id,
packageRevisionId: manifest.packageRevisionId,
registryExport: implementation.registryExport!,
exports: Object.freeze([...implementation.exports]),
});
}
this.entries = Object.freeze(entries.map((entry) => Object.freeze(entry)));
}
async response(url: string, ifNoneMatch?: string): Promise<ArtifactResponse | undefined> {
const route = this.routes.get(url);
if (!route) return undefined;
const bytes = await this.read(route.revision, route.file.path, route.file.bytes);
if (bytes.length !== route.file.bytes || createHash("sha256").update(bytes).digest("hex") !== route.file.digest)
fail(url, "Browser asset changed after world validation");
const etag = `"sha256-${route.file.digest}"`;
const headers = {
"content-type": route.file.mediaType,
"cache-control": "private, max-age=31536000, immutable",
etag,
"x-content-type-options": "nosniff",
};
return ifNoneMatch === etag
? { status: 304, headers, body: new Uint8Array() }
: { status: 200, headers, body: bytes };
}
}