UNPKG

@endo/compartment-mapper

Version:

The compartment mapper assembles Node applications in a sandbox

357 lines (333 loc) 12.2 kB
/* eslint-disable no-shadow */ /** * Provides {@link digestCompartmentMap} which creates a digest of a compartment * map suitable for archival or further inspection. * * @module */ /** * @import { * DigestedCompartmentDescriptors, * DigestResult, * PackageCompartmentDescriptors, * PackageCompartmentMapDescriptor, * ModuleConfiguration, * Sources, * ExitModuleConfiguration, * FileModuleConfiguration, * ErrorModuleConfiguration, * DigestedCompartmentMapDescriptor, * DigestedCompartmentDescriptor, * DigestCompartmentMapOptions, * PackageCompartmentDescriptor, * PackageCompartmentDescriptorName, * CompartmentModuleConfiguration, * CanonicalName, * CompartmentsRenameFn, FileUrlString, * } from './types.js' */ import { assertDigestedCompartmentMap, stringCompare, } from './compartment-map.js'; import { isErrorModuleSource, isExitModuleSource, isLocalModuleSource, } from './guards.js'; const { create, fromEntries, entries, keys, values } = Object; const { quote: q } = assert; const noop = () => {}; /** * We attempt to produce compartment maps that are consistent regardless of * whether the packages were originally laid out on disk for development or * production, and other trivia like the fully qualified path of a specific * installation. * * Naming compartments for the self-ascribed name and version of each Node.js * package is insufficient because they are not guaranteed to be unique. * Dependencies do not necessarilly come from the npm registry and may be * for example derived from fully qualified URL's or Github org and project * names. * Package managers are also not required to fully deduplicate the hard * copy of each package even when they are identical resources. * Duplication is undesirable, but we elect to defer that problem to solutions * in the package managers, as the alternative would be to consistently hash * the original sources of the packages themselves, which may not even be * available much less pristine for us. * * So, instead, we use the lexically least path of dependency names, delimited * by hashes. * The compartment maps generated by the ./node-modules.js tooling pre-compute * these traces for our use here. * We sort the compartments lexically on their self-ascribed name and version, * and use the lexically least dependency name path as a tie-breaker. * The dependency path is logical and orthogonal to the package manager's * actual installation location, so should be orthogonal to the vagaries of the * package manager's deduplication algorithm. * * @type {CompartmentsRenameFn<FileUrlString, PackageCompartmentDescriptorName>} */ const defaultRenameCompartments = compartments => { /** @type {Record<FileUrlString, PackageCompartmentDescriptorName>} */ const compartmentRenames = create(null); // The sort below combines two comparators to avoid depending on sort // stability, which became standard as recently as 2019. // If that date seems quaint, please accept my regards from the distant past. // We are very proud of you. const comaprtmentNamesToLabel = /** @type {Array<{name: FileUrlString, label: PackageCompartmentDescriptorName}>} */ ( Object.entries(compartments) .map(([name, compartment]) => ({ name, label: compartment.label, })) .sort((a, b) => stringCompare(a.label, b.label)) ); for (const { name, label } of comaprtmentNamesToLabel) { compartmentRenames[name] = label; } return compartmentRenames; }; /** * @template {string} [OldCompartmentName=FileUrlString] * @template {string} [NewCompartmentName=PackageCompartmentDescriptorName] * @overload * @param {PackageCompartmentDescriptors} compartmentDescriptors * @param {Sources} sources * @param {Record<OldCompartmentName, NewCompartmentName>} compartmentRenames * @param {DigestCompartmentMapOptions<OldCompartmentName, NewCompartmentName>} [options] * @returns {DigestedCompartmentDescriptors} */ /** * @overload * @param {PackageCompartmentDescriptors} compartmentDescriptors * @param {Sources} sources * @param {Record<FileUrlString, PackageCompartmentDescriptorName>} compartmentRenames * @param {DigestCompartmentMapOptions} [options] * @returns {DigestedCompartmentDescriptors} */ /** * @template {string} [OldCompartmentName=FileUrlString] * @template {string} [NewCompartmentName=PackageCompartmentDescriptorName] * @param {PackageCompartmentDescriptors} compartmentDescriptors * @param {Sources} sources * @param {Record<OldCompartmentName, NewCompartmentName>} compartmentRenames * @param {DigestCompartmentMapOptions<OldCompartmentName, NewCompartmentName>} [options] */ const translateCompartmentMap = ( compartmentDescriptors, sources, compartmentRenames, { packageConnectionsHook, log = noop } = {}, ) => { const result = create(null); for (const compartmentName of keys(compartmentRenames)) { /** @type {PackageCompartmentDescriptor} */ const compartmentDescriptor = compartmentDescriptors[compartmentName]; const { name, label, retained: compartmentRetained, policy, } = compartmentDescriptor; /** @type {Record<string, PackageCompartmentDescriptorName>} */ const renamedModuleConfigurations = {}; if (compartmentRetained) { // rename module compartments /** @type {Record<string, ModuleConfiguration>} */ const modules = create(null); const compartmentModules = compartmentDescriptor.modules; if (compartmentDescriptor.modules) { for (const name of keys(compartmentModules).sort()) { const { retained: moduleRetained, ...retainedModule } = compartmentModules[name]; if (moduleRetained) { if (retainedModule.compartment !== undefined) { /** @type {CompartmentModuleConfiguration} */ modules[name] = { ...retainedModule, compartment: compartmentRenames[retainedModule.compartment], }; renamedModuleConfigurations[name] = retainedModule.compartment; } else { modules[name] = retainedModule; } } } } // integrate sources into modules const compartmentSources = sources[compartmentName]; if (compartmentSources) { for (const name of keys(compartmentSources).sort()) { const source = compartmentSources[name]; if (isLocalModuleSource(source)) { const { location, parser, sha512 } = source; /** @type {FileModuleConfiguration} */ modules[name] = { location, parser, ...(sha512 !== undefined && { sha512 }), __createdBy: 'digest', }; } else if (isExitModuleSource(source)) { const { exit } = source; /** @type {ExitModuleConfiguration} */ modules[name] = { exit, __createdBy: 'digest', }; } else if (isErrorModuleSource(source)) { const { deferredError } = source; /** @type {ErrorModuleConfiguration} */ modules[name] = { deferredError, __createdBy: 'digest', }; } else { throw new TypeError( `Unexpected source type for compartment ${compartmentName} module ${name}: ${q( source, )}`, ); } } } /** @type {DigestedCompartmentDescriptor} */ result[compartmentRenames[compartmentName]] = { name, label, location: compartmentRenames[compartmentName], modules, policy, // `scopes`, `types`, and `parsers` are not necessary since every // loadable module is captured in `modules`. }; const links = /** @type {Set<CanonicalName>} */ ( values(modules).reduce((acc, moduleDescriptorConfig) => { if ('compartment' in moduleDescriptorConfig) { acc.add(moduleDescriptorConfig.compartment); } return acc; }, new Set()) ); if (packageConnectionsHook) { packageConnectionsHook({ canonicalName: label, connections: links, log, }); } } } return result; }; /** * @param {Sources} sources * @param {Record<string, string>} compartmentRenames * @returns {Sources} */ const renameSources = (sources, compartmentRenames) => { return fromEntries( entries(sources).map(([name, compartmentSources]) => [ compartmentRenames[name], compartmentSources, ]), ); }; /** * @template {string} [OldCompartmentName=FileUrlString] * @template {string} [NewCompartmentName=PackageCompartmentDescriptorName] * @overload * @param {PackageCompartmentMapDescriptor} compartmentMap * @param {Sources} sources * @param {DigestCompartmentMapOptions<OldCompartmentName, NewCompartmentName>} [options] * @returns {DigestResult<OldCompartmentName, NewCompartmentName>} */ /** * @overload * @param {PackageCompartmentMapDescriptor} compartmentMap * @param {Sources} sources * @param {DigestCompartmentMapOptions<FileUrlString, PackageCompartmentDescriptorName>} [options] * @returns {DigestResult<FileUrlString, PackageCompartmentDescriptorName>} */ /** * @template {string} [OldCompartmentName=FileUrlString] * @template {string} [NewCompartmentName=PackageCompartmentDescriptorName] * @param {PackageCompartmentMapDescriptor} compartmentMap * @param {Sources} sources * @param {DigestCompartmentMapOptions<OldCompartmentName, NewCompartmentName>} [options] */ export const digestCompartmentMap = ( compartmentMap, sources, { packageConnectionsHook, log = noop, renameCompartments } = {}, ) => { const { compartments, entry: { compartment: entryCompartmentName, module: entryModuleSpecifier }, } = compartmentMap; const renameCompartmentsFn = renameCompartments ?? defaultRenameCompartments; const oldToNewCompartmentNames = renameCompartmentsFn(compartments); const digestCompartments = translateCompartmentMap( compartments, sources, oldToNewCompartmentNames, { packageConnectionsHook, log }, ); const digestEntryCompartmentName = oldToNewCompartmentNames[entryCompartmentName]; const digestSources = renameSources(sources, oldToNewCompartmentNames); /** @type {DigestedCompartmentMapDescriptor} */ const digestCompartmentMap = { // TODO graceful migration from tags to conditions // https://github.com/endojs/endo/issues/2388 tags: [], entry: { compartment: digestEntryCompartmentName, module: entryModuleSpecifier, }, compartments: digestCompartments, }; // Cross-check: // We assert that we have constructed a valid compartment map, not because it // might not be, but to ensure that the assertCompartmentMap function can // accept all valid compartment maps. try { assertDigestedCompartmentMap(digestCompartmentMap); } catch (err) { const error = /** @type {Error} */ (err); throw new TypeError( `Invalid compartment map; ${JSON.stringify( digestCompartmentMap, )}:\n${error.message}`, { cause: error }, ); } /** @type {Record<NewCompartmentName, OldCompartmentName>} */ const newToOldCompartmentNames = fromEntries( entries(oldToNewCompartmentNames).map(([oldName, newName]) => [ newName, oldName, ]), ); if (renameCompartments === defaultRenameCompartments) { /** @type {DigestResult} */ return { compartmentMap: digestCompartmentMap, sources: digestSources, oldToNewCompartmentNames, newToOldCompartmentNames, compartmentRenames: newToOldCompartmentNames, }; } /** @type {DigestResult<OldCompartmentName, NewCompartmentName>} */ return { compartmentMap: digestCompartmentMap, sources: digestSources, oldToNewCompartmentNames, newToOldCompartmentNames, compartmentRenames: newToOldCompartmentNames, }; };