Skip to content

Commit c006b28

Browse files
sirtimidclaude
andcommitted
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) <noreply@anthropic.com>
1 parent ecafea4 commit c006b28

7 files changed

Lines changed: 113 additions & 28 deletions

File tree

packages/snaps-execution-environments/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- Export endowment factories via `@metamask/snaps-execution-environments/endowments` sub-path ([#3957](https://github.com/MetaMask/snaps/pull/3957))
13-
- Exports endowment factory modules: `timeout`, `interval`, `date`, `textEncoder`, `textDecoder`, `crypto`, `math`
13+
- Exports endowment factory modules: `timeout`, `interval`, `date`, `textEncoder`, `textDecoder`, `crypto`, `math`, `consoleEndowment`, `network`
14+
- Exports `buildCommonEndowments` and types: `EndowmentFactory`, `EndowmentFactoryOptions`, `EndowmentFactoryResult`, `NotifyFunction`
1415

1516
## [11.0.2]
1617

packages/snaps-execution-environments/src/common/endowments/commonEndowmentFactory.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ export type NotifyFunction = (
2424
*/
2525
export type EndowmentFactoryOptions = {
2626
/**
27-
* A label identifying the source of endowment interactions (e.g., console
28-
* output). The caller controls the format — Snaps passes `Snap: ${snapId}`,
29-
* but external consumers may use any label.
27+
* A label identifying the source of endowment interactions, used as a
28+
* prefix in console output. For example, passing `"MyApp"` causes console
29+
* messages to be prefixed with `[MyApp]`.
3030
*/
3131
sourceLabel?: string;
3232

@@ -44,9 +44,10 @@ export type EndowmentFactoryOptions = {
4444
*/
4545
export type EndowmentFactoryResult = {
4646
/**
47-
* An optional function that performs cleanup when the execution environment
48-
* becomes idle. Must not render endowments unusable — only restore them to
49-
* their initial state, since they may be reused without reconstruction.
47+
* An optional function that performs cleanup when active resources (e.g.,
48+
* pending timers or open network connections) should be released. Must not
49+
* render endowments unusable — only restore them to their initial state,
50+
* since they may be reused without reconstruction.
5051
*/
5152
teardownFunction?: () => Promise<void> | void;
5253
[key: string]: unknown;
@@ -106,16 +107,18 @@ const commonEndowments: CommonEndowmentSpecification[] = [
106107
* @returns An object with common endowments.
107108
*/
108109
const buildCommonEndowments = (): EndowmentFactory[] => {
110+
// Console and network have narrower option types for their public API,
111+
// but are widened here for internal dispatch via EndowmentFactory[].
109112
const endowmentFactories: EndowmentFactory[] = [
110113
crypto,
111114
interval,
112115
math,
113-
network,
116+
network as EndowmentFactory,
114117
timeout,
115118
textDecoder,
116119
textEncoder,
117120
date,
118-
consoleEndowment,
121+
consoleEndowment as EndowmentFactory,
119122
];
120123

121124
commonEndowments.forEach((endowmentSpecification) => {

packages/snaps-execution-environments/src/common/endowments/console.test.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ describe('Console endowment', () => {
3939
expect(console.log).not.toStrictEqual(rootRealmGlobal.console.log);
4040
});
4141

42-
it('will log a message identifying the source of the call (snap id)', () => {
42+
it('prefixes output with the source label', () => {
4343
const { console } = consoleEndowment.factory({
4444
sourceLabel: `Snap: ${MOCK_SNAP_ID}`,
4545
});
@@ -78,7 +78,7 @@ describe('Console endowment', () => {
7878
expect(console.error).not.toStrictEqual(rootRealmGlobal.console.error);
7979
});
8080

81-
it('will log a message identifying the source of the call (snap id)', () => {
81+
it('prefixes output with the source label', () => {
8282
const { console } = consoleEndowment.factory({
8383
sourceLabel: `Snap: ${MOCK_SNAP_ID}`,
8484
});
@@ -99,7 +99,7 @@ describe('Console endowment', () => {
9999
expect(console.assert).not.toStrictEqual(rootRealmGlobal.console.assert);
100100
});
101101

102-
it('will log a message identifying the source of the call (snap id)', () => {
102+
it('prefixes output with the source label', () => {
103103
const { console } = consoleEndowment.factory({
104104
sourceLabel: `Snap: ${MOCK_SNAP_ID}`,
105105
});
@@ -121,7 +121,7 @@ describe('Console endowment', () => {
121121
expect(console.debug).not.toStrictEqual(rootRealmGlobal.console.debug);
122122
});
123123

124-
it('will log a message identifying the source of the call (snap id)', () => {
124+
it('prefixes output with the source label', () => {
125125
const { console } = consoleEndowment.factory({
126126
sourceLabel: `Snap: ${MOCK_SNAP_ID}`,
127127
});
@@ -142,7 +142,7 @@ describe('Console endowment', () => {
142142
expect(console.info).not.toStrictEqual(rootRealmGlobal.console.info);
143143
});
144144

145-
it('will log a message identifying the source of the call (snap id)', () => {
145+
it('prefixes output with the source label', () => {
146146
const { console } = consoleEndowment.factory({
147147
sourceLabel: `Snap: ${MOCK_SNAP_ID}`,
148148
});
@@ -163,7 +163,7 @@ describe('Console endowment', () => {
163163
expect(console.warn).not.toStrictEqual(rootRealmGlobal.console.warn);
164164
});
165165

166-
it('will log a message identifying the source of the call (snap id)', () => {
166+
it('prefixes output with the source label', () => {
167167
const { console } = consoleEndowment.factory({
168168
sourceLabel: `Snap: ${MOCK_SNAP_ID}`,
169169
});
@@ -175,4 +175,20 @@ describe('Console endowment', () => {
175175
);
176176
});
177177
});
178+
179+
it('throws when sourceLabel is not provided', () => {
180+
// @ts-expect-error: Testing runtime guard for missing required option.
181+
expect(() => consoleEndowment.factory({})).toThrow(
182+
'The "sourceLabel" option is required by the console endowment factory.',
183+
);
184+
});
185+
186+
it('supports arbitrary source labels for non-Snap consumers', () => {
187+
const { console } = consoleEndowment.factory({
188+
sourceLabel: 'ocap-kernel: vat-42',
189+
});
190+
const logSpy = jest.spyOn(rootRealmGlobal.console, 'log');
191+
console.log('test message');
192+
expect(logSpy).toHaveBeenCalledWith('[ocap-kernel: vat-42] test message');
193+
});
178194
});

packages/snaps-execution-environments/src/common/endowments/console.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { assert } from '@metamask/utils';
33
import type { EndowmentFactoryOptions } from './commonEndowmentFactory';
44
import { rootRealmGlobal } from '../globalObject';
55

6+
type ConsoleEndowmentOptions = Required<
7+
Pick<EndowmentFactoryOptions, 'sourceLabel'>
8+
>;
9+
610
export const consoleAttenuatedMethods = new Set([
711
'log',
812
'assert',
@@ -13,8 +17,9 @@ export const consoleAttenuatedMethods = new Set([
1317
]);
1418

1519
/**
16-
* A set of all the `console` values that will be passed to the snap. This has
17-
* all the values that are available in both the browser and Node.js.
20+
* A set of all the `console` method names that will be included in the
21+
* attenuated console object. Covers values available in both browser and
22+
* Node.js.
1823
*/
1924
export const consoleMethods = new Set([
2025
'debug',
@@ -72,15 +77,18 @@ function getMessage(sourceLabel: string, message: unknown, ...args: unknown[]) {
7277
}
7378

7479
/**
75-
* Create a a {@link console} object, with the same properties as the global
80+
* Create a {@link console} object, with the same properties as the global
7681
* {@link console} object, but with some methods replaced.
7782
*
7883
* @param options - Factory options used in construction of the endowment.
7984
* @param options.sourceLabel - Label identifying the source of the console call.
8085
* @returns The {@link console} object with the replaced methods.
8186
*/
82-
function createConsole({ sourceLabel }: EndowmentFactoryOptions = {}) {
83-
assert(sourceLabel !== undefined);
87+
function createConsole({ sourceLabel }: ConsoleEndowmentOptions) {
88+
assert(
89+
sourceLabel !== undefined,
90+
'The "sourceLabel" option is required by the console endowment factory.',
91+
);
8492
const keys = Object.getOwnPropertyNames(
8593
rootRealmGlobal.console,
8694
) as (keyof typeof console)[];

packages/snaps-execution-environments/src/common/endowments/network.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { assert } from '@metamask/utils';
33
import type { EndowmentFactoryOptions } from './commonEndowmentFactory';
44
import { withTeardown } from '../utils';
55

6+
type NetworkEndowmentOptions = Required<
7+
Pick<EndowmentFactoryOptions, 'notify'>
8+
>;
9+
610
/**
711
* This class wraps a Response object.
812
* That way, a teardown process can stop any processes left.
@@ -157,20 +161,24 @@ class AlteredResponse extends Response {
157161
/**
158162
* Create a network endowment, consisting of a `fetch` function.
159163
* This allows us to provide a teardown function, so that we can cancel
160-
* any pending requests, connections, streams, etc. that may be open when a snap
161-
* is terminated.
164+
* any pending requests, connections, streams, etc. that may be open when the
165+
* execution context is torn down.
162166
*
163167
* This wraps the original implementation of `fetch`,
164168
* to ensure that a bad actor cannot get access to the original function, thus
165169
* potentially preventing the network requests from being torn down.
166170
*
167171
* @param options - An options bag.
168-
* @param options.notify - A reference to the notify function of the snap executor.
172+
* @param options.notify - A notification callback for outbound request
173+
* lifecycle events.
169174
* @returns An object containing a wrapped `fetch`
170175
* function, as well as a teardown function.
171176
*/
172-
const createNetwork = ({ notify }: EndowmentFactoryOptions = {}) => {
173-
assert(notify, 'Notify must be passed to network endowment factory');
177+
const createNetwork = ({ notify }: NetworkEndowmentOptions) => {
178+
assert(
179+
notify,
180+
'The "notify" callback is required by the network endowment factory.',
181+
);
174182
// Open fetch calls or open body streams
175183
const openConnections = new Set<{ cancel: () => Promise<void> }>();
176184
// Track last teardown count
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {
2+
timeout,
3+
interval,
4+
date,
5+
textEncoder,
6+
textDecoder,
7+
crypto,
8+
math,
9+
consoleEndowment,
10+
network,
11+
buildCommonEndowments,
12+
} from './endowments';
13+
14+
describe('endowments barrel', () => {
15+
it.each([
16+
{ name: 'timeout', module: timeout, expectedName: 'setTimeout' },
17+
{ name: 'interval', module: interval, expectedName: 'setInterval' },
18+
{ name: 'date', module: date, expectedName: 'Date' },
19+
{ name: 'textEncoder', module: textEncoder, expectedName: 'TextEncoder' },
20+
{ name: 'textDecoder', module: textDecoder, expectedName: 'TextDecoder' },
21+
{ name: 'crypto', module: crypto, expectedName: 'crypto' },
22+
{ name: 'math', module: math, expectedName: 'Math' },
23+
{
24+
name: 'consoleEndowment',
25+
module: consoleEndowment,
26+
expectedName: 'console',
27+
},
28+
{ name: 'network', module: network, expectedName: 'fetch' },
29+
])('exports $name with names and factory', ({ module, expectedName }) => {
30+
expect(module).toHaveProperty('names');
31+
expect(module).toHaveProperty('factory');
32+
expect(module.names).toContain(expectedName);
33+
expect(typeof module.factory).toBe('function');
34+
});
35+
36+
it('exports buildCommonEndowments', () => {
37+
expect(typeof buildCommonEndowments).toBe('function');
38+
const factories = buildCommonEndowments();
39+
expect(Array.isArray(factories)).toBe(true);
40+
expect(factories.length).toBeGreaterThan(0);
41+
factories.forEach((factory) => {
42+
expect(factory).toHaveProperty('names');
43+
expect(factory).toHaveProperty('factory');
44+
});
45+
});
46+
});

packages/snaps-execution-environments/src/endowments.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
/**
22
* Public endowment factory exports for use outside the Snaps ecosystem.
33
*
4+
* **Prerequisite**: These factories call the SES `harden()` global internally.
5+
* The consuming environment must have loaded SES and called `lockdown()` before
6+
* invoking any factory function.
7+
*
48
* Each module provides a `names` array and a `factory` function. Call
59
* `factory()` to obtain hardened endowment values (and an optional
6-
* `teardownFunction` for timer-based endowments).
10+
* `teardownFunction` for stateful endowments that manage resources).
711
*
812
* @example
913
* ```ts
@@ -13,7 +17,7 @@
1317
* // { setTimeout, clearTimeout, teardownFunction }
1418
*
1519
* const dateEndowment = date.factory();
16-
* // { Date } (with noise-added Date.now)
20+
* // { Date } (with attenuated Date.now)
1721
* ```
1822
*
1923
* @module endowments
@@ -37,5 +41,4 @@ export type {
3741
EndowmentFactoryOptions,
3842
EndowmentFactoryResult,
3943
EndowmentFactory,
40-
CommonEndowmentSpecification,
4144
} from './common/endowments/commonEndowmentFactory';

0 commit comments

Comments
 (0)