diff --git a/packages/snaps-execution-environments/CHANGELOG.md b/packages/snaps-execution-environments/CHANGELOG.md index 8e9ebbffe5..69c939bf73 100644 --- a/packages/snaps-execution-environments/CHANGELOG.md +++ b/packages/snaps-execution-environments/CHANGELOG.md @@ -7,6 +7,10 @@ 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` ([#3957](https://github.com/MetaMask/snaps/pull/3957)) + ## [11.0.2] ### Changed diff --git a/packages/snaps-execution-environments/coverage.json b/packages/snaps-execution-environments/coverage.json index 6648270c0a..d2d000e729 100644 --- a/packages/snaps-execution-environments/coverage.json +++ b/packages/snaps-execution-environments/coverage.json @@ -1,5 +1,5 @@ { - "branches": 90.09, + "branches": 92.03, "functions": 95.34, "lines": 92.74, "statements": 91.69 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/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..b5dcb560e7 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,64 @@ 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 endowments that perform outbound operations (e.g., network `fetch`) + * to signal 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, used as a + * prefix in console output. For example, passing `"MyApp"` causes console + * messages to be prefixed with `[MyApp]`. + */ + 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 endowment values + * keyed by their global name (e.g., `setTimeout`, `Date`) and an optional + * teardown function for lifecycle management. + */ +export type EndowmentFactoryResult = { + /** + * 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; +}; + +/** + * 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..c7dd593009 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 }); + it('prefixes output with the source label', () => { + 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 }); + it('prefixes output with the source label', () => { + 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 }); + it('prefixes output with the source label', () => { + 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 }); + it('prefixes output with the source label', () => { + 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 }); + it('prefixes output with the source label', () => { + 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 }); + it('prefixes output with the source label', () => { + 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); @@ -149,4 +175,23 @@ describe('Console endowment', () => { ); }); }); + + it('throws when sourceLabel is not provided', () => { + expect(() => consoleEndowment.factory()).toThrow( + 'The "sourceLabel" option is required by the console endowment factory.', + ); + + 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 dd3c394bda..b0f05b90e8 100644 --- a/packages/snaps-execution-environments/src/common/endowments/console.ts +++ b/packages/snaps-execution-environments/src/common/endowments/console.ts @@ -13,8 +13,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', @@ -52,13 +53,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. @@ -72,15 +73,18 @@ function getMessage(snapId: 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.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, + 'The "sourceLabel" option is required by the console endowment factory.', + ); const keys = Object.getOwnPropertyNames( rootRealmGlobal.console, ) as (keyof typeof console)[]; @@ -103,7 +107,7 @@ function createConsole({ snapId }: EndowmentFactoryOptions = {}) { ) => { rootRealmGlobal.console.assert( value, - ...getMessage(snapId, message, ...optionalParams), + ...getMessage(sourceLabel, message, ...optionalParams), ); }, ...consoleFunctions.reduce((target, key) => { @@ -111,7 +115,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/common/endowments/interval.test.ts b/packages/snaps-execution-environments/src/common/endowments/interval.test.ts index 87995f5cad..a695909d16 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('foo'); + }, undefined); + }); + + jest.advanceTimersByTime(10); + expect(await promise).toBe('foo'); + }); + 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/network.ts b/packages/snaps-execution-environments/src/common/endowments/network.ts index 2cec5f82f7..b8000616e6 100644 --- a/packages/snaps-execution-environments/src/common/endowments/network.ts +++ b/packages/snaps-execution-environments/src/common/endowments/network.ts @@ -157,20 +157,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'); + 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/common/endowments/timeout.test.ts b/packages/snaps-execution-environments/src/common/endowments/timeout.test.ts index 3a2c4e0797..e07e1cad7b 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('foo'), undefined); + }), + ).toBe('foo'); + }, 300); + it('the attenuated setTimeout should throw if passed a non-function', () => { const { setTimeout: _setTimeout } = timeout.factory(); 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 new file mode 100644 index 0000000000..51806eb1d6 --- /dev/null +++ b/packages/snaps-execution-environments/src/endowments.ts @@ -0,0 +1,81 @@ +/** + * 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 stateful endowments that manage resources). + * + * @example + * ```ts + * import { timeout, date } from '@metamask/snaps-execution-environments/endowments'; + * + * const timers = timeout.factory(); + * // { setTimeout, clearTimeout, teardownFunction } + * + * const dateEndowment = date.factory(); + * // { Date } (with attenuated Date.now) + * ``` + * + * @module endowments + */ + +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'; +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'; + +/** + * 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'; +export type { + NotifyFunction, + EndowmentFactoryOptions, + EndowmentFactoryResult, + EndowmentFactory, +} from './common/endowments/commonEndowmentFactory';