From d6fb8ff8c0d3d7c6fec4119e66485ebb1ac1726c Mon Sep 17 00:00:00 2001 From: naugtur Date: Mon, 27 Feb 2023 11:32:14 +0100 Subject: [PATCH] feat(compartment-mapper): add exitModuleImportHook for dynamic exit modules --- .../compartment-mapper/demo/policy/index.mjs | 27 ++++++--- packages/compartment-mapper/src/archive.js | 3 + packages/compartment-mapper/src/bundle.js | 1 + .../compartment-mapper/src/import-archive.js | 28 ++++++++- .../compartment-mapper/src/import-hook.js | 44 +++++++++++--- packages/compartment-mapper/src/import.js | 16 ++++- packages/compartment-mapper/src/link.js | 51 +--------------- packages/compartment-mapper/src/policy.js | 58 +++++++++---------- packages/compartment-mapper/src/types.js | 8 +++ .../node_modules/app/importActualBuiltin.js | 3 + packages/compartment-mapper/test/scaffold.js | 18 ++++++ .../compartment-mapper/test/test-exit-hook.js | 34 +++++++++++ 12 files changed, 186 insertions(+), 105 deletions(-) create mode 100644 packages/compartment-mapper/test/fixtures-policy/node_modules/app/importActualBuiltin.js create mode 100644 packages/compartment-mapper/test/test-exit-hook.js diff --git a/packages/compartment-mapper/demo/policy/index.mjs b/packages/compartment-mapper/demo/policy/index.mjs index ac34a64c03..91d8934d21 100644 --- a/packages/compartment-mapper/demo/policy/index.mjs +++ b/packages/compartment-mapper/demo/policy/index.mjs @@ -7,10 +7,6 @@ lockdown({ }); import fs from 'fs'; -import os from 'os'; -import assert from 'assert'; -import zlib from 'zlib'; -import path from 'path'; import { importLocation, makeArchive, parseArchive } from '../../index.js'; @@ -98,13 +94,24 @@ const options = { console, process, }, + exitModuleImportHook: async specifier => { + const ns = await import(specifier); + return Object.freeze({ + imports: [], + exports: Object.keys(ns), + execute: moduleExports => { + moduleExports.default = ns; + Object.assign(moduleExports, ns); + }, + }); + }, modules: { - path: await addToCompartment('path', path), - assert: await addToCompartment('assert', assert), + // path: await addToCompartment('path', path), + // assert: await addToCompartment('assert', assert), buffer: await addToCompartment('buffer', Object.create(null)), // imported but unused - zlib: await addToCompartment('zlib', zlib), - fs: await addToCompartment('fs', fs), - os: await addToCompartment('os', os), + // zlib: await addToCompartment('zlib', zlib), + // fs: await addToCompartment('fs', fs), + // os: await addToCompartment('os', os), }, }; @@ -124,6 +131,7 @@ console.log('\n\n________________________________________________ Archive\n'); const archive = await makeArchive(readPower, entrypointPath, { modules: options.modules, policy: options.policy, + isExitModuleImportAllowed: true, }); console.log('>----------makeArchive -> parseArchive'); const application = await parseArchive(archive, '', { @@ -133,6 +141,7 @@ console.log('\n\n________________________________________________ Archive\n'); const { namespace } = await application.import({ globals: options.globals, modules: options.modules, + exitModuleImportHook: options.exitModuleImportHook, }); console.log('>----------import -> end'); console.log(2, namespace.poem); diff --git a/packages/compartment-mapper/src/archive.js b/packages/compartment-mapper/src/archive.js index 8d4163ad0b..3eb6fcb620 100644 --- a/packages/compartment-mapper/src/archive.js +++ b/packages/compartment-mapper/src/archive.js @@ -298,6 +298,7 @@ const digestLocation = async (powers, moduleLocation, options) => { searchSuffixes = undefined, commonDependencies = undefined, policy = undefined, + isExitModuleImportAllowed = false, } = options || {}; const { read, computeSha512 } = unpackReadPowers(powers); const { @@ -338,6 +339,8 @@ const digestLocation = async (powers, moduleLocation, options) => { sources, compartments, exitModules, + undefined, + isExitModuleImportAllowed, computeSha512, searchSuffixes, ); diff --git a/packages/compartment-mapper/src/bundle.js b/packages/compartment-mapper/src/bundle.js index d4806150fa..d978617ae1 100644 --- a/packages/compartment-mapper/src/bundle.js +++ b/packages/compartment-mapper/src/bundle.js @@ -212,6 +212,7 @@ export const makeBundle = async (read, moduleLocation, options) => { sources, compartments, undefined, + undefined, // TODO: support exitModuleImportHook undefined, searchSuffixes, ); diff --git a/packages/compartment-mapper/src/import-archive.js b/packages/compartment-mapper/src/import-archive.js index 1135b7e7c0..3508f7c658 100644 --- a/packages/compartment-mapper/src/import-archive.js +++ b/packages/compartment-mapper/src/import-archive.js @@ -14,6 +14,7 @@ /** @typedef {import('./types.js').ComputeSourceLocationHook} ComputeSourceLocationHook */ /** @typedef {import('./types.js').LoadArchiveOptions} LoadArchiveOptions */ /** @typedef {import('./types.js').ExecuteOptions} ExecuteOptions */ +/** @typedef {import('./types.js').ExitModuleImportHook} ExitModuleImportHook */ import { ZipReader } from '@endo/zip'; import { link } from './link.js'; @@ -78,6 +79,7 @@ const postponeErrorToExecute = errorMessage => { * @param {string} archiveLocation * @param {HashFn} [computeSha512] * @param {ComputeSourceLocationHook} [computeSourceLocation] + * @param {ExitModuleImportHook} [exitModuleImportHook] * @returns {ArchiveImportHookMaker} */ const makeArchiveImportHookMaker = ( @@ -86,6 +88,7 @@ const makeArchiveImportHookMaker = ( archiveLocation, computeSha512 = undefined, computeSourceLocation = undefined, + exitModuleImportHook = undefined, ) => { // per-assembly: /** @type {ArchiveImportHookMaker} */ @@ -97,6 +100,16 @@ const makeArchiveImportHookMaker = ( // per-module: const module = modules[moduleSpecifier]; if (module === undefined) { + if (exitModuleImportHook) { + console.error('#################x'); + const record = await exitModuleImportHook(moduleSpecifier); + if (record) { + return { + record, + specifier: moduleSpecifier, + }; + } + } throw Error( `Cannot find module ${q(moduleSpecifier)} in package ${q( packageLocation, @@ -190,6 +203,7 @@ const makeFeauxModuleExportsNamespace = Compartment => { * @param {string} [options.expectedSha512] * @param {HashFn} [options.computeSha512] * @param {Record} [options.modules] + * @param {ExitModuleImportHook} [options.exitModuleImportHook] * @param {Compartment} [options.Compartment] * @param {ComputeSourceLocationHook} [options.computeSourceLocation] * @returns {Promise} @@ -205,6 +219,7 @@ export const parseArchive = async ( computeSourceLocation = undefined, Compartment = DefaultCompartment, modules = undefined, + exitModuleImportHook = undefined, } = options; const archive = new ZipReader(archiveBytes, { name: archiveLocation }); @@ -264,6 +279,7 @@ export const parseArchive = async ( archiveLocation, computeSha512, computeSourceLocation, + exitModuleImportHook, ); // A weakness of the current Compartment design is that the `modules` map // must be given a module namespace object that passes a brand check. @@ -277,6 +293,7 @@ export const parseArchive = async ( return [specifier, makeFeauxModuleExportsNamespace(Compartment)]; }), ), + isExitModuleImportAllowed: exitModuleImportHook !== undefined, Compartment, }); @@ -291,14 +308,21 @@ export const parseArchive = async ( /** @type {ExecuteFn} */ const execute = async options => { - const { globals, modules, transforms, __shimTransforms__, Compartment } = - options || {}; + const { + globals, + modules, + transforms, + __shimTransforms__, + Compartment, + exitModuleImportHook, + } = options || {}; const makeImportHook = makeArchiveImportHookMaker( get, compartments, archiveLocation, computeSha512, computeSourceLocation, + exitModuleImportHook, ); const { compartment, pendingJobsPromise } = link(compartmentMap, { makeImportHook, diff --git a/packages/compartment-mapper/src/import-hook.js b/packages/compartment-mapper/src/import-hook.js index be038abd8c..013aa86e2f 100644 --- a/packages/compartment-mapper/src/import-hook.js +++ b/packages/compartment-mapper/src/import-hook.js @@ -10,8 +10,10 @@ /** @typedef {import('./types.js').CompartmentSources} CompartmentSources */ /** @typedef {import('./types.js').CompartmentDescriptor} CompartmentDescriptor */ /** @typedef {import('./types.js').ImportHookMaker} ImportHookMaker */ +/** @typedef {import('./types.js').DeferredAttenuatorsProvider} DeferredAttenuatorsProvider */ +/** @typedef {import('./types.js').ExitModuleImportHook} ExitModuleImportHook */ -import { enforceModulePolicy } from './policy.js'; +import { attenuateModuleHook, enforceModulePolicy } from './policy.js'; import { unpackReadPowers } from './powers.js'; // q, as in quote, for quoting strings in error messages. @@ -62,9 +64,12 @@ const nodejsConventionSearchSuffixes = [ /** * @param {ReadFn|ReadPowers} readPowers * @param {string} baseLocation + * @param {DeferredAttenuatorsProvider} attenuators * @param {Sources} sources * @param {Record} compartmentDescriptors - * @param {Record} exitModules + * @param {ExitModuleImportHook=} exitModuleImportHook + * @param {boolean=} isExitModuleImportAllowed + * @param {boolean=} archiveOnly * @param {HashFn=} computeSha512 * @param {Array} searchSuffixes - Suffixes to search if the unmodified specifier is not found. * Pass [] to emulate Node.js’s strict behavior. @@ -77,9 +82,12 @@ const nodejsConventionSearchSuffixes = [ export const makeImportHookMaker = ( readPowers, baseLocation, + attenuators, sources = Object.create(null), compartmentDescriptors = Object.create(null), - exitModules = Object.create(null), + exitModuleImportHook = undefined, + isExitModuleImportAllowed = false, + archiveOnly = false, computeSha512 = undefined, searchSuffixes = nodejsConventionSearchSuffixes, ) => { @@ -143,19 +151,37 @@ export const makeImportHookMaker = ( // In Node.js, an absolute specifier always indicates a built-in or // third-party dependency. - // The `moduleMapHook` captures all third-party dependencies. + // The `moduleMapHook` captures all third-party dependencies, unless + // we allow importing any exit. if (moduleSpecifier !== '.' && !moduleSpecifier.startsWith('./')) { - if (has(exitModules, moduleSpecifier)) { - enforceModulePolicy(moduleSpecifier, compartmentDescriptor.policy, { + if (archiveOnly && isExitModuleImportAllowed) { + enforceModulePolicy(moduleSpecifier, compartmentDescriptor, { exit: true, }); - packageSources[moduleSpecifier] = { - exit: moduleSpecifier, - }; // Return a place-holder. // Archived compartments are not executed. return freeze({ imports: [], exports: [], execute() {} }); } + if (!archiveOnly && exitModuleImportHook) { + console.error('#################i'); + const record = await exitModuleImportHook(moduleSpecifier); + if (record) { + // It'd be nice to check the policy before importing it, but we can only throw a policy error if the + // hook returns something. Otherwise, we need to fall back to the 'cannot find' error below. + enforceModulePolicy(moduleSpecifier, compartmentDescriptor, { + exit: true, + }); + // note it's not being marked as exit in sources + // it could get marked and the second pass, when the archive is being executed, would have the data + // to enforce which exits can be dynamically imported + return attenuateModuleHook( + moduleSpecifier, + record, + compartmentDescriptor.policy, + attenuators, + ); + } + } return deferError( moduleSpecifier, Error( diff --git a/packages/compartment-mapper/src/import.js b/packages/compartment-mapper/src/import.js index 124a908ed0..e63a414ae9 100644 --- a/packages/compartment-mapper/src/import.js +++ b/packages/compartment-mapper/src/import.js @@ -70,14 +70,24 @@ export const loadLocation = async (readPowers, moduleLocation, options) => { /** @type {ExecuteFn} */ const execute = async (options = {}) => { - const { globals, modules, transforms, __shimTransforms__, Compartment } = - options; + const { + globals, + modules, + transforms, + __shimTransforms__, + Compartment, + exitModuleImportHook, + } = options; + const isExitModuleImportAllowed = exitModuleImportHook !== undefined; const makeImportHook = makeImportHookMaker( readPowers, packageLocation, + attenuators, // TODO: refactor how attenuators are imported to decouple from linking now. undefined, compartmentMap.compartments, - undefined, + modules, + exitModuleImportHook, + isExitModuleImportAllowed, undefined, searchSuffixes, ); diff --git a/packages/compartment-mapper/src/link.js b/packages/compartment-mapper/src/link.js index 1ee004b7de..f5ddb0d527 100644 --- a/packages/compartment-mapper/src/link.js +++ b/packages/compartment-mapper/src/link.js @@ -18,7 +18,6 @@ import { resolve } from './node-module-specifier.js'; import { parseExtension } from './extension.js'; import { enforceModulePolicy, - attenuateModuleHook, ATTENUATORS_COMPARTMENT, diagnoseMissingCompartmentError, attenuateGlobals, @@ -212,9 +211,6 @@ const trimModuleSpecifierPrefix = (moduleSpecifier, prefix) => { * @param {string} compartmentName * @param {Record} moduleDescriptors * @param {Record} scopeDescriptors - * @param {Record} exitModules - * @param {DeferredAttenuatorsProvider} attenuators - * @param {boolean} archiveOnly * @returns {ModuleMapHook | undefined} */ const makeModuleMapHook = ( @@ -223,9 +219,6 @@ const makeModuleMapHook = ( compartmentName, moduleDescriptors, scopeDescriptors, - exitModules, - attenuators, - archiveOnly, ) => { /** * @param {string} moduleSpecifier @@ -244,27 +237,7 @@ const makeModuleMapHook = ( exit, } = moduleDescriptor; if (exit !== undefined) { - enforceModulePolicy(moduleSpecifier, compartmentDescriptor, { - exit: true, - }); - const module = exitModules[exit]; - if (module === undefined) { - throw Error( - `Cannot import missing external module ${q( - exit, - )}, may be missing from ${compartmentName} package.json`, - ); - } - if (archiveOnly) { - return inertModuleNamespace; - } else { - return attenuateModuleHook( - exit, - module, - compartmentDescriptor.policy, - attenuators, - ); - } + return undefined; // fall through to import hook } if (foreignModuleSpecifier !== undefined) { if (!moduleSpecifier.startsWith('./')) { @@ -289,24 +262,6 @@ const makeModuleMapHook = ( } return foreignCompartment.module(foreignModuleSpecifier); } - } else if (has(exitModules, moduleSpecifier)) { - enforceModulePolicy(moduleSpecifier, compartmentDescriptor, { - exit: true, - }); - - // When linking off the filesystem as with `importLocation`, - // there isn't a module descriptor for every module. - moduleDescriptors[moduleSpecifier] = { exit: moduleSpecifier }; - if (archiveOnly) { - return inertModuleNamespace; - } else { - return attenuateModuleHook( - moduleSpecifier, - exitModules[moduleSpecifier], - compartmentDescriptor.policy, - attenuators, - ); - } } // Search for a scope that shares a prefix with the requested module @@ -395,7 +350,6 @@ export const link = ( transforms = [], moduleTransforms = {}, __shimTransforms__ = [], - modules: exitModules = {}, archiveOnly = false, Compartment = defaultCompartment, }, @@ -464,9 +418,6 @@ export const link = ( compartmentName, modules, scopes, - exitModules, - attenuators, - archiveOnly, ); const resolveHook = resolve; resolvers[compartmentName] = resolve; diff --git a/packages/compartment-mapper/src/policy.js b/packages/compartment-mapper/src/policy.js index c53d355ea9..7cfe3915ba 100644 --- a/packages/compartment-mapper/src/policy.js +++ b/packages/compartment-mapper/src/policy.js @@ -8,7 +8,7 @@ /** @typedef {import('./types.js').DeferredAttenuatorsProvider} DeferredAttenuatorsProvider */ /** @typedef {import('./types.js').CompartmentDescriptor} CompartmentDescriptor */ // get StaticModuleRecord from the ses package's types -/** @typedef {import('ses').ModuleExportsNamespace} ModuleExportsNamespace */ +/** @typedef {import('ses').ThirdPartyStaticModuleInterface} ThirdPartyStaticModuleInterface */ import { policyLookupHelper, @@ -389,57 +389,51 @@ export const enforceModulePolicy = (specifier, compartmentDescriptor, info) => { * @param {object} options * @param {DeferredAttenuatorsProvider} options.attenuators * @param {AttenuationDefinition} options.attenuationDefinition - * @param {ModuleExportsNamespace} options.originalModule - * @returns {ModuleExportsNamespace} + * @param {ThirdPartyStaticModuleInterface} options.originalModuleRecord + * @returns {ThirdPartyStaticModuleInterface} */ function attenuateModule({ attenuators, attenuationDefinition, - originalModule, + originalModuleRecord, }) { - const attenuationCompartment = new Compartment( - {}, - {}, - { - resolveHook: moduleSpecifier => moduleSpecifier, - importHook: async () => { - const attenuate = await importAttenuatorForDefinition( - attenuationDefinition, - attenuators, - MODULE_ATTENUATOR, - ); - const ns = await attenuate(originalModule); - const staticModuleRecord = freeze({ - imports: [], - exports: keys(ns), - execute: moduleExports => { - assign(moduleExports, ns); - }, - }); - return staticModuleRecord; - }, + // No need for a new compartment anymore, since instead of a module namespace, we're now producing a module record. + return freeze({ + imports: originalModuleRecord.imports, + // It seems ok to declare the exports but then let the attenuator trim the values. + // Seems ok for attenuation to leave them undefined - accessing them is malicious behavior. + exports: originalModuleRecord.exports, + execute: async moduleExports => { + const attenuate = await importAttenuatorForDefinition( + attenuationDefinition, + attenuators, + MODULE_ATTENUATOR, + ); + const ns = {}; + originalModuleRecord.execute(ns); // TODO: why does this execute function want more arguments? + const attenuated = await attenuate(ns, moduleExports); // can pass the actual exports reference now that we moved async steps around. Not that it brings a lot of value though, because it's not extensible + assign(moduleExports, attenuated); }, - ); - return attenuationCompartment.module('.'); + }); } - /** * Throws if importing of the specifier is not allowed by the policy * * @param {string} specifier - exit module name - * @param {object} originalModule - reference to the exit module + * @param {ThirdPartyStaticModuleInterface} originalModuleRecord - reference to the exit module * @param {object} policy - local compartment policy * @param {DeferredAttenuatorsProvider} attenuators - a key-value where attenuations can be found + * @returns {ThirdPartyStaticModuleInterface} - the attenuated module */ export const attenuateModuleHook = ( specifier, - originalModule, + originalModuleRecord, policy, attenuators, ) => { const policyValue = policyLookupHelper(policy, 'builtins', specifier); if (!policy || policyValue === true) { - return originalModule; + return originalModuleRecord; } if (!policyValue) { @@ -453,7 +447,7 @@ export const attenuateModuleHook = ( return attenuateModule({ attenuators, attenuationDefinition: policyValue, - originalModule, + originalModuleRecord, }); }; diff --git a/packages/compartment-mapper/src/types.js b/packages/compartment-mapper/src/types.js index e2be0ba3c5..0e9e9f5f13 100644 --- a/packages/compartment-mapper/src/types.js +++ b/packages/compartment-mapper/src/types.js @@ -225,6 +225,12 @@ export {}; * @returns {string|undefined} sourceLocation */ +/** + * @callback ExitModuleImportHook + * @param {string} specifier + * @returns {Promise} module namespace + */ + /** * @typedef {object} LoadArchiveOptions * @property {string} [expectedSha512] @@ -239,6 +245,7 @@ export {}; * @property {Array} [transforms] * @property {Array} [__shimTransforms__] * @property {Record} [modules] + * @property {ExitModuleImportHook} [exitModuleImportHook] * @property {Record} [attenuations] * @property {Compartment} [Compartment] */ @@ -310,6 +317,7 @@ export {}; * @typedef {object} ArchiveOptions * @property {ModuleTransforms} [moduleTransforms] * @property {Record} [modules] + * @property {boolean} [isExitModuleImportAllowed] * @property {boolean} [dev] * @property {object} [policy] * @property {Set} [tags] diff --git a/packages/compartment-mapper/test/fixtures-policy/node_modules/app/importActualBuiltin.js b/packages/compartment-mapper/test/fixtures-policy/node_modules/app/importActualBuiltin.js new file mode 100644 index 0000000000..c183bfdb41 --- /dev/null +++ b/packages/compartment-mapper/test/fixtures-policy/node_modules/app/importActualBuiltin.js @@ -0,0 +1,3 @@ +import fs from 'fs'; + +export const rootExists = fs.existsSync('/'); \ No newline at end of file diff --git a/packages/compartment-mapper/test/scaffold.js b/packages/compartment-mapper/test/scaffold.js index b237124f5f..db17656c6f 100644 --- a/packages/compartment-mapper/test/scaffold.js +++ b/packages/compartment-mapper/test/scaffold.js @@ -84,6 +84,7 @@ export function scaffold( tags = undefined, searchSuffixes = undefined, commonDependencies = undefined, + additionalOptions = {}, } = {}, ) { // wrapping each time allows for convenient use of test.only @@ -126,11 +127,13 @@ export function scaffold( tags, searchSuffixes, commonDependencies, + ...additionalOptions, }); const { namespace } = await application.import({ globals: { ...globals, ...addGlobals }, modules, Compartment, + ...additionalOptions, }); return namespace; }); @@ -148,6 +151,7 @@ export function scaffold( tags, searchSuffixes, commonDependencies, + ...additionalOptions, }); return namespace; }); @@ -165,6 +169,7 @@ export function scaffold( tags, searchSuffixes, commonDependencies, + ...additionalOptions, }); const application = await parseArchive(archive, '', { modules: Object.fromEntries( @@ -181,6 +186,7 @@ export function scaffold( globals: { ...globals, ...addGlobals }, modules, Compartment, + ...additionalOptions, }); return namespace; }, @@ -200,6 +206,7 @@ export function scaffold( tags, searchSuffixes, commonDependencies, + ...additionalOptions, }); const prefixArchive = new Uint8Array(archive.length + 10); prefixArchive.set(archive, 10); @@ -212,6 +219,7 @@ export function scaffold( globals: { ...globals, ...addGlobals }, modules, Compartment, + ...additionalOptions, }); return namespace; }, @@ -243,6 +251,7 @@ export function scaffold( tags, searchSuffixes, commonDependencies, + ...additionalOptions, }); const application = await loadArchive(fakeRead, 'app.agar', { modules, @@ -252,6 +261,7 @@ export function scaffold( globals: { ...globals, ...addGlobals }, modules, Compartment, + ...additionalOptions, }); return namespace; }, @@ -283,11 +293,13 @@ export function scaffold( tags, searchSuffixes, commonDependencies, + ...additionalOptions, }); const { namespace } = await importArchive(fakeRead, 'app.agar', { globals: { ...globals, ...addGlobals }, modules, Compartment, + ...additionalOptions, }); return namespace; }, @@ -305,6 +317,7 @@ export function scaffold( tags, searchSuffixes, commonDependencies, + ...additionalOptions, }); const archiveBytes = await makeArchive(readPowers, fixture, { @@ -313,6 +326,7 @@ export function scaffold( tags, searchSuffixes, commonDependencies, + ...additionalOptions, }); const { computeSha512 } = readPowers; @@ -325,6 +339,7 @@ export function scaffold( tags, computeSha512, expectedSha512, + ...additionalOptions, }, ); @@ -342,6 +357,7 @@ export function scaffold( tags, searchSuffixes, commonDependencies, + ...additionalOptions, }); const archive = await makeArchive(readPowers, fixture, { @@ -350,6 +366,7 @@ export function scaffold( tags, searchSuffixes, commonDependencies, + ...additionalOptions, }); const reader = new ZipReader(archive); @@ -366,6 +383,7 @@ export function scaffold( parseArchive(corruptArchive, 'app.agar', { computeSha512, expectedSha512, + ...additionalOptions, }), { message: /compartment map failed a SHA-512 integrity check/, diff --git a/packages/compartment-mapper/test/test-exit-hook.js b/packages/compartment-mapper/test/test-exit-hook.js new file mode 100644 index 0000000000..255299f7ba --- /dev/null +++ b/packages/compartment-mapper/test/test-exit-hook.js @@ -0,0 +1,34 @@ +// import "./ses-lockdown.js"; +import 'ses'; +import test from 'ava'; +import { scaffold } from './scaffold.js'; + +const fixture = new URL( + 'fixtures-policy/node_modules/app/importActualBuiltin.js', + import.meta.url, +).toString(); + +scaffold( + 'exitModuleImportHook - import actual builtin', + test, + fixture, + (t, { namespace }) => { + t.is(namespace.rootExists, true); + }, + 1, // expected number of assertions + { + additionalOptions: { + exitModuleImportHook: async specifier => { + const ns = await import(specifier); + return Object.freeze({ + imports: [], + exports: Object.keys(ns), + execute: moduleExports => { + moduleExports.default = ns; + Object.assign(moduleExports, ns); + }, + }); + }, + }, + }, +);