UNPKG

@endo/compartment-mapper

Version:

The compartment mapper assembles Node applications in a sandbox

638 lines 25.5 kB
/** * 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