diff --git a/packages/ses/NEWS.md b/packages/ses/NEWS.md index 75df0419a9..c33ba398e3 100644 --- a/packages/ses/NEWS.md +++ b/packages/ses/NEWS.md @@ -13,6 +13,8 @@ User-visible changes in `ses`: - Node 18, Node 20, and all browsers have `structuredClone` - Node <= 16 have neither, but are also no longer supported by Endo. - Now exports separate layer for console shim: `ses/console-shim.js`. +- Adds permits for `ModuleSource`, either the native implementation or from + `@endo/module-source/shim.js`. # v1.8.0 (2024-08-27) diff --git a/packages/ses/src/error/console.js b/packages/ses/src/error/console.js index 8ad8853805..60ff798d93 100644 --- a/packages/ses/src/error/console.js +++ b/packages/ses/src/error/console.js @@ -368,8 +368,10 @@ export const makeCausalConsole = (baseConsole, loggedErrorHandler) => { const levelMethod = (...logArgs) => { const subErrors = []; const argTags = extractErrorArgs(logArgs, subErrors); - // eslint-disable-next-line @endo/no-polymorphic-call - baseConsole[level](...argTags); + if (baseConsole[level]) { + // eslint-disable-next-line @endo/no-polymorphic-call + baseConsole[level](...argTags); + } // @ts-expect-error ConsoleProp vs LogSeverity mismatch logSubErrors(level, subErrors); }; @@ -476,12 +478,16 @@ export const defineCausalConsoleFromLogger = loggedErrorHandler => { // the host console will separate them with additional spaces. arrayPush(indents, ' '); }); + } else { + baseConsole[name] = () => {}; } } if (baseConsole.groupEnd) { baseConsole.groupEnd = makeNamed('groupEnd', (...args) => { arrayPop(indents); }); + } else { + baseConsole.groupEnd = () => {}; } harden(baseConsole); const causalConsole = makeCausalConsole( diff --git a/packages/ses/src/lockdown.js b/packages/ses/src/lockdown.js index 170c341fbc..56f5bcad0d 100644 --- a/packages/ses/src/lockdown.js +++ b/packages/ses/src/lockdown.js @@ -46,6 +46,7 @@ import { makeSafeEvaluator } from './make-safe-evaluator.js'; import { initialGlobalPropertyNames } from './permits.js'; import { tameFunctionToString } from './tame-function-tostring.js'; import { tameDomains } from './tame-domains.js'; +import { tameModuleSource } from './tame-module-source.js'; import { tameConsole } from './error/tame-console.js'; import tameErrorConstructor from './error/tame-error-constructor.js'; @@ -286,6 +287,7 @@ export const repairIntrinsics = (options = {}) => { addIntrinsics(tameRegExpConstructor(regExpTaming)); addIntrinsics(tameSymbolConstructor()); addIntrinsics(shimArrayBufferTransfer()); + addIntrinsics(tameModuleSource()); addIntrinsics(getAnonymousIntrinsics()); diff --git a/packages/ses/src/permits.js b/packages/ses/src/permits.js index 585d3c0c3f..dd9eabd55b 100644 --- a/packages/ses/src/permits.js +++ b/packages/ses/src/permits.js @@ -101,8 +101,12 @@ export const universalPropertyNames = { // ESNext + // https://github.com/tc39/proposal-source-phase-imports?tab=readme-ov-file#js-module-source + ModuleSource: 'ModuleSource', + lockdown: 'lockdown', harden: 'harden', + HandledPromise: 'HandledPromise', // TODO: Until Promise.delegate (see below). }; @@ -1505,6 +1509,32 @@ export const permitted = { resolve: fn, }, + // https://github.com/tc39/proposal-source-phase-imports?tab=readme-ov-file#js-module-source + + ModuleSource: { + '[[Proto]]': '%AbstractModuleSource%', + prototype: '%ModuleSourcePrototype%', + }, + + '%ModuleSourcePrototype%': { + '[[Proto]]': '%AbstractModuleSourcePrototype%', + constructor: 'ModuleSource', + '@@toStringTag': 'string', + // https://github.com/tc39/proposal-compartments + bindings: getter, + needsImport: getter, + needsImportMeta: getter, + }, + + '%AbstractModuleSource%': { + '[[Proto]]': '%FunctionPrototype%', + prototype: '%AbstractModuleSourcePrototype%', + }, + + '%AbstractModuleSourcePrototype%': { + constructor: '%AbstractModuleSource%', + }, + Promise: { // Properties of the Promise Constructor '[[Proto]]': '%FunctionPrototype%', diff --git a/packages/ses/src/tame-module-source.js b/packages/ses/src/tame-module-source.js new file mode 100644 index 0000000000..6ceb3f9678 --- /dev/null +++ b/packages/ses/src/tame-module-source.js @@ -0,0 +1,51 @@ +import { + functionPrototype, + getPrototypeOf, + globalThis, + objectPrototype, + setPrototypeOf, +} from './commons.js'; + +export const tameModuleSource = () => { + const newIntrinsics = {}; + + const ModuleSource = globalThis.ModuleSource; + if (ModuleSource !== undefined) { + newIntrinsics.ModuleSource = ModuleSource; + + // We introduce ModuleSource.[[Proto]] === AbstractModuleSource + // and ModuleSource.prototype.[[Proto]] === AbstractModuleSource.prototype + // if that layer is absent because the permitting system can more + // gracefully tolerate the absence of an expected prototype than the + // presence of an unexpected prototype,. + function AbstractModuleSource() { + // no-op safe to super() + } + + const ModuleSourceProto = getPrototypeOf(ModuleSource); + if (ModuleSourceProto === functionPrototype) { + setPrototypeOf(ModuleSource, AbstractModuleSource); + newIntrinsics['%AbstractModuleSource%'] = AbstractModuleSource; + newIntrinsics['%AbstractModuleSourcePrototype%'] = + AbstractModuleSource.prototype; + } else { + newIntrinsics['%AbstractModuleSource%'] = ModuleSourceProto; + newIntrinsics['%AbstractModuleSourcePrototype%'] = + ModuleSourceProto.prototype; + } + + const ModuleSourcePrototype = ModuleSource.prototype; + if (ModuleSourcePrototype !== undefined) { + newIntrinsics['%ModuleSourcePrototype%'] = ModuleSourcePrototype; + + // ModuleSource.prototype.__proto__ should be the + // AbstractModuleSource.prototype. + const ModuleSourcePrototypeProto = getPrototypeOf(ModuleSourcePrototype); + if (ModuleSourcePrototypeProto === objectPrototype) { + setPrototypeOf(ModuleSource.prototype, AbstractModuleSource.prototype); + } + } + } + + return newIntrinsics; +}; diff --git a/packages/ses/test/module-source.test.js b/packages/ses/test/module-source.test.js index 9d48d96391..f2385ae9e6 100644 --- a/packages/ses/test/module-source.test.js +++ b/packages/ses/test/module-source.test.js @@ -6,6 +6,19 @@ import '@endo/module-source/shim.js'; lockdown(); +test('module source property/prototype graph and hardening', t => { + const AbstractModuleSource = Object.getPrototypeOf(ModuleSource); + t.is( + Object.getPrototypeOf(ModuleSource.prototype), + AbstractModuleSource.prototype, + ); + + t.truthy(Object.isFrozen(ModuleSource)); + t.truthy(Object.isFrozen(AbstractModuleSource)); + t.truthy(Object.isFrozen(ModuleSource.prototype)); + t.truthy(Object.isFrozen(AbstractModuleSource.prototype)); +}); + test('module source constructor', t => { const msr = new ModuleSource(` import foo from 'import-default-export-from-me.js'; @@ -44,3 +57,7 @@ test('module source constructor', t => { 'ModuleSource imports should be frozen', ); }); + +test('ModuleSource is a shared intrinsic', t => { + t.truthy(ModuleSource === new Compartment().globalThis.ModuleSource); +});