115 lines
4.8 KiB
TypeScript
115 lines
4.8 KiB
TypeScript
import path from "node:path";
|
|||
|
|
import { loadCheckedArtifactWorld } from "./receipts.js";
|
||
|
|
import { contentArtifactUrl } from "./files.js";
|
||
|
|
import { selectArtifactImplementation, type ArtifactImplementation } from "./artifacts.js";
|
||
|
|
import { ExecutionPlanError, type ExecutionPlan } from "./plan.js";
|
||
|
|
|
||
|
|
export interface PackageInstallationSelection {
|
||
|
|
readonly browser?: { readonly id?: string };
|
||
|
|
readonly service?: { readonly id?: string };
|
||
|
|
/** Including a module is optional; executing it still requires host trust policy. */
|
||
|
|
readonly module?: { readonly id?: string };
|
||
|
|
}
|
||
|
|
export interface ExecutionInstallation {
|
||
|
|
readonly codec: "camino-execution-installation-v1";
|
||
|
|
readonly plan: string;
|
||
|
|
readonly world: string;
|
||
|
|
readonly packages: readonly {
|
||
|
|
readonly revision: string;
|
||
|
|
readonly root: string;
|
||
|
|
readonly service: ArtifactImplementation;
|
||
|
|
readonly module?: ArtifactImplementation;
|
||
|
|
readonly browserURL?: string;
|
||
|
|
readonly browserRegistry?: string;
|
||
|
|
}[];
|
||
|
|
readonly routes: Readonly<Record<string, { readonly path: string; readonly mediaType: string }>>;
|
||
|
|
readonly coreURL?: string;
|
||
|
|
readonly engineURL?: string;
|
||
|
|
}
|
||
|
|
/** Verify candidate receipts before assembling process entrypoints and browser assets.
|
||
|
|
* This grants no authority to activate a world or run optional modules in process. */
|
||
|
|
export async function loadExecutionInstallation(
|
||
|
|
plan: ExecutionPlan,
|
||
|
|
roots: Readonly<Record<string, string>>,
|
||
|
|
generator: string,
|
||
|
|
selections: Readonly<Record<string, PackageInstallationSelection>> = {},
|
||
|
|
): Promise<ExecutionInstallation> {
|
||
|
|
const loaded = await loadCheckedArtifactWorld(plan, roots, generator);
|
||
|
|
const fail = (where: string, message: string): never => {
|
||
|
|
throw new ExecutionPlanError("INSTALLATION_INVALID", where, message);
|
||
|
|
};
|
||
|
|
for (const revision of Object.keys(selections))
|
||
|
|
if (!loaded.manifests.some((manifest) => manifest.packageRevisionId === revision))
|
||
|
|
fail(revision, "Selection names a package outside this checked world");
|
||
|
|
const routes: Record<string, { path: string; mediaType: string }> = Object.create(null);
|
||
|
|
const packages: ExecutionInstallation["packages"][number][] = [];
|
||
|
|
let coreURL: string | undefined, engineURL: string | undefined;
|
||
|
|
for (const manifest of loaded.manifests) {
|
||
|
|
const revision = manifest.packageRevisionId;
|
||
|
|
const root = roots[revision]!;
|
||
|
|
const selection = selections[revision] ?? {};
|
||
|
|
const exports = plan.definition.packages.find((pkg) => pkg.revision === revision)!.exports;
|
||
|
|
const service = selectArtifactImplementation(manifest, {
|
||
|
|
...selection.service,
|
||
|
|
target: "server",
|
||
|
|
compatibility: "quixos-service-v1",
|
||
|
|
exports: exports.map((entry) => entry.id),
|
||
|
|
});
|
||
|
|
const module = selection.module
|
||
|
|
? selectArtifactImplementation(manifest, {
|
||
|
|
...selection.module,
|
||
|
|
target: "server",
|
||
|
|
compatibility: "quixos-js-module-v1",
|
||
|
|
exports: exports.map((entry) => entry.id),
|
||
|
|
})
|
||
|
|
: undefined;
|
||
|
|
const browserExports = exports.filter(
|
||
|
|
(entry) => entry.execution.kind !== "remote" && entry.execution.targets.includes("browser"),
|
||
|
|
);
|
||
|
|
if (!browserExports.length) {
|
||
|
|
if (selection.browser) fail(revision, "Package has no browser exports to select");
|
||
|
|
packages.push({ revision, root, service, ...(module ? { module } : {}) });
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const browser = selectArtifactImplementation(manifest, {
|
||
|
|
...selection.browser,
|
||
|
|
target: "browser",
|
||
|
|
compatibility: "quixos-js-module-v1",
|
||
|
|
exports: browserExports.map((entry) => entry.id),
|
||
|
|
});
|
||
|
|
const entry = browser.files.find((file) => file.path === browser.path)!;
|
||
|
|
const shared = (name: string) => {
|
||
|
|
const files = browser.files.filter((file) => file.path.endsWith("/" + name));
|
||
|
|
if (files.length !== 1) fail(revision, "Expected one shared " + name);
|
||
|
|
return contentArtifactUrl(files[0]!);
|
||
|
|
};
|
||
|
|
const core = shared("core.mjs"),
|
||
|
|
engine = shared("engine.mjs");
|
||
|
|
if ((coreURL && coreURL !== core) || (engineURL && engineURL !== engine))
|
||
|
|
fail(revision, "Browser packages disagree on the shared runtime identity");
|
||
|
|
coreURL = core;
|
||
|
|
engineURL = engine;
|
||
|
|
for (const file of browser.files) {
|
||
|
|
const url = contentArtifactUrl(file);
|
||
|
|
if (routes[url] && routes[url].mediaType !== file.mediaType) fail(url, "Conflicting artifact media types");
|
||
|
|
routes[url] ??= { path: path.join(root, file.path), mediaType: file.mediaType };
|
||
|
|
}
|
||
|
|
packages.push({
|
||
|
|
revision,
|
||
|
|
root,
|
||
|
|
service,
|
||
|
|
...(module ? { module } : {}),
|
||
|
|
browserURL: contentArtifactUrl(entry),
|
||
|
|
browserRegistry: browser.registryExport,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
codec: "camino-execution-installation-v1",
|
||
|
|
plan: plan.digest,
|
||
|
|
world: loaded.digest,
|
||
|
|
packages,
|
||
|
|
routes,
|
||
|
|
...(coreURL ? { coreURL, engineURL } : {}),
|
||
|
|
};
|
||
|
|
}
|