Project source-free capability contracts and keep exact source provenance in package assembly. Reuse compiled groups across unrelated graph and dependency implementation edits. Keep React public inputs separate from workspace query lowering and retain explicit type-companion dependencies. Allow independent execution-package checks, scope resource compilation to declared dependencies, and memoize source closure traversal. Document generic-specialization limits and measured Reader rebuild behavior. Verified protocol, artifact, package, React, and managed activation checks; retained-Git independent/workspace artifact equality and incompatible type dependency rejection; nix fmt -- --check and lint. One-off Reader measurements recompiled 1 of 13 groups for a child edit and 0 for a workspace QX default edit.
169 lines
7.3 KiB
TypeScript
169 lines
7.3 KiB
TypeScript
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<string, Uint8Array> } {
|
|
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<string, Uint8Array> } {
|
|
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<string, Uint8Array> } {
|
|
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<string, Uint8Array>();
|
|
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<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");
|
|
}
|
|
}
|
|
/** 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<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;
|
|
}
|