import { createHash } from "node:crypto"; import { generatePortableTypeScriptBindings, generateRemoteTypeScriptBindings } from "../bindings/portable.js"; import { type ExecutionPlan, ExecutionPlanError, hexDigest } from "./plan.js"; import { type ArtifactReader, type PackageArtifacts, type ArtifactGroup } from "./artifacts.js"; import { loadArtifactWorld, type LoadedArtifactWorld } from "./files.js"; import { componentAssetDeclarations } from "./client-contracts.js"; import { checkPackageBuildContract, packageBuildContract, type PackageBuildContract } from "./build-contract.js"; export interface GroupReceipt { schemaVersion: 1; packageRevisionId: string; bindingDigest: string; manifestDigest: string; generatorDigest: string; generated: { target: string; path: string; digest: string; bytes: number }[]; } export interface ArtifactReceipt extends GroupReceipt { sourceDigest: string; } const hashBytes = (bytes: Uint8Array) => createHash("sha256").update(bytes).digest("hex"); /** Nominal identities never become filesystem paths. */ export const generatedBindingPath = (target: string) => `share/quixos/generated/target-${hexDigest(target)}.ts`; /** 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 } { const pkg = plan.definition.packages.find((p) => p.revision === manifest.packageRevisionId); if (!pkg || manifest.sourceDigest !== pkg.sourceDigest) throw new ExecutionPlanError("INVALID_RECEIPT", manifest.packageRevisionId, "Receipt package/source mismatch"); return expectedPackageReceipt( packageBuildContract(plan.definition.bindingSchema, pkg.revision), manifest, pkg.sourceDigest, generatorDigest, ); } export function expectedPackageReceipt( contract: PackageBuildContract, manifest: PackageArtifacts, sourceDigest: string, generatorDigest: string, ): { receipt: ArtifactReceipt; sources: Map } { if (manifest.sourceDigest !== sourceDigest) throw new ExecutionPlanError("INVALID_RECEIPT", manifest.packageRevisionId, "Receipt source mismatch"); const result = expectedGroupReceipt(contract, manifest, generatorDigest); return { ...result, receipt: { ...result.receipt, sourceDigest } }; } export function expectedGroupReceipt( contract: PackageBuildContract, manifest: ArtifactGroup, generatorDigest: string, ): { receipt: GroupReceipt; sources: Map } { checkPackageBuildContract(contract); if (!/^[0-9a-f]{64}$/.test(generatorDigest)) throw new Error("Invalid trusted generator digest"); const pkg = contract.definition.package; const schemaPackage = contract.definition.bindings.packages.find((p) => p.revisionId === pkg.revision)!; if ( manifest.packageRevisionId !== pkg.revision || manifest.bindingDigest !== contract.digest || hexDigest(manifest.versions) !== hexDigest(contract.definition.versions) ) throw new ExecutionPlanError("INVALID_RECEIPT", manifest.packageRevisionId, "Receipt package/contract mismatch"); // 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" && i.exports.some((id) => pkg.exports.some((entry) => entry.id === id && entry.execution.kind !== "remote")), ) .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(); const generated = targets.map((target) => { const path = generatedBindingPath(target); const bytes = new TextEncoder().encode( generatePortableTypeScriptBindings(contract.definition.bindings, pkg.revision, { target }), ); sources.set(path, bytes); return { target, path, digest: hashBytes(bytes), bytes: bytes.length }; }); if (manifest.components?.length) { for (const [path, source] of [ ["share/quixos/generated/client.ts", contract.definition.component!], ["share/quixos/generated/component-assets.d.ts", componentAssetDeclarations()], ] as const) { const bytes = new TextEncoder().encode(source); sources.set(path, bytes); generated.push({ target: "window", path, digest: hashBytes(bytes), bytes: bytes.length }); } } // 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(contract.definition.bindings, pkg.revision), ); sources.set(path, bytes); generated.push({ target: "server", path, digest: hashBytes(bytes), bytes: bytes.length }); } return { receipt: { schemaVersion: 1, packageRevisionId: pkg.revision, bindingDigest: contract.digest, manifestDigest: hexDigest(manifest), generatorDigest, generated, }, sources, }; } export async function checkArtifactReceipt( plan: ExecutionPlan, manifest: PackageArtifacts, receipt: unknown, generatorDigest: string, read: ArtifactReader, ): Promise { 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"); } } /** Checked Nix builds validate generated sources with their selected compiler. * Runtime loading uses the stored outputs instead of repeating compilation. */ export async function loadCheckedArtifactWorld( plan: ExecutionPlan, roots: Readonly>, generatorDigest: string, ): Promise { 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; }