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";
|
2026-09-30 22:03:17 -07:00
|
|
|
import { type ArtifactReader, type PackageArtifacts, type ArtifactGroup } from "./artifacts.js";
|
2026-09-20 00:25:53 -07:00
|
|
|
import { loadArtifactWorld, type LoadedArtifactWorld } from "./files.js";
|
2026-09-30 22:03:17 -07:00
|
|
|
import { componentAssetDeclarations } from "./client-contracts.js";
|
2026-09-20 00:25:53 -07:00
|
|
|
|
2026-09-30 22:03:17 -07:00
|
|
|
import { checkPackageBuildContract, packageBuildContract, type PackageBuildContract } from "./build-contract.js";
|
|
|
|
|
|
|
|
|
|
export interface GroupReceipt {
|
2026-09-20 00:25:53 -07:00
|
|
|
schemaVersion: 1;
|
|
|
|
|
packageRevisionId: string;
|
|
|
|
|
bindingDigest: string;
|
|
|
|
|
manifestDigest: string;
|
|
|
|
|
generatorDigest: string;
|
|
|
|
|
generated: { target: string; path: string; digest: string; bytes: number }[];
|
|
|
|
|
}
|
2026-09-30 22:03:17 -07:00
|
|
|
export interface ArtifactReceipt extends GroupReceipt {
|
|
|
|
|
sourceDigest: string;
|
|
|
|
|
}
|
2026-09-20 00:25:53 -07:00
|
|
|
const hashBytes = (bytes: Uint8Array) => createHash("sha256").update(bytes).digest("hex");
|
2026-09-28 20:29:21 -07:00
|
|
|
/** Nominal identities never become filesystem paths. */
|
|
|
|
|
export const generatedBindingPath = (target: string) => `share/quixos/generated/target-${hexDigest(target)}.ts`;
|
2026-09-20 00:25:53 -07:00
|
|
|
/** 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);
|
2026-09-30 22:03:17 -07:00
|
|
|
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)!;
|
2026-09-20 00:25:53 -07:00
|
|
|
if (
|
2026-09-30 22:03:17 -07:00
|
|
|
manifest.packageRevisionId !== pkg.revision ||
|
|
|
|
|
manifest.bindingDigest !== contract.digest ||
|
|
|
|
|
hexDigest(manifest.versions) !== hexDigest(contract.definition.versions)
|
2026-09-20 00:25:53 -07:00
|
|
|
)
|
2026-09-30 22:03:17 -07:00
|
|
|
throw new ExecutionPlanError("INVALID_RECEIPT", manifest.packageRevisionId, "Receipt package/contract 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(
|
2026-09-22 13:51:32 -07:00
|
|
|
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),
|
2026-09-21 13:30:33 -07:00
|
|
|
);
|
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) => {
|
2026-09-28 20:29:21 -07:00
|
|
|
const path = generatedBindingPath(target);
|
2026-09-20 00:25:53 -07:00
|
|
|
const bytes = new TextEncoder().encode(
|
2026-09-30 22:03:17 -07:00
|
|
|
generatePortableTypeScriptBindings(contract.definition.bindings, pkg.revision, { target }),
|
2026-09-20 00:25:53 -07:00
|
|
|
);
|
|
|
|
|
sources.set(path, bytes);
|
|
|
|
|
return { target, path, digest: hashBytes(bytes), bytes: bytes.length };
|
|
|
|
|
});
|
2026-09-21 15:24:05 -07:00
|
|
|
if (manifest.components?.length) {
|
|
|
|
|
for (const [path, source] of [
|
2026-09-30 22:03:17 -07:00
|
|
|
["share/quixos/generated/client.ts", contract.definition.component!],
|
2026-09-21 15:24:05 -07:00
|
|
|
["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 });
|
|
|
|
|
}
|
|
|
|
|
}
|
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(
|
2026-09-30 22:03:17 -07:00
|
|
|
generateRemoteTypeScriptBindings(contract.definition.bindings, pkg.revision),
|
2026-09-20 20:58:01 -07:00
|
|
|
);
|
|
|
|
|
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,
|
2026-09-30 22:03:17 -07:00
|
|
|
bindingDigest: contract.digest,
|
2026-09-20 00:25:53 -07:00
|
|
|
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");
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-21 21:49:52 -07:00
|
|
|
/** Checked Nix builds validate generated sources with their selected compiler.
|
|
|
|
|
* Runtime loading uses the stored outputs instead of repeating compilation. */
|
2026-09-20 00:25:53 -07:00
|
|
|
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;
|
|
|
|
|
}
|