2026-09-20 00:25:53 -07:00
|
|
|
import { createHash } from "node:crypto";
|
2026-09-20 20:58:01 -07:00
|
|
|
import { generatePortableTypeScriptBindings, generateRemoteTypeScriptBindings } from "../bindings/portable.js";
|
2026-09-20 00:25:53 -07:00
|
|
|
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",
|
|
|
|
|
);
|
2026-09-21 13:30:33 -07:00
|
|
|
// A process service may implement transactions in any language. Only the
|
|
|
|
|
// checked JS module path promises generated TypeScript adapter evidence.
|
|
|
|
|
const available = new Set(
|
|
|
|
|
manifest.implementations.filter((i) => i.compatibility === "quixos-js-module-v1").map((i) => i.target),
|
|
|
|
|
);
|
2026-09-20 00:25:53 -07:00
|
|
|
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 };
|
|
|
|
|
});
|
2026-09-20 20:58:01 -07:00
|
|
|
// Custom language services need no TypeScript adapter. A checked JS module
|
|
|
|
|
// exporting remote handlers does, and must carry the exact generated source.
|
|
|
|
|
if (
|
|
|
|
|
manifest.implementations.some(
|
|
|
|
|
(implementation) =>
|
|
|
|
|
implementation.compatibility === "quixos-js-module-v1" &&
|
|
|
|
|
implementation.exports.some((id) =>
|
|
|
|
|
pkg.exports.some((entry) => entry.id === id && entry.execution.kind === "remote"),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
const path = "share/quixos/generated/remote.ts";
|
|
|
|
|
const bytes = new TextEncoder().encode(
|
|
|
|
|
generateRemoteTypeScriptBindings(plan.definition.bindingSchema, pkg.revision),
|
|
|
|
|
);
|
|
|
|
|
sources.set(path, bytes);
|
|
|
|
|
generated.push({ target: "server", path, digest: hashBytes(bytes), bytes: bytes.length });
|
|
|
|
|
}
|
2026-09-20 00:25:53 -07:00
|
|
|
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;
|
|
|
|
|
}
|