diff --git a/packages/snaps-controllers/package.json b/packages/snaps-controllers/package.json index 059d942965..eaa61ff6c1 100644 --- a/packages/snaps-controllers/package.json +++ b/packages/snaps-controllers/package.json @@ -138,6 +138,7 @@ "lodash": "^4.17.21", "prettier": "^3.3.3", "rimraf": "^4.1.2", + "ses": "^1.14.0", "ts-node": "^10.9.1", "tsx": "^4.20.3", "typescript": "~5.3.3", diff --git a/packages/snaps-controllers/src/services/AbstractExecutionService.ts b/packages/snaps-controllers/src/services/AbstractExecutionService.ts index cd07e5d25f..df56b9ec62 100644 --- a/packages/snaps-controllers/src/services/AbstractExecutionService.ts +++ b/packages/snaps-controllers/src/services/AbstractExecutionService.ts @@ -1,7 +1,6 @@ import { JsonRpcEngine } from '@metamask/json-rpc-engine'; import { createStreamMiddleware } from '@metamask/json-rpc-middleware-stream'; import ObjectMultiplex from '@metamask/object-multiplex'; -import type { BasePostMessageStream } from '@metamask/post-message-stream'; import { JsonRpcError } from '@metamask/rpc-errors'; import type { SnapRpcHookArgs } from '@metamask/snaps-utils'; import { SNAP_STREAM_NAMES, logError, logWarning } from '@metamask/snaps-utils'; @@ -47,7 +46,7 @@ export type ExecutionServiceArgs = { export type JobStreams = { command: Duplex; rpc: Duplex; - _connection: BasePostMessageStream; + _connection: Duplex; }; export type Job = { @@ -351,7 +350,7 @@ export abstract class AbstractExecutionService */ protected abstract initEnvStream(snapId: string): Promise<{ worker: WorkerType; - stream: BasePostMessageStream; + stream: Duplex; }>; /** diff --git a/packages/snaps-controllers/src/services/native/NativeExecutionService.test.browser.ts b/packages/snaps-controllers/src/services/native/NativeExecutionService.test.browser.ts new file mode 100644 index 0000000000..833d0d8e80 --- /dev/null +++ b/packages/snaps-controllers/src/services/native/NativeExecutionService.test.browser.ts @@ -0,0 +1,39 @@ +// eslint-disable-next-line import-x/no-unassigned-import +import 'ses'; +import { HandlerType } from '@metamask/snaps-utils'; +import { MOCK_SNAP_ID } from '@metamask/snaps-utils/test-utils'; +import { describe, expect, it } from 'vitest'; + +import { NativeExecutionService } from './NativeExecutionService'; +import { METAMASK_ORIGIN } from '../../snaps'; +import { createService } from '../../test-utils/service'; + +lockdown({ + domainTaming: 'unsafe', + errorTaming: 'unsafe', + stackFiltering: 'verbose', + overrideTaming: 'severe', + errorTrapping: 'none', +}); + +describe('NativeExecutionService', () => { + it('works', async () => { + const { service } = createService(NativeExecutionService); + + await service.executeSnap({ + snapId: MOCK_SNAP_ID, + sourceCode: `module.exports.onRpcRequest = () => { console.log('foo'); return 'bar'; }`, + endowments: ['console'], + }); + + const result = await service.handleRpcRequest(MOCK_SNAP_ID, { + origin: METAMASK_ORIGIN, + request: { + method: 'foo', + }, + handler: HandlerType.OnRpcRequest, + }); + + expect(result).toBe('bar'); + }); +}); diff --git a/packages/snaps-controllers/src/services/native/NativeExecutionService.ts b/packages/snaps-controllers/src/services/native/NativeExecutionService.ts new file mode 100644 index 0000000000..8ebef124be --- /dev/null +++ b/packages/snaps-controllers/src/services/native/NativeExecutionService.ts @@ -0,0 +1,47 @@ +import { NativeSnapExecutor } from '@metamask/snaps-execution-environments'; +import type { Json } from '@metamask/utils'; +import { Duplex } from 'readable-stream'; + +import { + AbstractExecutionService, + type TerminateJobArgs, +} from '../AbstractExecutionService'; + +export class NativeExecutionService extends AbstractExecutionService { + protected terminateJob(_job: TerminateJobArgs): void { + // no-op + } + + protected async initEnvStream( + _snapId: string, + ): Promise<{ worker: NativeSnapExecutor; stream: Duplex }> { + // TODO: Sanity check this. + const workerStream = new Duplex({ + objectMode: true, + read() { + return undefined; + }, + write(chunk: Json, encoding: BufferEncoding, callback) { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + stream.push(chunk, encoding); + callback(); + }, + }); + const stream = new Duplex({ + objectMode: true, + read() { + return undefined; + }, + write(chunk: Json, encoding: BufferEncoding, callback) { + workerStream.push(chunk, encoding); + callback(); + }, + }); + + // NOTE: Initializes a Snap executor that runs in the same JS thread as the execution service. + // Does not provide process isolation. + const worker = NativeSnapExecutor.initialize(workerStream); + + return Promise.resolve({ worker, stream }); + } +} diff --git a/packages/snaps-controllers/src/services/native/index.ts b/packages/snaps-controllers/src/services/native/index.ts new file mode 100644 index 0000000000..52ff3e5e9c --- /dev/null +++ b/packages/snaps-controllers/src/services/native/index.ts @@ -0,0 +1 @@ +export * from './NativeExecutionService'; diff --git a/packages/snaps-controllers/src/services/react-native.ts b/packages/snaps-controllers/src/services/react-native.ts index 0ff2738272..01170cd81d 100644 --- a/packages/snaps-controllers/src/services/react-native.ts +++ b/packages/snaps-controllers/src/services/react-native.ts @@ -1,2 +1,3 @@ export * from '.'; export * from './webview'; +export * from './native'; diff --git a/packages/snaps-execution-environments/src/common/endowments/commonEndowmentFactory.ts b/packages/snaps-execution-environments/src/common/endowments/commonEndowmentFactory.ts index c69d555081..02eb6a3e8c 100644 --- a/packages/snaps-execution-environments/src/common/endowments/commonEndowmentFactory.ts +++ b/packages/snaps-execution-environments/src/common/endowments/commonEndowmentFactory.ts @@ -28,29 +28,29 @@ export type CommonEndowmentSpecification = { // Array of common endowments const commonEndowments: CommonEndowmentSpecification[] = [ - { endowment: AbortController, name: 'AbortController' }, - { endowment: AbortSignal, name: 'AbortSignal' }, - { endowment: ArrayBuffer, name: 'ArrayBuffer' }, - { endowment: atob, name: 'atob', bind: true }, - { endowment: BigInt, name: 'BigInt' }, - { endowment: BigInt64Array, name: 'BigInt64Array' }, - { endowment: BigUint64Array, name: 'BigUint64Array' }, - { endowment: btoa, name: 'btoa', bind: true }, - { endowment: DataView, name: 'DataView' }, - { endowment: Float32Array, name: 'Float32Array' }, - { endowment: Float64Array, name: 'Float64Array' }, - { endowment: Intl, name: 'Intl' }, - { endowment: Int8Array, name: 'Int8Array' }, - { endowment: Int16Array, name: 'Int16Array' }, - { endowment: Int32Array, name: 'Int32Array' }, - { endowment: globalThis.isSecureContext, name: 'isSecureContext' }, - { endowment: Uint8Array, name: 'Uint8Array' }, - { endowment: Uint8ClampedArray, name: 'Uint8ClampedArray' }, - { endowment: Uint16Array, name: 'Uint16Array' }, - { endowment: Uint32Array, name: 'Uint32Array' }, - { endowment: URL, name: 'URL' }, - { endowment: URLSearchParams, name: 'URLSearchParams' }, - { endowment: WebAssembly, name: 'WebAssembly' }, + { endowment: rootRealmGlobal.AbortController, name: 'AbortController' }, + { endowment: rootRealmGlobal.AbortSignal, name: 'AbortSignal' }, + { endowment: rootRealmGlobal.ArrayBuffer, name: 'ArrayBuffer' }, + { endowment: rootRealmGlobal.atob, name: 'atob', bind: true }, + { endowment: rootRealmGlobal.BigInt, name: 'BigInt' }, + { endowment: rootRealmGlobal.BigInt64Array, name: 'BigInt64Array' }, + { endowment: rootRealmGlobal.BigUint64Array, name: 'BigUint64Array' }, + { endowment: rootRealmGlobal.btoa, name: 'btoa', bind: true }, + { endowment: rootRealmGlobal.DataView, name: 'DataView' }, + { endowment: rootRealmGlobal.Float32Array, name: 'Float32Array' }, + { endowment: rootRealmGlobal.Float64Array, name: 'Float64Array' }, + { endowment: rootRealmGlobal.Intl, name: 'Intl' }, + { endowment: rootRealmGlobal.Int8Array, name: 'Int8Array' }, + { endowment: rootRealmGlobal.Int16Array, name: 'Int16Array' }, + { endowment: rootRealmGlobal.Int32Array, name: 'Int32Array' }, + { endowment: rootRealmGlobal.isSecureContext, name: 'isSecureContext' }, + { endowment: rootRealmGlobal.Uint8Array, name: 'Uint8Array' }, + { endowment: rootRealmGlobal.Uint8ClampedArray, name: 'Uint8ClampedArray' }, + { endowment: rootRealmGlobal.Uint16Array, name: 'Uint16Array' }, + { endowment: rootRealmGlobal.Uint32Array, name: 'Uint32Array' }, + { endowment: rootRealmGlobal.URL, name: 'URL' }, + { endowment: rootRealmGlobal.URLSearchParams, name: 'URLSearchParams' }, + // { endowment: WebAssembly, name: 'WebAssembly' }, ]; /** @@ -65,11 +65,11 @@ const buildCommonEndowments = (): EndowmentFactory[] => { crypto, interval, math, - network, + // network, timeout, textDecoder, textEncoder, - date, + // date, consoleEndowment, ]; diff --git a/packages/snaps-execution-environments/src/common/endowments/crypto.ts b/packages/snaps-execution-environments/src/common/endowments/crypto.ts index f425482211..f18d3cd94b 100644 --- a/packages/snaps-execution-environments/src/common/endowments/crypto.ts +++ b/packages/snaps-execution-environments/src/common/endowments/crypto.ts @@ -8,14 +8,14 @@ export const createCrypto = () => { 'Crypto endowment requires `globalThis.crypto` to be defined.', ); - assert( + /**assert( rootRealmGlobal.SubtleCrypto, 'Crypto endowment requires `globalThis.SubtleCrypto` to be defined.', - ); + );**/ return { crypto: harden(rootRealmGlobal.crypto), - SubtleCrypto: harden(rootRealmGlobal.SubtleCrypto), + // SubtleCrypto: harden(rootRealmGlobal.SubtleCrypto), }; }; diff --git a/packages/snaps-execution-environments/src/common/endowments/network.ts b/packages/snaps-execution-environments/src/common/endowments/network.ts index 2cec5f82f7..c99079b671 100644 --- a/packages/snaps-execution-environments/src/common/endowments/network.ts +++ b/packages/snaps-execution-environments/src/common/endowments/network.ts @@ -2,6 +2,7 @@ import { assert } from '@metamask/utils'; import type { EndowmentFactoryOptions } from './commonEndowmentFactory'; import { withTeardown } from '../utils'; +import { rootRealmGlobal } from '../globalObject'; /** * This class wraps a Response object. @@ -177,10 +178,10 @@ const createNetwork = ({ notify }: EndowmentFactoryOptions = {}) => { const teardownRef = { lastTeardown: 0 }; // Remove items from openConnections after they were garbage collected - const cleanup = new FinalizationRegistry<() => void>( + const cleanup = 'FinalizationRegistry' in rootRealmGlobal ? new FinalizationRegistry<() => void>( /* istanbul ignore next: can't test garbage collection without modifying node parameters */ (callback) => callback(), - ); + ) : undefined; const _fetch: typeof fetch = async ( input: RequestInfo | URL, @@ -279,7 +280,7 @@ const createNetwork = ({ notify }: EndowmentFactoryOptions = {}) => { }, }; openConnections.add(openBodyConnection); - cleanup.register( + cleanup?.register( res.body, /* istanbul ignore next: can't test garbage collection without modifying node parameters */ () => openConnections.delete(openBodyConnection), diff --git a/packages/snaps-execution-environments/src/common/globalObject.ts b/packages/snaps-execution-environments/src/common/globalObject.ts index b1e429eee6..87c551e567 100644 --- a/packages/snaps-execution-environments/src/common/globalObject.ts +++ b/packages/snaps-execution-environments/src/common/globalObject.ts @@ -1,3 +1,6 @@ +// @ts-expect-error No types. +import { endowmentsToolkit } from 'lavamoat-core/src/endowmentsToolkit' + enum GlobalObjectNames { // The globalThis entry is incorrectly identified as shadowing the global // globalThis. @@ -33,10 +36,12 @@ if (typeof globalThis !== 'undefined') { } /* eslint-enable no-negated-condition */ +const { copyWrappedGlobals } = endowmentsToolkit(); + /** * A platform-agnostic alias for the root realm global object. */ -const rootRealmGlobal = _rootRealmGlobal; +const rootRealmGlobal = copyWrappedGlobals(_rootRealmGlobal, {}, ['globalThis', 'global', 'self', 'window']); /** * The string literal corresponding to the name of the root realm global object. diff --git a/packages/snaps-execution-environments/src/index.ts b/packages/snaps-execution-environments/src/index.ts index 9bcd868901..57a672f2dc 100644 --- a/packages/snaps-execution-environments/src/index.ts +++ b/packages/snaps-execution-environments/src/index.ts @@ -1 +1,2 @@ export * from './proxy'; +export * from './native'; diff --git a/packages/snaps-execution-environments/src/native/NativeSnapExecutor.ts b/packages/snaps-execution-environments/src/native/NativeSnapExecutor.ts new file mode 100644 index 0000000000..a65049b5d8 --- /dev/null +++ b/packages/snaps-execution-environments/src/native/NativeSnapExecutor.ts @@ -0,0 +1,23 @@ +import ObjectMultiplex from '@metamask/object-multiplex'; +import { logError, SNAP_STREAM_NAMES } from '@metamask/snaps-utils'; +import type { Duplex } from 'readable-stream'; +import { pipeline } from 'readable-stream'; + +import { BaseSnapExecutor } from '../common/BaseSnapExecutor'; + +export class NativeSnapExecutor extends BaseSnapExecutor { + static initialize(stream: Duplex) { + const mux = new ObjectMultiplex(); + pipeline(stream, mux, stream, (error) => { + if (error) { + logError(`Parent stream failure, closing worker.`, error); + } + self.close(); + }); + + const commandStream = mux.createStream(SNAP_STREAM_NAMES.COMMAND); + const rpcStream = mux.createStream(SNAP_STREAM_NAMES.JSON_RPC); + + return new NativeSnapExecutor(commandStream, rpcStream); + } +} diff --git a/packages/snaps-execution-environments/src/native/index.ts b/packages/snaps-execution-environments/src/native/index.ts new file mode 100644 index 0000000000..be4990de33 --- /dev/null +++ b/packages/snaps-execution-environments/src/native/index.ts @@ -0,0 +1 @@ +export * from './NativeSnapExecutor'; diff --git a/yarn.lock b/yarn.lock index cb460a4fa2..846a7700ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4311,6 +4311,7 @@ __metadata: readable-web-to-node-stream: "npm:^3.0.2" rimraf: "npm:^4.1.2" semver: "npm:^7.5.4" + ses: "npm:^1.14.0" tar-stream: "npm:^3.1.7" ts-node: "npm:^10.9.1" tsx: "npm:^4.20.3"