File size: 16,573 Bytes
51c026d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
import { createHash } from "node:crypto";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { createRequire, isBuiltin } from "node:module";
import { tmpdir } from "node:os";
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { compileFunction } from "node:vm";
import type { Facet, FacetLoader, LoadedFacets } from "../types.ts";
import {
	FACET_BUNDLE_ARTIFACT_FORMAT,
	FACET_BUNDLE_ARTIFACT_FORMAT_VERSION,
	FACET_BUNDLE_FORMAT,
	FACET_BUNDLE_FORMAT_VERSION,
	FACET_BUNDLE_MANIFEST_FILE,
	type FacetBundleArtifact,
	type FacetBundleEntry,
	type FacetBundleManifest,
} from "./manifest.ts";

export type FacetBundleExternalResolver = (specifier: string) => string | URL | undefined;

export interface FacetBundleLoaderOptions {
	readonly manifestPath: string | URL;
	readonly entry: string;
	/** Verify the entry's SHA-256 integrity before evaluating it. Defaults to true. */
	readonly verifyIntegrity?: boolean;
	/** Resolve host-provided external imports when the bundle is outside the host's package tree. */
	readonly resolveExternal?: FacetBundleExternalResolver;
}

export interface FacetBundleArtifactLoaderOptions {
	readonly artifact: unknown;
	/** Resolve host-provided external imports against the receiving application. */
	readonly resolveExternal?: FacetBundleExternalResolver;
	/** Parent directory for materialized module generations. Defaults to the operating system temp directory. */
	readonly temporaryDirectory?: string;
}

interface CommonJsModule {
	exports: unknown;
}

/** Read and validate a versioned facet bundle manifest. */
export async function readFacetBundleManifest(path: string | URL): Promise<FacetBundleManifest> {
	const manifestPath = toFilePath(path);
	let parsed: unknown;
	try {
		parsed = JSON.parse(await readFile(manifestPath, "utf8"));
	} catch (error) {
		throw new Error(`Could not read facet bundle manifest ${manifestPath}`, { cause: error });
	}
	return validateManifest(parsed, manifestPath);
}

/** Read and verify one transportable entry from a facet bundle on disk. */
export async function readFacetBundleArtifact(options: {
	readonly manifestPath: string | URL;
	readonly entry: string;
}): Promise<FacetBundleArtifact> {
	if (options.entry.length === 0) throw new TypeError("Facet bundle entry name must not be empty");
	const manifestPath = toFilePath(options.manifestPath);
	const manifest = await readFacetBundleManifest(manifestPath);
	const entry = manifest.entries[options.entry];
	if (entry === undefined) throw new Error(`Facet bundle ${manifest.plugin.id} has no entry named ${options.entry}`);
	const modulePath = resolveBundleFile(manifestPath, entry.file, "entry");
	const source = await readFile(modulePath, "utf8");
	verifySource(source, entry);
	const sourceMapContents =
		entry.sourceMap === undefined
			? undefined
			: await readFile(resolveBundleFile(manifestPath, entry.sourceMap, "source map"), "utf8");
	return Object.freeze({
		format: FACET_BUNDLE_ARTIFACT_FORMAT,
		formatVersion: FACET_BUNDLE_ARTIFACT_FORMAT_VERSION,
		plugin: manifest.plugin,
		entryName: options.entry,
		entry,
		source,
		...(sourceMapContents === undefined ? {} : { sourceMapContents }),
	});
}

/** Materialize a transported artifact and create a fresh VM-compiled CommonJS generation for each load. */
export function createFacetBundleArtifactLoader(options: FacetBundleArtifactLoaderOptions): FacetLoader {
	const artifact = validateArtifact(options.artifact);
	const temporaryParent = resolve(options.temporaryDirectory ?? tmpdir());
	return {
		async load(): Promise<LoadedFacets> {
			await mkdir(temporaryParent, { recursive: true });
			const directory = await mkdtemp(join(temporaryParent, "chord-facet-"));
			try {
				await materializeArtifact(directory, artifact);
				const loaded = await createFacetBundleLoader({
					manifestPath: join(directory, FACET_BUNDLE_MANIFEST_FILE),
					entry: artifact.entryName,
					resolveExternal: options.resolveExternal,
				}).load();
				let disposed = false;
				return {
					get facets() {
						return loaded.facets;
					},
					async dispose() {
						if (disposed) return;
						disposed = true;
						const errors: unknown[] = [];
						try {
							await loaded.dispose();
						} catch (error) {
							errors.push(error);
						}
						try {
							await rm(directory, { force: true, recursive: true });
						} catch (error) {
							errors.push(error);
						}
						if (errors.length === 1) throw errors[0];
						if (errors.length > 1) throw new AggregateError(errors, "Failed to dispose facet bundle artifact");
					},
				};
			} catch (error) {
				try {
					await rm(directory, { force: true, recursive: true });
				} catch (cleanupError) {
					throw new AggregateError([error, cleanupError], "Facet bundle artifact loading and cleanup failed");
				}
				throw error;
			}
		},
	};
}

/** Create a reusable loader for one opaque entry in a facet bundle manifest. */
export function createFacetBundleLoader(options: FacetBundleLoaderOptions): FacetLoader {
	if (options.entry.length === 0) throw new TypeError("Facet bundle entry name must not be empty");
	const manifestPath = toFilePath(options.manifestPath);
	return {
		async load(): Promise<LoadedFacets> {
			const manifest = await readFacetBundleManifest(manifestPath);
			const entry = manifest.entries[options.entry];
			if (entry === undefined) {
				throw new Error(`Facet bundle ${manifest.plugin.id} has no entry named ${options.entry}`);
			}
			const modulePath = resolveBundleFile(manifestPath, entry.file, "entry");
			try {
				const source = await readFile(modulePath, "utf8");
				if (options.verifyIntegrity !== false) verifySource(source, entry);
				const exported = executeCommonJsModule(source, modulePath, entry.externalImports, options.resolveExternal);
				let facets = facetsFromModule(exported, manifest.plugin.id, options.entry);
				let disposed = false;
				return {
					get facets() {
						return facets;
					},
					async dispose() {
						if (disposed) return;
						disposed = true;
						facets = Object.freeze([]);
					},
				};
			} catch (error) {
				const detail = error instanceof Error ? `: ${error.message}` : "";
				throw new Error(`Could not load facet bundle entry ${manifest.plugin.id}/${options.entry}${detail}`, {
					cause: error,
				});
			}
		},
	};
}

function executeCommonJsModule(
	source: string,
	modulePath: string,
	externalImports: readonly string[],
	resolver?: FacetBundleExternalResolver,
): unknown {
	const requireFromModule = createRequire(modulePath);
	const externalTargets = new Map<string, string>();
	for (const specifier of externalImports) {
		if (!isBuiltin(specifier)) validatePackageSpecifier(specifier);
		externalTargets.set(specifier, toRequireSpecifier(resolveExternalTarget(specifier, resolver)));
	}
	const targetFor = (specifier: string): string => {
		const target = externalTargets.get(specifier);
		if (target === undefined) throw new Error(`Facet bundle required undeclared external import: ${specifier}`);
		return target;
	};
	const requireExternal = Object.assign((specifier: string): unknown => requireFromModule(targetFor(specifier)), {
		resolve: (specifier: string): string => requireFromModule.resolve(targetFor(specifier)),
	});
	const commonJsModule: CommonJsModule = { exports: Object.create(null) };
	const compiled = compileFunction(source, ["exports", "require", "module", "__filename", "__dirname"], {
		filename: modulePath,
	});
	Reflect.apply(compiled, commonJsModule.exports, [
		commonJsModule.exports,
		requireExternal,
		commonJsModule,
		modulePath,
		dirname(modulePath),
	]);
	return commonJsModule.exports;
}

function toRequireSpecifier(target: string): string {
	if (isAbsolute(target) || isBuiltin(target)) return target;
	let url: URL;
	try {
		url = new URL(target);
	} catch {
		return target;
	}
	if (url.protocol === "file:") return fileURLToPath(url);
	if (url.protocol === "node:") return url.href;
	throw new Error(`Facet bundle external target must be a file, package, or built-in module: ${target}`);
}

function toFilePath(path: string | URL): string {
	if (typeof path === "string") return resolve(path);
	if (path.protocol !== "file:") throw new TypeError(`Facet bundle manifest must be a file URL, not ${path.protocol}`);
	return fileURLToPath(path);
}

function resolveBundleFile(manifestPath: string, file: string, label: string): string {
	if (file.length === 0 || isAbsolute(file) || basename(file) !== file || file === "." || file === "..") {
		throw new Error(`Facet bundle ${label} must be a filename relative to its manifest`);
	}
	return resolve(dirname(manifestPath), file);
}

function verifySource(source: string, entry: FacetBundleEntry): void {
	const expected = parseIntegrity(entry.integrity);
	const actual = createHash("sha256").update(source, "utf8").digest("base64");
	if (actual !== expected) throw new Error(`Facet bundle integrity check failed for ${entry.file}`);
}

function parseIntegrity(integrity: string): string {
	const prefix = "sha256-";
	if (!integrity.startsWith(prefix) || integrity.length === prefix.length) {
		throw new Error("Facet bundle entry has an invalid SHA-256 integrity value");
	}
	return integrity.slice(prefix.length);
}

function facetsFromModule(imported: unknown, pluginId: string, entryName: string): readonly Facet[] {
	if (!isRecord(imported)) throw new Error(`Facet bundle entry ${pluginId}/${entryName} did not export a module`);
	const exported = imported.default;
	const candidates: readonly unknown[] = Array.isArray(exported) ? (exported as readonly unknown[]) : [exported];
	if (candidates.length === 0) {
		throw new Error(`Facet bundle entry ${pluginId}/${entryName} exported no facets`);
	}
	const facets: Facet[] = [];
	for (const candidate of candidates) {
		if (!isRecord(candidate) || typeof candidate.id !== "string" || candidate.id.length === 0) {
			throw new Error(`Facet bundle entry ${pluginId}/${entryName} has a facet with an invalid ID`);
		}
		if (typeof candidate.setup !== "function") {
			throw new Error(`Facet bundle entry ${pluginId}/${entryName} facet ${candidate.id} has no setup function`);
		}
		facets.push(candidate as unknown as Facet);
	}
	const ids = facets.map(({ id }) => id);
	if (new Set(ids).size !== ids.length) {
		throw new Error(`Facet bundle entry ${pluginId}/${entryName} exports duplicate facet IDs`);
	}
	return Object.freeze(facets);
}

function validateArtifact(value: unknown): FacetBundleArtifact {
	if (
		!isRecord(value) ||
		value.format !== FACET_BUNDLE_ARTIFACT_FORMAT ||
		value.formatVersion !== FACET_BUNDLE_ARTIFACT_FORMAT_VERSION ||
		typeof value.entryName !== "string" ||
		value.entryName.length === 0 ||
		typeof value.source !== "string"
	) {
		throw new Error("Invalid facet bundle artifact");
	}
	const manifest = validateManifest(
		{
			format: FACET_BUNDLE_FORMAT,
			formatVersion: FACET_BUNDLE_FORMAT_VERSION,
			plugin: value.plugin,
			entries: { [value.entryName]: value.entry },
		},
		"facet bundle artifact",
	);
	const entry = manifest.entries[value.entryName]!;
	if (entry.sourceMap === undefined) {
		if (value.sourceMapContents !== undefined) {
			throw new Error("Facet bundle artifact has source map contents without a source map");
		}
	} else if (typeof value.sourceMapContents !== "string") {
		throw new Error("Facet bundle artifact is missing its source map contents");
	}
	verifySource(value.source, entry);
	return Object.freeze({
		format: FACET_BUNDLE_ARTIFACT_FORMAT,
		formatVersion: FACET_BUNDLE_ARTIFACT_FORMAT_VERSION,
		plugin: manifest.plugin,
		entryName: value.entryName,
		entry,
		source: value.source,
		...(value.sourceMapContents === undefined ? {} : { sourceMapContents: value.sourceMapContents }),
	});
}

async function materializeArtifact(directory: string, artifact: FacetBundleArtifact): Promise<void> {
	const manifest: FacetBundleManifest = {
		format: FACET_BUNDLE_FORMAT,
		formatVersion: FACET_BUNDLE_FORMAT_VERSION,
		plugin: artifact.plugin,
		entries: { [artifact.entryName]: artifact.entry },
	};
	await Promise.all([
		writeFile(join(directory, artifact.entry.file), artifact.source, "utf8"),
		writeFile(join(directory, FACET_BUNDLE_MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`, "utf8"),
		...(artifact.entry.sourceMap === undefined
			? []
			: [writeFile(join(directory, artifact.entry.sourceMap), artifact.sourceMapContents!, "utf8")]),
	]);
}

function resolveExternalTarget(specifier: string, resolver?: FacetBundleExternalResolver): string {
	const resolved = resolver?.(specifier);
	if (resolved !== undefined) return typeof resolved === "string" ? resolved : resolved.href;
	const extension = import.meta.url.endsWith(".ts") ? "ts" : "js";
	if (specifier === "@earendil-works/chord") return new URL(`../index.${extension}`, import.meta.url).href;
	if (specifier === "@earendil-works/chord/context") {
		return new URL(`../context/index.${extension}`, import.meta.url).href;
	}
	if (specifier === "@earendil-works/chord/node") return new URL(`../node.${extension}`, import.meta.url).href;
	return import.meta.resolve(specifier);
}

function validatePackageSpecifier(specifier: string): void {
	const segments = specifier.split("/");
	const packageParts = specifier.startsWith("@") ? 2 : 1;
	if (
		specifier.startsWith(".") ||
		specifier.startsWith("/") ||
		specifier.startsWith("#") ||
		specifier.includes(":") ||
		specifier.includes("\\") ||
		segments.length < packageParts ||
		segments.some((part) => part.length === 0 || part === "." || part === "..")
	) {
		throw new Error(`Facet bundle artifact has an unsupported external import: ${specifier}`);
	}
}

function validateManifest(value: unknown, path: string): FacetBundleManifest {
	if (!isRecord(value) || value.format !== FACET_BUNDLE_FORMAT) {
		throw new Error(`Invalid facet bundle manifest format in ${path}`);
	}
	if (value.formatVersion !== FACET_BUNDLE_FORMAT_VERSION) {
		throw new Error(`Unsupported facet bundle manifest version in ${path}: ${String(value.formatVersion)}`);
	}
	if (!isRecord(value.plugin) || typeof value.plugin.id !== "string" || value.plugin.id.length === 0) {
		throw new Error(`Facet bundle manifest has an invalid plugin identity in ${path}`);
	}
	if (
		value.plugin.version !== undefined &&
		(typeof value.plugin.version !== "string" || value.plugin.version.length === 0)
	) {
		throw new Error(`Facet bundle manifest has an invalid plugin version in ${path}`);
	}
	if (!isRecord(value.entries) || Object.keys(value.entries).length === 0) {
		throw new Error(`Facet bundle manifest has no entries in ${path}`);
	}
	const entries: Record<string, FacetBundleEntry> = {};
	for (const [name, candidate] of Object.entries(value.entries)) {
		if (name.length === 0 || !isRecord(candidate)) {
			throw new Error(`Facet bundle manifest has an invalid entry in ${path}`);
		}
		if (typeof candidate.file !== "string") throw new Error(`Facet bundle entry ${name} has no file`);
		resolveBundleFile(path, candidate.file, `entry ${name}`);
		if (typeof candidate.integrity !== "string") throw new Error(`Facet bundle entry ${name} has no integrity`);
		parseIntegrity(candidate.integrity);
		if (
			!Array.isArray(candidate.externalImports) ||
			candidate.externalImports.some((item) => typeof item !== "string")
		) {
			throw new Error(`Facet bundle entry ${name} has invalid external imports`);
		}
		const externalImports = Object.freeze([...(candidate.externalImports as readonly string[])]);
		if (new Set(externalImports).size !== externalImports.length) {
			throw new Error(`Facet bundle entry ${name} has duplicate external imports`);
		}
		if (candidate.sourceMap !== undefined) {
			if (typeof candidate.sourceMap !== "string")
				throw new Error(`Facet bundle entry ${name} has an invalid source map`);
			resolveBundleFile(path, candidate.sourceMap, `entry ${name} source map`);
		}
		entries[name] = Object.freeze({
			file: candidate.file,
			integrity: candidate.integrity,
			externalImports,
			...(candidate.sourceMap === undefined ? {} : { sourceMap: candidate.sourceMap }),
		});
	}
	return Object.freeze({
		format: FACET_BUNDLE_FORMAT,
		formatVersion: FACET_BUNDLE_FORMAT_VERSION,
		plugin: Object.freeze({
			id: value.plugin.id,
			...(value.plugin.version === undefined ? {} : { version: value.plugin.version }),
		}),
		entries: Object.freeze(entries),
	});
}

function isRecord(value: unknown): value is Record<string, unknown> {
	return typeof value === "object" && value !== null && !Array.isArray(value);
}