Build checked portable package artifacts and shared runtime assets
This commit is contained in:
@@ -99,6 +99,13 @@ export interface ArtifactResponse {
|
||||
headers: Record<string, string>;
|
||||
body: Uint8Array;
|
||||
}
|
||||
/** Shared runtime imports use content identity, independent of the importing package/world.
|
||||
* The file remains covered by every importing implementation's checked manifest. */
|
||||
export function contentArtifactUrl(file: Pick<ArtifactFile, "path" | "digest">): string {
|
||||
if (!/^[0-9a-f]{64}$/.test(file.digest)) fail(file.path, "Invalid content digest");
|
||||
artifactPath(file.path);
|
||||
return `/__quixos/artifacts/content/${file.digest}/${encodeURIComponent(path.basename(file.path))}`;
|
||||
}
|
||||
/** 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 {
|
||||
@@ -124,6 +131,14 @@ export class BrowserArtifactGateway {
|
||||
)
|
||||
fail(url, "Conflicting browser asset route");
|
||||
this.routes.set(url, { revision: manifest.packageRevisionId, file: Object.freeze({ ...file }) });
|
||||
// Exact shared-file names are a packaging convention, never a path lookup capability.
|
||||
if (file.path.startsWith("share/quixos/shared/")) {
|
||||
const contentUrl = contentArtifactUrl(file);
|
||||
const existing = this.routes.get(contentUrl);
|
||||
if (existing && (existing.file.bytes !== file.bytes || existing.file.mediaType !== file.mediaType))
|
||||
fail(contentUrl, "Conflicting shared asset metadata");
|
||||
this.routes.set(contentUrl, { revision: manifest.packageRevisionId, file: Object.freeze({ ...file }) });
|
||||
}
|
||||
}
|
||||
entries.push({
|
||||
url: prefix + implementation.path.split("/").map(encodeURIComponent).join("/"),
|
||||
|
||||
@@ -2,3 +2,7 @@ export * from "./plan.js";
|
||||
export * from "./artifacts.js";
|
||||
export * from "./ports.js";
|
||||
export * from "./files.js";
|
||||
|
||||
export * from "./receipts.js";
|
||||
|
||||
export { generatePortableTypeScriptBindings } from "../bindings/portable.js";
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
export const transactionPortAbi = "quixos-transaction-ports-v1";
|
||||
export type PortAction =
|
||||
| { kind: "state"; primitive: "read" | "write" | "edit" }
|
||||
| { kind: "edge"; primitive: "resolve" | "connect" | "disconnect" }
|
||||
| { kind: "edge"; primitive: "resolve" | "disconnect" }
|
||||
| { kind: "edge"; primitive: "connect"; key: string }
|
||||
| { kind: "interface"; operationId: string; key?: string }
|
||||
| { kind: "query" }
|
||||
| { kind: "creation"; key: string };
|
||||
@@ -19,6 +20,7 @@ export interface PortInvocation {
|
||||
}
|
||||
export interface PackageInvocation {
|
||||
abi: typeof transactionPortAbi;
|
||||
workspace: string;
|
||||
frame: string;
|
||||
invocation: string;
|
||||
exportId: string;
|
||||
@@ -66,7 +68,8 @@ export function checkPortInvocation(value: unknown): PortInvocation {
|
||||
action.kind === "state"
|
||||
? exact(["kind", "primitive"]) && ["read", "write", "edit"].includes(action.primitive as string)
|
||||
: action.kind === "edge"
|
||||
? exact(["kind", "primitive"]) && ["resolve", "connect", "disconnect"].includes(action.primitive as string)
|
||||
? (exact(["kind", "primitive"]) && ["resolve", "disconnect"].includes(action.primitive as string)) ||
|
||||
(exact(["kind", "primitive", "key"]) && action.primitive === "connect" && identifier(action.key))
|
||||
: action.kind === "interface"
|
||||
? (exact(["kind", "operationId"]) || (exact(["kind", "operationId", "key"]) && identifier(action.key))) &&
|
||||
identifier(action.operationId)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { generatePortableTypeScriptBindings } from "../bindings/portable.js";
|
||||
import { type ExecutionPlan, ExecutionPlanError, hexDigest } from "./plan.js";
|
||||
import { type ArtifactReader, type PackageArtifacts } from "./artifacts.js";
|
||||
import { loadArtifactWorld, type LoadedArtifactWorld } from "./files.js";
|
||||
|
||||
export interface ArtifactReceipt {
|
||||
schemaVersion: 1;
|
||||
packageRevisionId: string;
|
||||
bindingDigest: string;
|
||||
sourceDigest: string;
|
||||
manifestDigest: string;
|
||||
generatorDigest: string;
|
||||
generated: { target: string; path: string; digest: string; bytes: number }[];
|
||||
}
|
||||
const hashBytes = (bytes: Uint8Array) => createHash("sha256").update(bytes).digest("hex");
|
||||
/** Deterministic expected receipt. A trusted build must also typecheck and bundle these exact sources.
|
||||
* This is consistency evidence inside a trusted Nix build path, not remote build attestation. */
|
||||
export function expectedArtifactReceipt(
|
||||
plan: ExecutionPlan,
|
||||
manifest: PackageArtifacts,
|
||||
generatorDigest: string,
|
||||
): { receipt: ArtifactReceipt; sources: Map<string, Uint8Array> } {
|
||||
if (!/^[0-9a-f]{64}$/.test(generatorDigest)) throw new Error("Invalid trusted generator digest");
|
||||
const pkg = plan.definition.packages.find((p) => p.revision === manifest.packageRevisionId);
|
||||
const schemaPackage = plan.definition.bindingSchema.packages.find((p) => p.revisionId === manifest.packageRevisionId);
|
||||
if (
|
||||
!pkg ||
|
||||
!schemaPackage ||
|
||||
manifest.bindingDigest !== pkg.bindingDigest ||
|
||||
manifest.sourceDigest !== pkg.sourceDigest
|
||||
)
|
||||
throw new ExecutionPlanError(
|
||||
"INVALID_RECEIPT",
|
||||
manifest.packageRevisionId,
|
||||
"Receipt package/schema/source mismatch",
|
||||
);
|
||||
const available = new Set(manifest.implementations.map((i) => i.target));
|
||||
const targets = [
|
||||
...new Set(
|
||||
schemaPackage.exports.flatMap((e) => (e.execution && e.execution.kind !== "remote" ? e.execution.targets : [])),
|
||||
),
|
||||
]
|
||||
.filter((t) => available.has(t))
|
||||
.sort();
|
||||
const sources = new Map<string, Uint8Array>();
|
||||
const generated = targets.map((target) => {
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(target)) throw new Error("Unsupported generated target filename");
|
||||
const path = `share/quixos/generated/${target}.ts`;
|
||||
const bytes = new TextEncoder().encode(
|
||||
generatePortableTypeScriptBindings(plan.definition.bindingSchema, pkg.revision, { target }),
|
||||
);
|
||||
sources.set(path, bytes);
|
||||
return { target, path, digest: hashBytes(bytes), bytes: bytes.length };
|
||||
});
|
||||
return {
|
||||
receipt: {
|
||||
schemaVersion: 1,
|
||||
packageRevisionId: pkg.revision,
|
||||
bindingDigest: pkg.bindingDigest,
|
||||
sourceDigest: pkg.sourceDigest,
|
||||
manifestDigest: hexDigest(manifest),
|
||||
generatorDigest,
|
||||
generated,
|
||||
},
|
||||
sources,
|
||||
};
|
||||
}
|
||||
export async function checkArtifactReceipt(
|
||||
plan: ExecutionPlan,
|
||||
manifest: PackageArtifacts,
|
||||
receipt: unknown,
|
||||
generatorDigest: string,
|
||||
read: ArtifactReader,
|
||||
): Promise<void> {
|
||||
const expected = expectedArtifactReceipt(plan, manifest, generatorDigest);
|
||||
if (hexDigest(receipt) !== hexDigest(expected.receipt))
|
||||
throw new ExecutionPlanError(
|
||||
"INVALID_RECEIPT",
|
||||
manifest.packageRevisionId,
|
||||
"Receipt differs from the trusted compiler/schema/artifact expectation",
|
||||
);
|
||||
for (const file of expected.receipt.generated) {
|
||||
const actual = await read(manifest.packageRevisionId, file.path, file.bytes);
|
||||
if (actual.length !== file.bytes || hashBytes(actual) !== file.digest)
|
||||
throw new ExecutionPlanError("INVALID_RECEIPT", file.path, "Generated bindings were replaced or omitted");
|
||||
}
|
||||
}
|
||||
/** Activation uses this entrypoint with its trusted generator identity, not the manifest-only loader. */
|
||||
export async function loadCheckedArtifactWorld(
|
||||
plan: ExecutionPlan,
|
||||
roots: Readonly<Record<string, string>>,
|
||||
generatorDigest: string,
|
||||
): Promise<LoadedArtifactWorld> {
|
||||
const loaded = await loadArtifactWorld(plan, roots);
|
||||
for (const manifest of loaded.manifests) {
|
||||
const bytes = await loaded.read(manifest.packageRevisionId, "share/quixos/build-receipt.json", 16 * 1024 * 1024);
|
||||
const receipt: unknown = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
||||
await checkArtifactReceipt(plan, manifest, receipt, generatorDigest, loaded.read);
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
Reference in New Issue
Block a user