@endo/compartment-mapper
Version:
The compartment mapper assembles Node applications in a sandbox
638 lines • 25.5 kB
TypeScript
/**
* External types of the compartment mapper.
*
* @module
*/
import type { FinalStaticModuleType, ModuleDescriptor, StaticModuleType, Transform } from 'ses';
import type { ATTENUATORS_COMPARTMENT, ENTRY_COMPARTMENT } from '../policy-format.js';
import type { CanonicalName } from './canonical-name.js';
import type { CompartmentDescriptor, DigestedCompartmentMapDescriptor, Language, LanguageForExtension, PackageCompartmentDescriptorName, PackageCompartmentDescriptors } from './compartment-map-schema.js';
import type { PackageDescriptor } from './node-modules.js';
import type { SomePolicy } from './policy-schema.js';
import type { HashFn, ReadFn, ReadPowers } from './powers.js';
import type { LiteralUnion } from './typescript.js';
export type { CanonicalName, PackageDescriptor };
/**
* Hook executed for each canonical name mentioned in policy but not found in the
* compartment map
*/
export type UnknownCanonicalNameHook = (params: {
canonicalName: CanonicalName;
path: string[];
message: string;
suggestion?: CanonicalName | undefined;
log: LogFn;
}) => void;
export type { PackageCompartmentDescriptorName };
/**
* Data about a package provided by a {@link PackageDataHook}
*/
export type PackageData = {
name: string;
packageDescriptor: PackageDescriptor;
location: FileUrlString;
canonicalName: PackageCompartmentDescriptorName;
};
/**
* Hook executed with data about all packages found while crawling `node_modules`.
*
* Called once before `translateGraph`.
*/
export type PackageDataHook = (params: {
packageData: Readonly<Map<LiteralUnion<typeof ATTENUATORS_COMPARTMENT | typeof ENTRY_COMPARTMENT, FileUrlString>, PackageData>>;
log: LogFn;
}) => void;
/**
* Hook executed for each canonical name (corresponding to a package) in the
* `CompartmentMapDescriptor` with a list of canonical names of its
* dependencies.
*
* Can return partial updates to modify the dependencies set.
*
* Suggested use cases:
* - Adding dependencies based on policy
* - Removing dependencies based on policy
* - Filtering dependencies based on multiple criteria
*/
export type PackageDependenciesHook = (params: {
canonicalName: CanonicalName;
dependencies: Readonly<Set<CanonicalName>>;
log: LogFn;
}) => Partial<{
dependencies: Set<CanonicalName>;
}> | void;
/**
* The `moduleSource` property value for {@link ModuleSourceHook}
*/
export type ModuleSourceHookModuleSource = ModuleSourceHookFileModuleSource | ModuleSourceHookExitModuleSource;
/**
* The `moduleSource` property value for {@link ModuleSourceHook} for a module
* on disk
*/
export type ModuleSourceHookFileModuleSource = {
location: FileUrlString;
language: Language;
bytes: Uint8Array;
imports?: string[] | undefined;
exports?: string[] | undefined;
reexports?: string[] | undefined;
sha512?: string | undefined;
};
/**
* The `moduleSource` property value for {@link ModuleSourceHook} for an exit
* module (virtual)
*/
export type ModuleSourceHookExitModuleSource = {
exit: string;
};
/**
* Hook executed when processing a module source.
*/
export type ModuleSourceHook = (params: {
moduleSource: ModuleSourceHookModuleSource;
canonicalName: CanonicalName;
log: LogFn;
}) => void;
/**
* Hook executed for each canonical name with its connections (linked compartments).
*/
export type PackageConnectionsHook = (params: {
canonicalName: CanonicalName;
connections: Set<CanonicalName>;
log: LogFn;
}) => void;
/**
* Hook executed during preloading when a compartment designated to be preloaded
* is already loaded.
*/
export type RedundantPreloadHook = (params: {
canonicalName: CanonicalName;
entry: string;
log: LogFn;
}) => void;
/**
* Set of options available in the context of code execution.
*
* May be used only as an intersection with other options types.
*/
export type ExecuteOptions = Partial<{
globals: object | undefined;
transforms: Array<Transform> | undefined;
__shimTransforms__: Array<Transform> | undefined;
attenuations: Record<string, object> | undefined;
Compartment: typeof Compartment | undefined;
__native__: boolean | undefined;
}> & ModulesOption & ExitModuleImportHookOption;
export type ParseArchiveOptions = Partial<{
expectedSha512: string | undefined;
computeSha512: HashFn | undefined;
computeSourceLocation: ComputeSourceLocationHook | undefined;
computeSourceMapLocation: ComputeSourceMapLocationHook | undefined;
__native__: boolean | undefined;
}> & ModulesOption & CompartmentOption & ParserForLanguageOption & ExitModuleImportHookOption;
export type LoadArchiveOptions = ParseArchiveOptions;
/**
* Options having an optional `log` property.
*/
export interface LogOptions {
/**
* A logger (for logging)
*/
log?: LogFn | undefined;
}
/**
* Options for `mapNodeModules()`
*/
export type MapNodeModulesOptions = MapNodeModulesOptionsOmitPolicy & PolicyOption & MapNodeModulesHookOptions & LogOptions;
type MapNodeModulesOptionsOmitPolicy = Partial<{
/** @deprecated renamed `conditions` to be consistent with Node.js */
tags: Set<string> | undefined;
/**
* Conditions for package `"imports"` and `"exports"`.
*
* Common conditions include `"node"`, `"browser"`, `"require"`, `"import"`,
* and `"default"`. The conditions `"import"`, `"default"`, and `"endo"` need
* not be specified.
*
* _If using the `"development"` condition_ and you just need to map
* `devDependencies`, use the {@link MapNodeModulesOptions.dev dev} flag
* instead.
*/
conditions: Set<string> | undefined;
/**
* If `true`, include packages from `devDependencies` in the resulting {@link CompartmentMapDescriptor}.
*
* Historically this is synonymous with the `"development"`
* {@link MapNodeModulesOptions.conditions condition}, but this behavior may
* be deprecated in a future version.
*/
dev: boolean | undefined;
/**
* Indicates that the node_modules tree should fail to map if it does not
* strictly reach every expected package.
* By default, unreachable packages are simply omitted from the map,
* which defers some errors to when modules load.
*/
strict: boolean | undefined;
/** Dependencies to make reachable from any package */
commonDependencies: Record<string, string> | undefined;
/** Maps extensions to languages for all packages, like `txt` to `text` */
languageForExtension: LanguageForExtension | undefined;
/** Maps additional extensions to languages for all type=module packages */
moduleLanguageForExtension: LanguageForExtension | undefined;
/** Maps additional extensions to languages for all type=commonjs packages (default) */
commonjsLanguageForExtension: LanguageForExtension | undefined;
/** Maps extensions to languages for packages not under node_modules */
workspaceLanguageForExtension: LanguageForExtension | undefined;
/**
* Maps additional extensions to languages for all type=module packages that
* are not under node_modules
*/
workspaceModuleLanguageForExtension: LanguageForExtension | undefined;
/**
* Maps additional extensions to languages for all type=commonjs packages
* (default)
*/
workspaceCommonjsLanguageForExtension: LanguageForExtension | undefined;
/**
* Accounts for languages not present as values in any of the extension to
* language mappings.
* For higher level functions like `importLocation`, these are inferred
* from the `parserForLanguage` option.
*/
languages: Array<Language> | undefined;
/**
* Additional package locations to graph alongside the entry package. Each
* entry is graphed into the same compartment map, with an edge from the entry
* package. If `modules` is specified, those paths are injected into the
* package's external aliases.
*
* These can be thought of as "additional entry points" insofar as building
* the compartment map.
*/
additionalLocations: Array<AdditionalLocation>;
}>;
/**
* An additional package location to include in the compartment map graph.
*
* This allows packages that are not reachable through the entry package's
* dependency graph to appear in the compartment map. A common use case is
* including a project root package when the entry point is a tool binary
* (e.g., webpack) inside `node_modules`.
*/
export interface AdditionalLocation {
/**
* File URL of the package root directory (must contain a `package.json`).
*/
location: FileUrlString;
/**
* Specific relative module paths to expose as external aliases. These paths
* are relative to {@link location}.
*
* If omitted, only modules discovered by standard inference (see
* `infer-exports.js`) are externally visible.
*/
modules?: Array<string>;
}
/**
* Hook options for `mapNodeModules()`
*/
export type MapNodeModulesHookOptions = {
unknownCanonicalNameHook?: UnknownCanonicalNameHook | undefined;
packageDependenciesHook?: PackageDependenciesHook | undefined;
packageDataHook?: PackageDataHook | undefined;
};
export type CompartmentMapForNodeModulesOptions = Omit<MapNodeModulesOptions, 'conditions' | 'tags'>;
/**
* Options for `captureFromMap()`
*/
export type CaptureLiteOptions = ImportingOptions & LinkingOptions & PolicyOption & LogOptions & PreloadOption & CaptureFromMapHookOptions;
/**
* Hook options for `captureFromMap()`
*/
export type CaptureFromMapHookOptions = {
packageConnectionsHook?: PackageConnectionsHook | undefined;
moduleSourceHook?: ModuleSourceHook | undefined;
};
/**
* Options bag containing a `preload` array.
*/
export interface PreloadOption {
/**
* List of compartment names (the keys of
* {@link CompartmentMapDescriptor.compartments}) and entries (`ModuleConfiguration` names) to force-load _after_ the
* entry compartment and any attenuators.
*
* If an array of strings is provided, the entry is assumed to be `.`.
*/
_preload?: Array<string | {
compartment: string;
entry: string;
}>;
/**
* Hook executed during preloading when a compartment designated to be preloaded
* has already been loaded (via entry Compartment).
*/
_redundantPreloadHook?: RedundantPreloadHook | undefined;
}
export type ArchiveLiteOptions = SyncOrAsyncArchiveOptions & ModuleTransformsOption & ImportingOptions & ExitModuleImportHookOption & LinkingOptions & LogOptions;
export type SyncArchiveLiteOptions = SyncOrAsyncArchiveOptions & SyncModuleTransformsOption & SyncImportingOptions & ExitModuleImportNowHookOption;
export type ArchiveOptions = Omit<MapNodeModulesOptions, 'language'> & ArchiveLiteOptions;
export type BundleOptions = ArchiveOptions & {
/**
* Format of the bundle for purposes of importing modules from the surrounding
* environment.
* The default can be CommonJS or ESM but depends on neither `require` nor `import`
* for external modules, but errors early if the entrained modules need to import
* a host module.
* Specifying `cjs` makes `require` available for modules outside the bundle
* (exits to the import graph).
*/
format?: 'cjs' | undefined;
/**
* Evaluates individual module functors in-place so stack traces represent
* original source locations better.
* The resulting script cannot be used on a web page with a no-unsafe-eval
* Content Security Policy.
*/
useEvaluate?: boolean | undefined;
/**
* A prefix for the sourceURL comment in each module format that supports
* sourceURL comments.
* Requires `useEvaluate` for effect.
*/
sourceUrlPrefix?: string | undefined;
};
export type SyncArchiveOptions = Omit<MapNodeModulesOptions, 'languages' | 'unknownCanonicalNameHook' | 'packageDependenciesHook' | 'packageDataHook'> & SyncArchiveLiteOptions;
export type SyncLoadLocationOptions = SyncArchiveOptions & LogOptions & PreloadOption & LoadLocationHookOptions;
/**
* Options for `loadLocation()`
*/
export type LoadLocationOptions = ArchiveOptions & SyncArchiveOptions & LogOptions & PreloadOption & LoadLocationHookOptions;
/**
* Hook options for `loadLocation()`
*/
export type LoadLocationHookOptions = MapNodeModulesHookOptions & {
moduleSourceHook?: ModuleSourceHook | undefined;
};
/**
* Options for `importLocation()` necessary (but not sufficient--see
* `ReadNowPowers`) for dynamic require support
*/
export type SyncImportLocationOptions = SyncArchiveOptions & ExecuteOptions & LogOptions & PreloadOption;
/**
* Options for `importLocation()` without dynamic require support
*/
export type ImportLocationOptions = ArchiveOptions & ExecuteOptions & LoadLocationHookOptions & LogOptions & PreloadOption;
export type ComputeSha512Option = {
/**
* For computing integrity hashes for module descriptors based on captured sources.
*/
computeSha512?: HashFn | undefined;
};
export type SearchSuffixesOption = {
/**
* Suffixes to search if the unmodified specifier is not found. Pass `[]` to
* emulate Node.js' strict behavior. The default handles Node.js' CommonJS
* behavior.
* Unlike Node.js, the Compartment Mapper lifts CommonJS up, more like a
* bundler, and does not attempt to vary the behavior of resolution depending
* on the language of the importing module.
*/
searchSuffixes?: string[] | undefined;
};
export type SourceMapHookOption = {
sourceMapHook?: SourceMapHook | undefined;
};
export type ModulesOption = {
modules?: Record<string, any> | undefined;
};
export type ExitModuleImportHookOption = {
/**
* For obtaining module descriptors for modules that must be provided
* by the eventual runtime execution environment, asynchronously.
*/
importHook?: ExitModuleImportHook | undefined;
};
export type ExitModuleImportNowHookOption = {
/**
* For obtaining module descriptors for modules that must be provided
* by the eventual runtime execution environment, synchronusly.
*/
importNowHook?: ExitModuleImportNowHook | undefined;
};
export type ParserForLanguageOption = {
parserForLanguage?: ParserForLanguage | undefined;
};
export type CompartmentOption = {
Compartment?: typeof Compartment | undefined;
};
export type ModuleTransformsOption = {
moduleTransforms?: ModuleTransforms | undefined;
};
export type SyncModuleTransformsOption = {
syncModuleTransforms?: SyncModuleTransforms | undefined;
};
export type ArchiveOnlyOption = {
/**
* Whether to prepare to create an archive or script bundle for execution
* elsewhere or elsewhen, as opposed to preparing to execute immediately.
*
* This has several practical effects.
*
* Archives expect to exit to potentially different host modules than the current
* host, but cannot instantiate those modules.
* For example, when preparing a bundle for execution in Node.js from within a
* web page, exiting to `node:fs` is appropriate but cannot be instantiated.
* So, the import hook will make a note of the exit and provide a stub module
* that throws an error if it is imported.
*
* Also, importing a module graph off a local medium immediately should
* inject a fully qualified source location into the module source,
* but sources loaded for an archive must not capture the original source
* location, but give the runtime an opportunity to inject a sourceURL.
*
* Also, the linker does not apply attenuations to the global environment
* if it is preparing to write an archive or script bundle.
*
* This option does not generally surface to users, but is set by the scenario,
* off for `importLocation`, on for `makeArchive` and `makeScript`.
*/
archiveOnly?: boolean | undefined;
};
export type PolicyOption = {
policy?: SomePolicy | undefined;
};
export type LanguageForExtensionOption = {
languageForExtension?: LanguageForExtension | undefined;
};
export type SyncOrAsyncArchiveOptions = Partial<{
captureSourceLocation: CaptureSourceLocationHook | undefined;
}> & ParserForLanguageOption & CompartmentOption & PolicyOption;
type SyncOrAsyncImportingOptions = SearchSuffixesOption & SourceMapHookOption;
type ImportingOptions = ModulesOption & SyncOrAsyncImportingOptions & ExitModuleImportHookOption;
type SyncImportingOptions = ModulesOption & SyncOrAsyncImportingOptions & ExitModuleImportNowHookOption;
type LinkingOptions = ParserForLanguageOption & CompartmentOption & SyncModuleTransformsOption & ModuleTransformsOption;
/**
* Result of `digestCompartmentMap()`
*/
export interface DigestResult<OldCompartmentName extends string = FileUrlString, NewCompartmentName extends string = PackageCompartmentDescriptorName> {
/**
* Normalized `CompartmentMapDescriptor`
*/
compartmentMap: DigestedCompartmentMapDescriptor;
/**
* Sources found in the `CompartmentMapDescriptor`
*/
sources: Sources;
/**
* A record of renamed {@link CompartmentDescriptor CompartmentDescriptors}
* from _new_ to _original_ name
*/
newToOldCompartmentNames: Record<NewCompartmentName, OldCompartmentName>;
/**
* A record of renamed {@link CompartmentDescriptor CompartmentDescriptors}
* from _original_ to _new_ name
*/
oldToNewCompartmentNames: Record<OldCompartmentName, NewCompartmentName>;
/**
* Alias for `newToOldCompartmentNames`
* @deprecated Use {@link newToOldCompartmentNames} instead.
*/
compartmentRenames: Record<NewCompartmentName, OldCompartmentName>;
}
/**
* The result of `captureFromMap`.
*/
export type CaptureResult = Omit<DigestResult, 'compartmentMap' | 'sources'> & {
captureCompartmentMap: DigestedCompartmentMapDescriptor;
captureSources: DigestResult['sources'];
};
/**
* The result of `makeArchiveCompartmentMap`
*/
export type ArchiveResult = Omit<DigestResult<FileUrlString, string>, 'compartmentMap' | 'sources'> & {
archiveCompartmentMap: DigestedCompartmentMapDescriptor;
archiveSources: DigestResult['sources'];
};
/**
* The compartment mapper can capture the Sources for all loaded modules
* for bundling, archiving, or analysis.
*/
export type Sources = Record<string, CompartmentSources>;
export type CompartmentSources = Record<string, ModuleSource>;
export type ModuleSource = LocalModuleSource | ExitModuleSource | ErrorModuleSource;
export interface BaseModuleSource {
/** module loading error deferred to later stage */
deferredError?: string;
}
export interface ErrorModuleSource extends BaseModuleSource {
deferredError: string;
}
export interface LocalModuleSource extends BaseModuleSource {
/**
* package-relative location.
* Not suitable for capture in an archive or bundle since it varies from host
* to host and would frustrate integrity hash checks.
*/
location: string;
parser: Language;
/** in lowercase base-16 (hexadecimal) */
sha512?: string;
/**
* directory name of the original source.
* This is safe to capture in a compartment map because it is _unlikely_ to
* vary between hosts.
* Package managers tend to drop a package in a consistently named location.
* If entry package is in a workspace, git enforces consistency.
* If entry package is the root of a repository, we rely on the developer
* to name the package consistently and suffer an inconsistent integrity hash
* otherwise.
* We do not currently capture this property in a compartment map because the
* schema validator currently (2024) deployed to Agoric blockchains does not
* tolerate compartment maps with unknown properties.
* https://github.com/endojs/endo/issues/2671
*/
sourceDirname: string;
/** fully qualified location */
sourceLocation: FileUrlString;
bytes: Uint8Array;
/** module for the module */
record: StaticModuleType;
}
export interface ExitModuleSource extends BaseModuleSource {
/** indicates that this is a reference that exits the mapped compartments */
exit: string;
}
export type SourceMapHook = (sourceMap: string, details: SourceMapHookDetails) => void;
export type SourceMapHookDetails = {
compartment: string;
module: string;
location: string;
sha512: string;
};
export type ModuleTransforms = Record<string, ModuleTransform>;
export type SyncModuleTransforms = Record<string, SyncModuleTransform>;
export type ModuleTransform = (...args: ModuleTransformArguments) => Promise<ModuleTransformResult>;
export type SyncModuleTransform = (...args: ModuleTransformArguments) => ModuleTransformResult;
type ModuleTransformArguments = [
bytes: Uint8Array,
specifier: string,
moduleLocation: string,
packageLocation: string,
params: {
sourceMap?: string | undefined;
}
];
type ModuleTransformResult = {
bytes: Uint8Array;
parser: Language;
sourceMap?: string | undefined;
};
export type ExitModuleImportHook = (specifier: string, packageLocation: string) => Promise<ModuleDescriptor | undefined>;
export type ExitModuleImportNowHook = (specifier: string, packageLocation: string) => ModuleDescriptor | undefined;
export type ComputeSourceLocationHook = (compartmentName: string, moduleSpecifier: string) => string | undefined;
/**
* A hook for archiving that allows the caller to create a side-table or
* out-of-band reference for where the archive's sources originated,
* intended to assist debuggers.
* When making and importing an archive locally, `import-archive.js` can inject
* the original source location for compartment name and module specifier chosen
* by `archive.js.
*/
export type CaptureSourceLocationHook = (compartmentName: string, moduleSpecifier: string, sourceLocation: string) => void;
export type ComputeSourceMapLocationDetails = {
compartment: string;
module: string;
location: string;
sha512: string;
};
export type ComputeSourceMapLocationHook = (details: ComputeSourceMapLocationDetails) => string;
interface BaseParserImplementation {
/**
* Whether a heuristic is used by parser to detect imports.
* CommonJS uses a lexer to heuristically discover static require calls.
*/
heuristicImports: boolean;
}
export interface ParserImplementation extends BaseParserImplementation {
parse: ParseFn;
synchronous: true;
}
export interface AsyncParserImplementation extends BaseParserImplementation {
parse: AsyncParseFn;
synchronous: false;
}
type ParseArguments = [
bytes: Uint8Array,
specifier: string,
moduleLocation: string,
packageLocation: string,
options?: Partial<{
sourceMap: string | undefined;
sourceMapHook: SourceMapHook | undefined;
sourceMapUrl: string | undefined;
readPowers: ReadFn | ReadPowers | undefined;
compartmentDescriptor: CompartmentDescriptor | undefined;
}> & ArchiveOnlyOption
];
/**
* Result of a {@link ParseFn} or {@link AsyncParseFn}
*/
export type ParseResult = {
bytes: Uint8Array;
parser: Language;
record: FinalStaticModuleType;
sourceMap?: string | undefined;
};
/**
* A synchronous module parsing function.
*
* @todo This and all parser-related types (including {@link ParseResult} should
* be moved into a separate package. `@endo/parser-pipeline` needs these types,
* and a package needing the types in `@endo/parser-pipeline` would then pull in
* `@endo/compartment-mapper` as a transitive dep and all _its_ transitive deps.
* Because {@link ParseResult} contains {@link FinalStaticModuleType} from
* `ses`, those types would want to be moved out of `ses` with it.
*/
export type ParseFn = {
isSyncParser?: true;
} & ((...args: ParseArguments) => ParseResult);
/**
* An asynchronous module parsing function.
*/
export type AsyncParseFn = {
isSyncParser?: false;
} & ((...args: ParseArguments) => Promise<ParseResult>);
/**
* Mapping of `Language` to synchronous {@link ParserImplementation}s only.
*
* Used when all parsers are known to be synchronous.
*/
export type SyncParserForLanguage = Record<Language | string, ParserImplementation>;
/**
* Mapping of `Language` to {@link ParserImplementation
* ParserImplementations} or {@link AsyncParserImplementation}s.
*
* When any entry has `synchronous: false`, the pipeline uses the async
* trampoline.
*/
export type ParserForLanguage = Record<Language | string, ParserImplementation | AsyncParserImplementation>;
/**
* Generic logging function accepted by various functions.
*/
export type LogFn = (...args: any[]) => void;
/**
* A string that represents a file URL.
*/
export type FileUrlString = `file://${string}`;
/**
* A function that renames compartments; used by `digestCompartmentMap()`.
*
* The default implementation uses {@link PackageCompartmentDescriptorName} as the key type.
*
* @returns Mapping from old compartment names to new compartment names
* @template NewName Key type
*/
export type CompartmentsRenameFn<OldName extends string = FileUrlString, NewName extends string = PackageCompartmentDescriptorName> = (compartments: PackageCompartmentDescriptors) => Record<OldName, NewName>;
//# sourceMappingURL=external.d.ts.map