From d5cc4076faf3d175e0fa82fc966827639ff61e23 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 14 Apr 2026 17:02:11 +0200 Subject: [PATCH 01/11] feat: Export endowment factories via sub-path Add `@metamask/snaps-execution-environments/endowments` sub-path export that exposes generic endowment factory modules (timeout, interval, date, textEncoder, textDecoder, crypto, math) for reuse in other SES-based projects like ocap-kernel. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../snaps-execution-environments/CHANGELOG.md | 5 ++++ .../snaps-execution-environments/package.json | 10 +++++++ .../src/endowments.ts | 29 +++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 packages/snaps-execution-environments/src/endowments.ts diff --git a/packages/snaps-execution-environments/CHANGELOG.md b/packages/snaps-execution-environments/CHANGELOG.md index 8e9ebbffe5..3ef46a8e85 100644 --- a/packages/snaps-execution-environments/CHANGELOG.md +++ b/packages/snaps-execution-environments/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Export endowment factories via `@metamask/snaps-execution-environments/endowments` sub-path ([#XXXX](https://github.com/MetaMask/snaps/pull/XXXX)) + - Exports endowment factory modules: `timeout`, `interval`, `date`, `textEncoder`, `textDecoder`, `crypto`, `math` + ## [11.0.2] ### Changed diff --git a/packages/snaps-execution-environments/package.json b/packages/snaps-execution-environments/package.json index 5d9624f09b..948b05fe83 100644 --- a/packages/snaps-execution-environments/package.json +++ b/packages/snaps-execution-environments/package.json @@ -28,6 +28,16 @@ "default": "./dist/index.cjs" } }, + "./endowments": { + "import": { + "types": "./dist/endowments.d.mts", + "default": "./dist/endowments.mjs" + }, + "require": { + "types": "./dist/endowments.d.cts", + "default": "./dist/endowments.cjs" + } + }, "./node-process": "./dist/webpack/node-process/bundle.js", "./node-thread": "./dist/webpack/node-thread/bundle.js", "./package.json": "./package.json" diff --git a/packages/snaps-execution-environments/src/endowments.ts b/packages/snaps-execution-environments/src/endowments.ts new file mode 100644 index 0000000000..aa74ef0928 --- /dev/null +++ b/packages/snaps-execution-environments/src/endowments.ts @@ -0,0 +1,29 @@ +/** + * Public endowment factory exports for use outside the Snaps ecosystem. + * + * Each module provides a `names` array and a `factory` function. Call + * `factory()` to obtain hardened endowment values (and an optional + * `teardownFunction` for timer-based endowments). + * + * @example + * ```ts + * import { timeout, date } from '@metamask/snaps-execution-environments/endowments'; + * + * const timers = timeout.factory(); + * // { setTimeout, clearTimeout, teardownFunction } + * + * const dateEndowment = date.factory(); + * // { Date } (with noise-added Date.now) + * ``` + * + * @module endowments + */ + +// Individual endowment factory modules +export { default as timeout } from './common/endowments/timeout'; +export { default as interval } from './common/endowments/interval'; +export { default as date } from './common/endowments/date'; +export { default as textEncoder } from './common/endowments/textEncoder'; +export { default as textDecoder } from './common/endowments/textDecoder'; +export { default as crypto } from './common/endowments/crypto'; +export { default as math } from './common/endowments/math'; From 316fff3476998c4cd3bfab2f2fd69475b3fc5f6f Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 14 Apr 2026 17:02:42 +0200 Subject: [PATCH 02/11] chore: Update changelog PR number to #3957 Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/snaps-execution-environments/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/snaps-execution-environments/CHANGELOG.md b/packages/snaps-execution-environments/CHANGELOG.md index 3ef46a8e85..1bb5612594 100644 --- a/packages/snaps-execution-environments/CHANGELOG.md +++ b/packages/snaps-execution-environments/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Export endowment factories via `@metamask/snaps-execution-environments/endowments` sub-path ([#XXXX](https://github.com/MetaMask/snaps/pull/XXXX)) +- Export endowment factories via `@metamask/snaps-execution-environments/endowments` sub-path ([#3957](https://github.com/MetaMask/snaps/pull/3957)) - Exports endowment factory modules: `timeout`, `interval`, `date`, `textEncoder`, `textDecoder`, `crypto`, `math` ## [11.0.2] From 461d9cdb16e4b78bb76e081dd81da3e1a9e958b1 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 14 Apr 2026 18:40:22 +0200 Subject: [PATCH 03/11] fix: Exclude endowments barrel from coverage The endowments.ts barrel file is a pure re-export module with no logic to test, matching the existing exclusion pattern for index.ts. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/snaps-execution-environments/jest.config.js | 1 + packages/snaps-execution-environments/vitest.config.mts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/snaps-execution-environments/jest.config.js b/packages/snaps-execution-environments/jest.config.js index 3680265193..9a0b30256f 100644 --- a/packages/snaps-execution-environments/jest.config.js +++ b/packages/snaps-execution-environments/jest.config.js @@ -10,6 +10,7 @@ module.exports = deepmerge(baseConfig, { coverageReporters: ['html', 'json-summary', 'json'], coveragePathIgnorePatterns: [ './src/index.ts', + './src/endowments.ts', './src/iframe/index.ts', './src/offscreen/index.ts', './src/webworker/executor/index.ts', diff --git a/packages/snaps-execution-environments/vitest.config.mts b/packages/snaps-execution-environments/vitest.config.mts index e33858818a..9af51d76c4 100644 --- a/packages/snaps-execution-environments/vitest.config.mts +++ b/packages/snaps-execution-environments/vitest.config.mts @@ -114,6 +114,7 @@ export default defineConfig({ // The files to exclude from the coverage report. exclude: [ 'src/**/index.ts', + 'src/endowments.ts', 'src/**/*.d.ts', 'src/**/*.test.ts', 'src/**/test-utils/**', From ecafea487c11fb91453a5bd0c62db5d2c26720cd Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Thu, 16 Apr 2026 15:56:52 +0200 Subject: [PATCH 04/11] refactor: Generalize endowment factory types for public API Extract NotifyFunction from BaseSnapExecutor into commonEndowmentFactory to remove the dependency on the executor. Rename snapId to sourceLabel in EndowmentFactoryOptions so external consumers (e.g., ocap-kernel) can provide their own label format. Add EndowmentFactoryResult type with explicit teardownFunction. Export all endowment factories, including console and network, via the /endowments sub-path. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/common/BaseSnapExecutor.ts | 4 +- .../endowments/commonEndowmentFactory.ts | 51 ++++++++++++++++-- .../src/common/endowments/console.test.ts | 54 ++++++++++++++----- .../src/common/endowments/console.ts | 18 +++---- .../endowments/endowments.test.browser.ts | 7 ++- .../src/common/endowments/index.ts | 22 +++----- .../src/endowments.ts | 12 +++++ 7 files changed, 121 insertions(+), 47 deletions(-) diff --git a/packages/snaps-execution-environments/src/common/BaseSnapExecutor.ts b/packages/snaps-execution-environments/src/common/BaseSnapExecutor.ts index 2e88648259..f57e3f1314 100644 --- a/packages/snaps-execution-environments/src/common/BaseSnapExecutor.ts +++ b/packages/snaps-execution-environments/src/common/BaseSnapExecutor.ts @@ -89,9 +89,7 @@ export type InvokeSnap = ( args: InvokeSnapArgs | undefined, ) => Promise; -export type NotifyFunction = ( - notification: Omit, -) => Promise; +export type { NotifyFunction } from './endowments/commonEndowmentFactory'; export class BaseSnapExecutor { readonly #snapData: Map; diff --git a/packages/snaps-execution-environments/src/common/endowments/commonEndowmentFactory.ts b/packages/snaps-execution-environments/src/common/endowments/commonEndowmentFactory.ts index c69d555081..db17e5b8fa 100644 --- a/packages/snaps-execution-environments/src/common/endowments/commonEndowmentFactory.ts +++ b/packages/snaps-execution-environments/src/common/endowments/commonEndowmentFactory.ts @@ -1,3 +1,5 @@ +import type { JsonRpcNotification } from '@metamask/utils'; + import consoleEndowment from './console'; import crypto from './crypto'; import date from './date'; @@ -7,19 +9,62 @@ import network from './network'; import textDecoder from './textDecoder'; import textEncoder from './textEncoder'; import timeout from './timeout'; -import type { NotifyFunction } from '../BaseSnapExecutor'; import { rootRealmGlobal } from '../globalObject'; +/** + * A function for sending JSON-RPC notifications from an endowment. + * Used by the network endowment to signal outbound request lifecycle events. + */ +export type NotifyFunction = ( + notification: Omit, +) => Promise; + +/** + * Options passed to endowment factory functions. + */ export type EndowmentFactoryOptions = { - snapId?: string; + /** + * A label identifying the source of endowment interactions (e.g., console + * output). The caller controls the format — Snaps passes `Snap: ${snapId}`, + * but external consumers may use any label. + */ + sourceLabel?: string; + + /** + * A notification callback used by endowments that perform outbound + * operations (e.g., network `fetch`). + */ notify?: NotifyFunction; }; +/** + * The object returned by an endowment factory. Contains the named endowment + * values (keyed by name) and an optional teardown function for lifecycle + * management. + */ +export type EndowmentFactoryResult = { + /** + * An optional function that performs cleanup when the execution environment + * becomes idle. Must not render endowments unusable — only restore them to + * their initial state, since they may be reused without reconstruction. + */ + teardownFunction?: () => Promise | void; + [key: string]: unknown; +}; + +/** + * Describes an endowment factory module. Each module exposes the names of + * the endowments it provides and a factory function that produces them. + */ export type EndowmentFactory = { names: readonly string[]; - factory: (options?: EndowmentFactoryOptions) => { [key: string]: unknown }; + factory: (options?: EndowmentFactoryOptions) => EndowmentFactoryResult; }; +/** + * Describes a simple global value that should be hardened and exposed as an + * endowment without additional attenuation. + */ export type CommonEndowmentSpecification = { endowment: unknown; name: string; diff --git a/packages/snaps-execution-environments/src/common/endowments/console.test.ts b/packages/snaps-execution-environments/src/common/endowments/console.test.ts index 61f628dcac..498cc170c6 100644 --- a/packages/snaps-execution-environments/src/common/endowments/console.test.ts +++ b/packages/snaps-execution-environments/src/common/endowments/console.test.ts @@ -16,7 +16,7 @@ describe('Console endowment', () => { it('returns console properties from rootRealmGlobal', () => { const { console }: { console: Partial } = - consoleEndowment.factory({ snapId: MOCK_SNAP_ID }); + consoleEndowment.factory({ sourceLabel: `Snap: ${MOCK_SNAP_ID}` }); const consoleProperties = Object.getOwnPropertyNames( rootRealmGlobal.console, ); @@ -33,12 +33,16 @@ describe('Console endowment', () => { describe('log', () => { it('does not return the original console.log', () => { - const { console } = consoleEndowment.factory({ snapId: MOCK_SNAP_ID }); + const { console } = consoleEndowment.factory({ + sourceLabel: `Snap: ${MOCK_SNAP_ID}`, + }); expect(console.log).not.toStrictEqual(rootRealmGlobal.console.log); }); it('will log a message identifying the source of the call (snap id)', () => { - const { console } = consoleEndowment.factory({ snapId: MOCK_SNAP_ID }); + const { console } = consoleEndowment.factory({ + sourceLabel: `Snap: ${MOCK_SNAP_ID}`, + }); const logSpy = jest.spyOn(rootRealmGlobal.console, 'log'); console.log('This is a log message.'); expect(logSpy).toHaveBeenCalledTimes(1); @@ -48,7 +52,9 @@ describe('Console endowment', () => { }); it('can handle non-string message types', () => { - const { console } = consoleEndowment.factory({ snapId: MOCK_SNAP_ID }); + const { console } = consoleEndowment.factory({ + sourceLabel: `Snap: ${MOCK_SNAP_ID}`, + }); const logSpy = jest.spyOn(rootRealmGlobal.console, 'log'); console.log(12345); console.log({ foo: 'bar' }); @@ -66,12 +72,16 @@ describe('Console endowment', () => { describe('error', () => { it('does not return the original console.error', () => { - const { console } = consoleEndowment.factory({ snapId: MOCK_SNAP_ID }); + const { console } = consoleEndowment.factory({ + sourceLabel: `Snap: ${MOCK_SNAP_ID}`, + }); expect(console.error).not.toStrictEqual(rootRealmGlobal.console.error); }); it('will log a message identifying the source of the call (snap id)', () => { - const { console } = consoleEndowment.factory({ snapId: MOCK_SNAP_ID }); + const { console } = consoleEndowment.factory({ + sourceLabel: `Snap: ${MOCK_SNAP_ID}`, + }); const errorSpy = jest.spyOn(rootRealmGlobal.console, 'error'); console.error('This is an error message.'); expect(errorSpy).toHaveBeenCalledTimes(1); @@ -83,12 +93,16 @@ describe('Console endowment', () => { describe('assert', () => { it('does not return the original console.assert', () => { - const { console } = consoleEndowment.factory({ snapId: MOCK_SNAP_ID }); + const { console } = consoleEndowment.factory({ + sourceLabel: `Snap: ${MOCK_SNAP_ID}`, + }); expect(console.assert).not.toStrictEqual(rootRealmGlobal.console.assert); }); it('will log a message identifying the source of the call (snap id)', () => { - const { console } = consoleEndowment.factory({ snapId: MOCK_SNAP_ID }); + const { console } = consoleEndowment.factory({ + sourceLabel: `Snap: ${MOCK_SNAP_ID}`, + }); const assertSpy = jest.spyOn(rootRealmGlobal.console, 'assert'); console.assert(1 > 2, 'This is an assert message.'); expect(assertSpy).toHaveBeenCalledTimes(1); @@ -101,12 +115,16 @@ describe('Console endowment', () => { describe('debug', () => { it('does not return the original console.debug', () => { - const { console } = consoleEndowment.factory({ snapId: MOCK_SNAP_ID }); + const { console } = consoleEndowment.factory({ + sourceLabel: `Snap: ${MOCK_SNAP_ID}`, + }); expect(console.debug).not.toStrictEqual(rootRealmGlobal.console.debug); }); it('will log a message identifying the source of the call (snap id)', () => { - const { console } = consoleEndowment.factory({ snapId: MOCK_SNAP_ID }); + const { console } = consoleEndowment.factory({ + sourceLabel: `Snap: ${MOCK_SNAP_ID}`, + }); const debugSpy = jest.spyOn(rootRealmGlobal.console, 'debug'); console.debug('This is a debug message.'); expect(debugSpy).toHaveBeenCalledTimes(1); @@ -118,12 +136,16 @@ describe('Console endowment', () => { describe('info', () => { it('does not return the original console.info', () => { - const { console } = consoleEndowment.factory({ snapId: MOCK_SNAP_ID }); + const { console } = consoleEndowment.factory({ + sourceLabel: `Snap: ${MOCK_SNAP_ID}`, + }); expect(console.info).not.toStrictEqual(rootRealmGlobal.console.info); }); it('will log a message identifying the source of the call (snap id)', () => { - const { console } = consoleEndowment.factory({ snapId: MOCK_SNAP_ID }); + const { console } = consoleEndowment.factory({ + sourceLabel: `Snap: ${MOCK_SNAP_ID}`, + }); const infoSpy = jest.spyOn(rootRealmGlobal.console, 'info'); console.info('This is an info message.'); expect(infoSpy).toHaveBeenCalledTimes(1); @@ -135,12 +157,16 @@ describe('Console endowment', () => { describe('warn', () => { it('does not return the original console.warn', () => { - const { console } = consoleEndowment.factory({ snapId: MOCK_SNAP_ID }); + const { console } = consoleEndowment.factory({ + sourceLabel: `Snap: ${MOCK_SNAP_ID}`, + }); expect(console.warn).not.toStrictEqual(rootRealmGlobal.console.warn); }); it('will log a message identifying the source of the call (snap id)', () => { - const { console } = consoleEndowment.factory({ snapId: MOCK_SNAP_ID }); + const { console } = consoleEndowment.factory({ + sourceLabel: `Snap: ${MOCK_SNAP_ID}`, + }); const warnSpy = jest.spyOn(rootRealmGlobal.console, 'warn'); console.warn('This is a warn message.'); expect(warnSpy).toHaveBeenCalledTimes(1); diff --git a/packages/snaps-execution-environments/src/common/endowments/console.ts b/packages/snaps-execution-environments/src/common/endowments/console.ts index dd3c394bda..11fc5f5b76 100644 --- a/packages/snaps-execution-environments/src/common/endowments/console.ts +++ b/packages/snaps-execution-environments/src/common/endowments/console.ts @@ -52,13 +52,13 @@ type ConsoleFunctions = { * Gets the appropriate (prepended) message to pass to one of the attenuated * method calls. * - * @param snapId - Id of the snap that we're getting a message for. - * @param message - The id of the snap that will interact with the endowment. + * @param sourceLabel - Label identifying the source of the console call. + * @param message - The first argument passed to the console method. * @param args - The array of additional arguments. * @returns An array of arguments to be passed into an attenuated console method call. */ -function getMessage(snapId: string, message: unknown, ...args: unknown[]) { - const prefix = `[Snap: ${snapId}]`; +function getMessage(sourceLabel: string, message: unknown, ...args: unknown[]) { + const prefix = `[${sourceLabel}]`; // If the first argument is a string, prepend the prefix to the message, and keep the // rest of the arguments as-is. @@ -76,11 +76,11 @@ function getMessage(snapId: string, message: unknown, ...args: unknown[]) { * {@link console} object, but with some methods replaced. * * @param options - Factory options used in construction of the endowment. - * @param options.snapId - The id of the snap that will interact with the endowment. + * @param options.sourceLabel - Label identifying the source of the console call. * @returns The {@link console} object with the replaced methods. */ -function createConsole({ snapId }: EndowmentFactoryOptions = {}) { - assert(snapId !== undefined); +function createConsole({ sourceLabel }: EndowmentFactoryOptions = {}) { + assert(sourceLabel !== undefined); const keys = Object.getOwnPropertyNames( rootRealmGlobal.console, ) as (keyof typeof console)[]; @@ -103,7 +103,7 @@ function createConsole({ snapId }: EndowmentFactoryOptions = {}) { ) => { rootRealmGlobal.console.assert( value, - ...getMessage(snapId, message, ...optionalParams), + ...getMessage(sourceLabel, message, ...optionalParams), ); }, ...consoleFunctions.reduce((target, key) => { @@ -111,7 +111,7 @@ function createConsole({ snapId }: EndowmentFactoryOptions = {}) { ...target, [key]: (message?: unknown, ...optionalParams: any[]) => { rootRealmGlobal.console[key]( - ...getMessage(snapId, message, ...optionalParams), + ...getMessage(sourceLabel, message, ...optionalParams), ); }, }; diff --git a/packages/snaps-execution-environments/src/common/endowments/endowments.test.browser.ts b/packages/snaps-execution-environments/src/common/endowments/endowments.test.browser.ts index 22d64d7b6d..1e768d79ff 100644 --- a/packages/snaps-execution-environments/src/common/endowments/endowments.test.browser.ts +++ b/packages/snaps-execution-environments/src/common/endowments/endowments.test.browser.ts @@ -43,7 +43,10 @@ describe('endowments', () => { const modules = buildCommonEndowments(); modules.forEach((endowment) => // @ts-expect-error: Partial mock. - endowment.factory({ snapId: MOCK_SNAP_ID, notify: mockNotify }), + endowment.factory({ + sourceLabel: `Snap: ${MOCK_SNAP_ID}`, + notify: mockNotify, + }), ); // Specially attenuated endowments or endowments that require @@ -70,7 +73,7 @@ describe('endowments', () => { }); const { Date: DateAttenuated } = date.factory(); const { console: consoleAttenuated } = consoleEndowment.factory({ - snapId: MOCK_SNAP_ID, + sourceLabel: `Snap: ${MOCK_SNAP_ID}`, }); const TEST_ENDOWMENTS = { diff --git a/packages/snaps-execution-environments/src/common/endowments/index.ts b/packages/snaps-execution-environments/src/common/endowments/index.ts index 30ebfa209a..da589ae55c 100644 --- a/packages/snaps-execution-environments/src/common/endowments/index.ts +++ b/packages/snaps-execution-environments/src/common/endowments/index.ts @@ -3,24 +3,14 @@ import type { SnapsEthereumProvider, SnapsProvider } from '@metamask/snaps-sdk'; import { logWarning } from '@metamask/snaps-utils'; import { hasProperty } from '@metamask/utils'; -import type { EndowmentFactoryOptions } from './commonEndowmentFactory'; +import type { + EndowmentFactoryOptions, + EndowmentFactoryResult, + NotifyFunction, +} from './commonEndowmentFactory'; import buildCommonEndowments from './commonEndowmentFactory'; -import type { NotifyFunction } from '../BaseSnapExecutor'; import { rootRealmGlobal } from '../globalObject'; -type EndowmentFactoryResult = { - /** - * A function that performs any necessary teardown when the snap becomes idle. - * - * NOTE:** The endowments are not reconstructed if the snap is re-invoked - * before being terminated, so the teardown operation must not render the - * endowments unusable; it should simply restore the endowments to their - * original state. - */ - teardownFunction?: () => Promise | void; - [key: string]: unknown; -}; - /** * Retrieve consolidated endowment factories for common endowments. */ @@ -89,7 +79,7 @@ export function createEndowments({ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const { teardownFunction, ...endowment } = endowmentFactories.get( endowmentName, - )!({ snapId, notify }); + )!({ sourceLabel: `Snap: ${snapId}`, notify }); Object.assign(attenuatedEndowments, endowment); if (teardownFunction) { teardowns.push(teardownFunction); diff --git a/packages/snaps-execution-environments/src/endowments.ts b/packages/snaps-execution-environments/src/endowments.ts index aa74ef0928..199c7b3b9b 100644 --- a/packages/snaps-execution-environments/src/endowments.ts +++ b/packages/snaps-execution-environments/src/endowments.ts @@ -27,3 +27,15 @@ export { default as textEncoder } from './common/endowments/textEncoder'; export { default as textDecoder } from './common/endowments/textDecoder'; export { default as crypto } from './common/endowments/crypto'; export { default as math } from './common/endowments/math'; +export { default as consoleEndowment } from './common/endowments/console'; +export { default as network } from './common/endowments/network'; + +// Consolidated factory builder and types +export { default as buildCommonEndowments } from './common/endowments/commonEndowmentFactory'; +export type { + NotifyFunction, + EndowmentFactoryOptions, + EndowmentFactoryResult, + EndowmentFactory, + CommonEndowmentSpecification, +} from './common/endowments/commonEndowmentFactory'; From c006b2875368f4b1fce26116cd9f744e19072938 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Thu, 16 Apr 2026 16:10:08 +0200 Subject: [PATCH 05/11] fix: Improve public endowment API based on review - Narrow console/network factory types to require their options at compile time (sourceLabel and notify respectively) - Add descriptive assert messages for runtime safety (JS consumers) - Remove CommonEndowmentSpecification from public exports (internal detail) - Add SES lockdown() prerequisite to module JSDoc - Fix stale Snap-specific language in comments and JSDoc - Fix "Create a a" typo in console.ts - Update changelog to list all exported modules and types - Add barrel smoke test verifying all re-exports - Add tests for non-Snap labels and missing sourceLabel Co-Authored-By: Claude Opus 4.6 (1M context) --- .../snaps-execution-environments/CHANGELOG.md | 3 +- .../endowments/commonEndowmentFactory.ts | 19 ++++---- .../src/common/endowments/console.test.ts | 28 ++++++++--- .../src/common/endowments/console.ts | 18 ++++++-- .../src/common/endowments/network.ts | 18 ++++++-- .../src/endowments.test.ts | 46 +++++++++++++++++++ .../src/endowments.ts | 9 ++-- 7 files changed, 113 insertions(+), 28 deletions(-) create mode 100644 packages/snaps-execution-environments/src/endowments.test.ts diff --git a/packages/snaps-execution-environments/CHANGELOG.md b/packages/snaps-execution-environments/CHANGELOG.md index 1bb5612594..ed274d587d 100644 --- a/packages/snaps-execution-environments/CHANGELOG.md +++ b/packages/snaps-execution-environments/CHANGELOG.md @@ -10,7 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Export endowment factories via `@metamask/snaps-execution-environments/endowments` sub-path ([#3957](https://github.com/MetaMask/snaps/pull/3957)) - - Exports endowment factory modules: `timeout`, `interval`, `date`, `textEncoder`, `textDecoder`, `crypto`, `math` + - Exports endowment factory modules: `timeout`, `interval`, `date`, `textEncoder`, `textDecoder`, `crypto`, `math`, `consoleEndowment`, `network` + - Exports `buildCommonEndowments` and types: `EndowmentFactory`, `EndowmentFactoryOptions`, `EndowmentFactoryResult`, `NotifyFunction` ## [11.0.2] diff --git a/packages/snaps-execution-environments/src/common/endowments/commonEndowmentFactory.ts b/packages/snaps-execution-environments/src/common/endowments/commonEndowmentFactory.ts index db17e5b8fa..cf167b5eda 100644 --- a/packages/snaps-execution-environments/src/common/endowments/commonEndowmentFactory.ts +++ b/packages/snaps-execution-environments/src/common/endowments/commonEndowmentFactory.ts @@ -24,9 +24,9 @@ export type NotifyFunction = ( */ export type EndowmentFactoryOptions = { /** - * A label identifying the source of endowment interactions (e.g., console - * output). The caller controls the format — Snaps passes `Snap: ${snapId}`, - * but external consumers may use any label. + * A label identifying the source of endowment interactions, used as a + * prefix in console output. For example, passing `"MyApp"` causes console + * messages to be prefixed with `[MyApp]`. */ sourceLabel?: string; @@ -44,9 +44,10 @@ export type EndowmentFactoryOptions = { */ export type EndowmentFactoryResult = { /** - * An optional function that performs cleanup when the execution environment - * becomes idle. Must not render endowments unusable — only restore them to - * their initial state, since they may be reused without reconstruction. + * An optional function that performs cleanup when active resources (e.g., + * pending timers or open network connections) should be released. Must not + * render endowments unusable — only restore them to their initial state, + * since they may be reused without reconstruction. */ teardownFunction?: () => Promise | void; [key: string]: unknown; @@ -106,16 +107,18 @@ const commonEndowments: CommonEndowmentSpecification[] = [ * @returns An object with common endowments. */ const buildCommonEndowments = (): EndowmentFactory[] => { + // Console and network have narrower option types for their public API, + // but are widened here for internal dispatch via EndowmentFactory[]. const endowmentFactories: EndowmentFactory[] = [ crypto, interval, math, - network, + network as EndowmentFactory, timeout, textDecoder, textEncoder, date, - consoleEndowment, + consoleEndowment as EndowmentFactory, ]; commonEndowments.forEach((endowmentSpecification) => { diff --git a/packages/snaps-execution-environments/src/common/endowments/console.test.ts b/packages/snaps-execution-environments/src/common/endowments/console.test.ts index 498cc170c6..7faffac67b 100644 --- a/packages/snaps-execution-environments/src/common/endowments/console.test.ts +++ b/packages/snaps-execution-environments/src/common/endowments/console.test.ts @@ -39,7 +39,7 @@ describe('Console endowment', () => { expect(console.log).not.toStrictEqual(rootRealmGlobal.console.log); }); - it('will log a message identifying the source of the call (snap id)', () => { + it('prefixes output with the source label', () => { const { console } = consoleEndowment.factory({ sourceLabel: `Snap: ${MOCK_SNAP_ID}`, }); @@ -78,7 +78,7 @@ describe('Console endowment', () => { expect(console.error).not.toStrictEqual(rootRealmGlobal.console.error); }); - it('will log a message identifying the source of the call (snap id)', () => { + it('prefixes output with the source label', () => { const { console } = consoleEndowment.factory({ sourceLabel: `Snap: ${MOCK_SNAP_ID}`, }); @@ -99,7 +99,7 @@ describe('Console endowment', () => { expect(console.assert).not.toStrictEqual(rootRealmGlobal.console.assert); }); - it('will log a message identifying the source of the call (snap id)', () => { + it('prefixes output with the source label', () => { const { console } = consoleEndowment.factory({ sourceLabel: `Snap: ${MOCK_SNAP_ID}`, }); @@ -121,7 +121,7 @@ describe('Console endowment', () => { expect(console.debug).not.toStrictEqual(rootRealmGlobal.console.debug); }); - it('will log a message identifying the source of the call (snap id)', () => { + it('prefixes output with the source label', () => { const { console } = consoleEndowment.factory({ sourceLabel: `Snap: ${MOCK_SNAP_ID}`, }); @@ -142,7 +142,7 @@ describe('Console endowment', () => { expect(console.info).not.toStrictEqual(rootRealmGlobal.console.info); }); - it('will log a message identifying the source of the call (snap id)', () => { + it('prefixes output with the source label', () => { const { console } = consoleEndowment.factory({ sourceLabel: `Snap: ${MOCK_SNAP_ID}`, }); @@ -163,7 +163,7 @@ describe('Console endowment', () => { expect(console.warn).not.toStrictEqual(rootRealmGlobal.console.warn); }); - it('will log a message identifying the source of the call (snap id)', () => { + it('prefixes output with the source label', () => { const { console } = consoleEndowment.factory({ sourceLabel: `Snap: ${MOCK_SNAP_ID}`, }); @@ -175,4 +175,20 @@ describe('Console endowment', () => { ); }); }); + + it('throws when sourceLabel is not provided', () => { + // @ts-expect-error: Testing runtime guard for missing required option. + expect(() => consoleEndowment.factory({})).toThrow( + 'The "sourceLabel" option is required by the console endowment factory.', + ); + }); + + it('supports arbitrary source labels for non-Snap consumers', () => { + const { console } = consoleEndowment.factory({ + sourceLabel: 'ocap-kernel: vat-42', + }); + const logSpy = jest.spyOn(rootRealmGlobal.console, 'log'); + console.log('test message'); + expect(logSpy).toHaveBeenCalledWith('[ocap-kernel: vat-42] test message'); + }); }); diff --git a/packages/snaps-execution-environments/src/common/endowments/console.ts b/packages/snaps-execution-environments/src/common/endowments/console.ts index 11fc5f5b76..b9ae4e892c 100644 --- a/packages/snaps-execution-environments/src/common/endowments/console.ts +++ b/packages/snaps-execution-environments/src/common/endowments/console.ts @@ -3,6 +3,10 @@ import { assert } from '@metamask/utils'; import type { EndowmentFactoryOptions } from './commonEndowmentFactory'; import { rootRealmGlobal } from '../globalObject'; +type ConsoleEndowmentOptions = Required< + Pick +>; + export const consoleAttenuatedMethods = new Set([ 'log', 'assert', @@ -13,8 +17,9 @@ export const consoleAttenuatedMethods = new Set([ ]); /** - * A set of all the `console` values that will be passed to the snap. This has - * all the values that are available in both the browser and Node.js. + * A set of all the `console` method names that will be included in the + * attenuated console object. Covers values available in both browser and + * Node.js. */ export const consoleMethods = new Set([ 'debug', @@ -72,15 +77,18 @@ function getMessage(sourceLabel: string, message: unknown, ...args: unknown[]) { } /** - * Create a a {@link console} object, with the same properties as the global + * Create a {@link console} object, with the same properties as the global * {@link console} object, but with some methods replaced. * * @param options - Factory options used in construction of the endowment. * @param options.sourceLabel - Label identifying the source of the console call. * @returns The {@link console} object with the replaced methods. */ -function createConsole({ sourceLabel }: EndowmentFactoryOptions = {}) { - assert(sourceLabel !== undefined); +function createConsole({ sourceLabel }: ConsoleEndowmentOptions) { + assert( + sourceLabel !== undefined, + 'The "sourceLabel" option is required by the console endowment factory.', + ); const keys = Object.getOwnPropertyNames( rootRealmGlobal.console, ) as (keyof typeof console)[]; diff --git a/packages/snaps-execution-environments/src/common/endowments/network.ts b/packages/snaps-execution-environments/src/common/endowments/network.ts index 2cec5f82f7..da0643a7bd 100644 --- a/packages/snaps-execution-environments/src/common/endowments/network.ts +++ b/packages/snaps-execution-environments/src/common/endowments/network.ts @@ -3,6 +3,10 @@ import { assert } from '@metamask/utils'; import type { EndowmentFactoryOptions } from './commonEndowmentFactory'; import { withTeardown } from '../utils'; +type NetworkEndowmentOptions = Required< + Pick +>; + /** * This class wraps a Response object. * That way, a teardown process can stop any processes left. @@ -157,20 +161,24 @@ class AlteredResponse extends Response { /** * Create a network endowment, consisting of a `fetch` function. * This allows us to provide a teardown function, so that we can cancel - * any pending requests, connections, streams, etc. that may be open when a snap - * is terminated. + * any pending requests, connections, streams, etc. that may be open when the + * execution context is torn down. * * This wraps the original implementation of `fetch`, * to ensure that a bad actor cannot get access to the original function, thus * potentially preventing the network requests from being torn down. * * @param options - An options bag. - * @param options.notify - A reference to the notify function of the snap executor. + * @param options.notify - A notification callback for outbound request + * lifecycle events. * @returns An object containing a wrapped `fetch` * function, as well as a teardown function. */ -const createNetwork = ({ notify }: EndowmentFactoryOptions = {}) => { - assert(notify, 'Notify must be passed to network endowment factory'); +const createNetwork = ({ notify }: NetworkEndowmentOptions) => { + assert( + notify, + 'The "notify" callback is required by the network endowment factory.', + ); // Open fetch calls or open body streams const openConnections = new Set<{ cancel: () => Promise }>(); // Track last teardown count diff --git a/packages/snaps-execution-environments/src/endowments.test.ts b/packages/snaps-execution-environments/src/endowments.test.ts new file mode 100644 index 0000000000..1a9ba4bd49 --- /dev/null +++ b/packages/snaps-execution-environments/src/endowments.test.ts @@ -0,0 +1,46 @@ +import { + timeout, + interval, + date, + textEncoder, + textDecoder, + crypto, + math, + consoleEndowment, + network, + buildCommonEndowments, +} from './endowments'; + +describe('endowments barrel', () => { + it.each([ + { name: 'timeout', module: timeout, expectedName: 'setTimeout' }, + { name: 'interval', module: interval, expectedName: 'setInterval' }, + { name: 'date', module: date, expectedName: 'Date' }, + { name: 'textEncoder', module: textEncoder, expectedName: 'TextEncoder' }, + { name: 'textDecoder', module: textDecoder, expectedName: 'TextDecoder' }, + { name: 'crypto', module: crypto, expectedName: 'crypto' }, + { name: 'math', module: math, expectedName: 'Math' }, + { + name: 'consoleEndowment', + module: consoleEndowment, + expectedName: 'console', + }, + { name: 'network', module: network, expectedName: 'fetch' }, + ])('exports $name with names and factory', ({ module, expectedName }) => { + expect(module).toHaveProperty('names'); + expect(module).toHaveProperty('factory'); + expect(module.names).toContain(expectedName); + expect(typeof module.factory).toBe('function'); + }); + + it('exports buildCommonEndowments', () => { + expect(typeof buildCommonEndowments).toBe('function'); + const factories = buildCommonEndowments(); + expect(Array.isArray(factories)).toBe(true); + expect(factories.length).toBeGreaterThan(0); + factories.forEach((factory) => { + expect(factory).toHaveProperty('names'); + expect(factory).toHaveProperty('factory'); + }); + }); +}); diff --git a/packages/snaps-execution-environments/src/endowments.ts b/packages/snaps-execution-environments/src/endowments.ts index 199c7b3b9b..4b59f9521d 100644 --- a/packages/snaps-execution-environments/src/endowments.ts +++ b/packages/snaps-execution-environments/src/endowments.ts @@ -1,9 +1,13 @@ /** * Public endowment factory exports for use outside the Snaps ecosystem. * + * **Prerequisite**: These factories call the SES `harden()` global internally. + * The consuming environment must have loaded SES and called `lockdown()` before + * invoking any factory function. + * * Each module provides a `names` array and a `factory` function. Call * `factory()` to obtain hardened endowment values (and an optional - * `teardownFunction` for timer-based endowments). + * `teardownFunction` for stateful endowments that manage resources). * * @example * ```ts @@ -13,7 +17,7 @@ * // { setTimeout, clearTimeout, teardownFunction } * * const dateEndowment = date.factory(); - * // { Date } (with noise-added Date.now) + * // { Date } (with attenuated Date.now) * ``` * * @module endowments @@ -37,5 +41,4 @@ export type { EndowmentFactoryOptions, EndowmentFactoryResult, EndowmentFactory, - CommonEndowmentSpecification, } from './common/endowments/commonEndowmentFactory'; From c3ef4af3c31ec4ceb4cb166682d4aef7baf20a3a Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Thu, 16 Apr 2026 16:11:53 +0200 Subject: [PATCH 06/11] fix: Broaden NotifyFunction and EndowmentFactoryResult JSDoc Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/common/endowments/commonEndowmentFactory.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/snaps-execution-environments/src/common/endowments/commonEndowmentFactory.ts b/packages/snaps-execution-environments/src/common/endowments/commonEndowmentFactory.ts index cf167b5eda..a376182fdd 100644 --- a/packages/snaps-execution-environments/src/common/endowments/commonEndowmentFactory.ts +++ b/packages/snaps-execution-environments/src/common/endowments/commonEndowmentFactory.ts @@ -13,7 +13,8 @@ import { rootRealmGlobal } from '../globalObject'; /** * A function for sending JSON-RPC notifications from an endowment. - * Used by the network endowment to signal outbound request lifecycle events. + * Used by endowments that perform outbound operations (e.g., network `fetch`) + * to signal request lifecycle events. */ export type NotifyFunction = ( notification: Omit, @@ -38,9 +39,9 @@ export type EndowmentFactoryOptions = { }; /** - * The object returned by an endowment factory. Contains the named endowment - * values (keyed by name) and an optional teardown function for lifecycle - * management. + * The object returned by an endowment factory. Contains the endowment values + * keyed by their global name (e.g., `setTimeout`, `Date`) and an optional + * teardown function for lifecycle management. */ export type EndowmentFactoryResult = { /** From 242ee5452a7cc2ff1a3fde329b20324ce47f151a Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Thu, 16 Apr 2026 16:21:35 +0200 Subject: [PATCH 07/11] chore: Update branch coverage threshold to 90.95% New endowment tests raised branch coverage above the previous threshold. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/snaps-execution-environments/coverage.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/snaps-execution-environments/coverage.json b/packages/snaps-execution-environments/coverage.json index 6648270c0a..5a2b895413 100644 --- a/packages/snaps-execution-environments/coverage.json +++ b/packages/snaps-execution-environments/coverage.json @@ -1,5 +1,5 @@ { - "branches": 90.09, + "branches": 90.95, "functions": 95.34, "lines": 92.74, "statements": 91.69 From 49b701cbf4862258d467701a868de5bc7f94f200 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Thu, 16 Apr 2026 16:58:39 +0200 Subject: [PATCH 08/11] fix: Restore default params so assert fires instead of TypeError When called without arguments (e.g., iterating buildCommonEndowments()), the descriptive assert message now fires instead of a generic TypeError from destructuring undefined. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/common/endowments/console.test.ts | 4 ++++ .../src/common/endowments/console.ts | 4 +++- .../src/common/endowments/network.ts | 4 +++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/snaps-execution-environments/src/common/endowments/console.test.ts b/packages/snaps-execution-environments/src/common/endowments/console.test.ts index 7faffac67b..bb68fcc8b7 100644 --- a/packages/snaps-execution-environments/src/common/endowments/console.test.ts +++ b/packages/snaps-execution-environments/src/common/endowments/console.test.ts @@ -177,6 +177,10 @@ describe('Console endowment', () => { }); it('throws when sourceLabel is not provided', () => { + expect(() => consoleEndowment.factory()).toThrow( + 'The "sourceLabel" option is required by the console endowment factory.', + ); + // @ts-expect-error: Testing runtime guard for missing required option. expect(() => consoleEndowment.factory({})).toThrow( 'The "sourceLabel" option is required by the console endowment factory.', diff --git a/packages/snaps-execution-environments/src/common/endowments/console.ts b/packages/snaps-execution-environments/src/common/endowments/console.ts index b9ae4e892c..4cee451637 100644 --- a/packages/snaps-execution-environments/src/common/endowments/console.ts +++ b/packages/snaps-execution-environments/src/common/endowments/console.ts @@ -84,7 +84,9 @@ function getMessage(sourceLabel: string, message: unknown, ...args: unknown[]) { * @param options.sourceLabel - Label identifying the source of the console call. * @returns The {@link console} object with the replaced methods. */ -function createConsole({ sourceLabel }: ConsoleEndowmentOptions) { +function createConsole( + { sourceLabel }: ConsoleEndowmentOptions = {} as ConsoleEndowmentOptions, +) { assert( sourceLabel !== undefined, 'The "sourceLabel" option is required by the console endowment factory.', diff --git a/packages/snaps-execution-environments/src/common/endowments/network.ts b/packages/snaps-execution-environments/src/common/endowments/network.ts index da0643a7bd..4310f52c0f 100644 --- a/packages/snaps-execution-environments/src/common/endowments/network.ts +++ b/packages/snaps-execution-environments/src/common/endowments/network.ts @@ -174,7 +174,9 @@ class AlteredResponse extends Response { * @returns An object containing a wrapped `fetch` * function, as well as a teardown function. */ -const createNetwork = ({ notify }: NetworkEndowmentOptions) => { +const createNetwork = ( + { notify }: NetworkEndowmentOptions = {} as NetworkEndowmentOptions, +) => { assert( notify, 'The "notify" callback is required by the network endowment factory.', From 892d09ebebc5616fd428a6294454bfb4b8df1682 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Thu, 16 Apr 2026 18:09:51 +0200 Subject: [PATCH 09/11] test: Add coverage for uncovered endowment branches - Test network factory no-args path (descriptive assert message) - Test timeout with undefined delay (covers ?? 0 branch) - Test interval with undefined delay (covers ?? 0 branch) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../snaps-execution-environments/coverage.json | 2 +- .../src/common/endowments/interval.test.ts | 15 +++++++++++++++ .../src/common/endowments/network.test.ts | 6 ++++++ .../src/common/endowments/timeout.test.ts | 10 ++++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/snaps-execution-environments/coverage.json b/packages/snaps-execution-environments/coverage.json index 5a2b895413..dcac70e0ba 100644 --- a/packages/snaps-execution-environments/coverage.json +++ b/packages/snaps-execution-environments/coverage.json @@ -1,5 +1,5 @@ { - "branches": 90.95, + "branches": 91, "functions": 95.34, "lines": 92.74, "statements": 91.69 diff --git a/packages/snaps-execution-environments/src/common/endowments/interval.test.ts b/packages/snaps-execution-environments/src/common/endowments/interval.test.ts index 87995f5cad..d59634b3fd 100644 --- a/packages/snaps-execution-environments/src/common/endowments/interval.test.ts +++ b/packages/snaps-execution-environments/src/common/endowments/interval.test.ts @@ -71,6 +71,21 @@ describe('Interval endowments', () => { expect(await promise).toBeUndefined(); }); + it('should default to minimum interval when timeout is undefined', async () => { + const { setInterval: _setInterval, clearInterval: _clearInterval } = + interval.factory(); + + const promise = new Promise((resolve) => { + const handle = _setInterval(() => { + _clearInterval(handle); + resolve(undefined); + }, undefined); + }); + + jest.advanceTimersByTime(100); + expect(await promise).toBeUndefined(); + }); + it('the attenuated setInterval should throw if passed a non-function', () => { const { setInterval: _setInterval } = interval.factory(); diff --git a/packages/snaps-execution-environments/src/common/endowments/network.test.ts b/packages/snaps-execution-environments/src/common/endowments/network.test.ts index 4085356c63..734fbee700 100644 --- a/packages/snaps-execution-environments/src/common/endowments/network.test.ts +++ b/packages/snaps-execution-environments/src/common/endowments/network.test.ts @@ -200,4 +200,10 @@ describe('Network endowments', () => { expect(await result.json()).toStrictEqual({}); }); }); + + it('throws when notify is not provided', () => { + expect(() => network.factory()).toThrow( + 'The "notify" callback is required by the network endowment factory.', + ); + }); }); diff --git a/packages/snaps-execution-environments/src/common/endowments/timeout.test.ts b/packages/snaps-execution-environments/src/common/endowments/timeout.test.ts index 3a2c4e0797..6998d9e8e7 100644 --- a/packages/snaps-execution-environments/src/common/endowments/timeout.test.ts +++ b/packages/snaps-execution-environments/src/common/endowments/timeout.test.ts @@ -61,6 +61,16 @@ describe('Timeout endowments', () => { ).toBeUndefined(); }, 200); + it('should default to minimum timeout when timeout is undefined', async () => { + const { setTimeout: _setTimeout } = timeout.factory(); + + expect( + await new Promise((resolve) => { + _setTimeout(resolve, undefined); + }), + ).toBeUndefined(); + }, 300); + it('the attenuated setTimeout should throw if passed a non-function', () => { const { setTimeout: _setTimeout } = timeout.factory(); From d7b28fb415c720acf0198bb8c28cb34229d0424c Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Thu, 16 Apr 2026 19:33:12 +0200 Subject: [PATCH 10/11] chore: Update branch coverage threshold to 92.03% Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/snaps-execution-environments/coverage.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/snaps-execution-environments/coverage.json b/packages/snaps-execution-environments/coverage.json index dcac70e0ba..d2d000e729 100644 --- a/packages/snaps-execution-environments/coverage.json +++ b/packages/snaps-execution-environments/coverage.json @@ -1,5 +1,5 @@ { - "branches": 91, + "branches": 92.03, "functions": 95.34, "lines": 92.74, "statements": 91.69 From 92bc03812613cf8c9f77d22e38c9effc8691fa59 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Thu, 16 Apr 2026 19:51:41 +0200 Subject: [PATCH 11/11] refactor: Remove type casts from endowment factory implementations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use wide EndowmentFactoryOptions inside createConsole and createNetwork so no default-value casts are needed. Apply narrow types (ConsoleEndowmentOptions, NetworkEndowmentOptions) at the endowments.ts barrel re-export — wide-to-narrow factory assignment is type-safe via TypeScript's contravariance rules. External consumers importing from @metamask/snaps-execution-environments/endowments get compile-time errors when required options are missing: - consoleEndowment.factory({}) → Property 'sourceLabel' is missing - network.factory({}) → Property 'notify' is missing Co-Authored-By: Claude Opus 4.7 (1M context) --- .../endowments/commonEndowmentFactory.ts | 6 +-- .../src/common/endowments/console.test.ts | 1 - .../src/common/endowments/console.ts | 8 +--- .../src/common/endowments/network.ts | 8 +--- .../src/endowments.ts | 43 +++++++++++++++++-- 5 files changed, 44 insertions(+), 22 deletions(-) diff --git a/packages/snaps-execution-environments/src/common/endowments/commonEndowmentFactory.ts b/packages/snaps-execution-environments/src/common/endowments/commonEndowmentFactory.ts index a376182fdd..b5dcb560e7 100644 --- a/packages/snaps-execution-environments/src/common/endowments/commonEndowmentFactory.ts +++ b/packages/snaps-execution-environments/src/common/endowments/commonEndowmentFactory.ts @@ -108,18 +108,16 @@ const commonEndowments: CommonEndowmentSpecification[] = [ * @returns An object with common endowments. */ const buildCommonEndowments = (): EndowmentFactory[] => { - // Console and network have narrower option types for their public API, - // but are widened here for internal dispatch via EndowmentFactory[]. const endowmentFactories: EndowmentFactory[] = [ crypto, interval, math, - network as EndowmentFactory, + network, timeout, textDecoder, textEncoder, date, - consoleEndowment as EndowmentFactory, + consoleEndowment, ]; commonEndowments.forEach((endowmentSpecification) => { diff --git a/packages/snaps-execution-environments/src/common/endowments/console.test.ts b/packages/snaps-execution-environments/src/common/endowments/console.test.ts index bb68fcc8b7..c7dd593009 100644 --- a/packages/snaps-execution-environments/src/common/endowments/console.test.ts +++ b/packages/snaps-execution-environments/src/common/endowments/console.test.ts @@ -181,7 +181,6 @@ describe('Console endowment', () => { 'The "sourceLabel" option is required by the console endowment factory.', ); - // @ts-expect-error: Testing runtime guard for missing required option. expect(() => consoleEndowment.factory({})).toThrow( 'The "sourceLabel" option is required by the console endowment factory.', ); diff --git a/packages/snaps-execution-environments/src/common/endowments/console.ts b/packages/snaps-execution-environments/src/common/endowments/console.ts index 4cee451637..b0f05b90e8 100644 --- a/packages/snaps-execution-environments/src/common/endowments/console.ts +++ b/packages/snaps-execution-environments/src/common/endowments/console.ts @@ -3,10 +3,6 @@ import { assert } from '@metamask/utils'; import type { EndowmentFactoryOptions } from './commonEndowmentFactory'; import { rootRealmGlobal } from '../globalObject'; -type ConsoleEndowmentOptions = Required< - Pick ->; - export const consoleAttenuatedMethods = new Set([ 'log', 'assert', @@ -84,9 +80,7 @@ function getMessage(sourceLabel: string, message: unknown, ...args: unknown[]) { * @param options.sourceLabel - Label identifying the source of the console call. * @returns The {@link console} object with the replaced methods. */ -function createConsole( - { sourceLabel }: ConsoleEndowmentOptions = {} as ConsoleEndowmentOptions, -) { +function createConsole({ sourceLabel }: EndowmentFactoryOptions = {}) { assert( sourceLabel !== undefined, 'The "sourceLabel" option is required by the console endowment factory.', diff --git a/packages/snaps-execution-environments/src/common/endowments/network.ts b/packages/snaps-execution-environments/src/common/endowments/network.ts index 4310f52c0f..b8000616e6 100644 --- a/packages/snaps-execution-environments/src/common/endowments/network.ts +++ b/packages/snaps-execution-environments/src/common/endowments/network.ts @@ -3,10 +3,6 @@ import { assert } from '@metamask/utils'; import type { EndowmentFactoryOptions } from './commonEndowmentFactory'; import { withTeardown } from '../utils'; -type NetworkEndowmentOptions = Required< - Pick ->; - /** * This class wraps a Response object. * That way, a teardown process can stop any processes left. @@ -174,9 +170,7 @@ class AlteredResponse extends Response { * @returns An object containing a wrapped `fetch` * function, as well as a teardown function. */ -const createNetwork = ( - { notify }: NetworkEndowmentOptions = {} as NetworkEndowmentOptions, -) => { +const createNetwork = ({ notify }: EndowmentFactoryOptions = {}) => { assert( notify, 'The "notify" callback is required by the network endowment factory.', diff --git a/packages/snaps-execution-environments/src/endowments.ts b/packages/snaps-execution-environments/src/endowments.ts index 4b59f9521d..51806eb1d6 100644 --- a/packages/snaps-execution-environments/src/endowments.ts +++ b/packages/snaps-execution-environments/src/endowments.ts @@ -23,7 +23,14 @@ * @module endowments */ -// Individual endowment factory modules +import type { + EndowmentFactoryOptions, + EndowmentFactoryResult, +} from './common/endowments/commonEndowmentFactory'; +import consoleEndowmentModule from './common/endowments/console'; +import networkModule from './common/endowments/network'; + +// Individual endowment factory modules with no required options export { default as timeout } from './common/endowments/timeout'; export { default as interval } from './common/endowments/interval'; export { default as date } from './common/endowments/date'; @@ -31,8 +38,38 @@ export { default as textEncoder } from './common/endowments/textEncoder'; export { default as textDecoder } from './common/endowments/textDecoder'; export { default as crypto } from './common/endowments/crypto'; export { default as math } from './common/endowments/math'; -export { default as consoleEndowment } from './common/endowments/console'; -export { default as network } from './common/endowments/network'; + +/** + * Options required by the console endowment factory. + */ +export type ConsoleEndowmentOptions = Required< + Pick +>; + +/** + * Options required by the network endowment factory. + */ +export type NetworkEndowmentOptions = Required< + Pick +>; + +/** + * The console endowment factory. Produces an attenuated `console` object that + * prefixes output with the provided source label. + */ +export const consoleEndowment: { + readonly names: readonly ['console']; + factory: (options: ConsoleEndowmentOptions) => EndowmentFactoryResult; +} = consoleEndowmentModule; + +/** + * The network endowment factory. Produces a wrapped `fetch` function and + * related types with teardown support. + */ +export const network: { + readonly names: readonly ['fetch', 'Request', 'Headers', 'Response']; + factory: (options: NetworkEndowmentOptions) => EndowmentFactoryResult; +} = networkModule; // Consolidated factory builder and types export { default as buildCommonEndowments } from './common/endowments/commonEndowmentFactory';