From ef52274b5ecc2846c4ec0537b5db450eaba3b631 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Tue, 30 Jun 2026 11:43:58 +0200 Subject: [PATCH 01/19] feat: Add endowment:messenger --- .../snaps-rpc-methods/src/endowments/enum.ts | 1 + .../snaps-rpc-methods/src/endowments/index.ts | 6 + .../src/endowments/messenger.ts | 136 ++++++++++++++++++ packages/snaps-utils/src/caveats.ts | 5 + .../snaps-utils/src/manifest/validation.ts | 7 + 5 files changed, 155 insertions(+) create mode 100644 packages/snaps-rpc-methods/src/endowments/messenger.ts diff --git a/packages/snaps-rpc-methods/src/endowments/enum.ts b/packages/snaps-rpc-methods/src/endowments/enum.ts index fa8c695d6e..37bf1d669f 100644 --- a/packages/snaps-rpc-methods/src/endowments/enum.ts +++ b/packages/snaps-rpc-methods/src/endowments/enum.ts @@ -14,4 +14,5 @@ export enum SnapEndowments { SettingsPage = 'endowment:page-settings', Assets = 'endowment:assets', Protocol = 'endowment:protocol', + Messenger = 'endowment:messenger', } diff --git a/packages/snaps-rpc-methods/src/endowments/index.ts b/packages/snaps-rpc-methods/src/endowments/index.ts index ec162587cb..646a6a1c63 100644 --- a/packages/snaps-rpc-methods/src/endowments/index.ts +++ b/packages/snaps-rpc-methods/src/endowments/index.ts @@ -21,6 +21,10 @@ import { keyringEndowmentBuilder, } from './keyring'; import { lifecycleHooksEndowmentBuilder } from './lifecycle-hooks'; +import { + getMessengerCaveatMapper, + messengerEndowmentBuilder, +} from './messenger'; import { multichainProviderEndowmentBuilder } from './multichain-provider'; import { getNameLookupCaveatMapper, @@ -71,6 +75,7 @@ export const endowmentPermissionBuilders = { [signatureInsightEndowmentBuilder.targetName]: signatureInsightEndowmentBuilder, [assetsEndowmentBuilder.targetName]: assetsEndowmentBuilder, + [messengerEndowmentBuilder.targetName]: messengerEndowmentBuilder, } as const; export const endowmentCaveatSpecifications = { @@ -114,6 +119,7 @@ export const endowmentCaveatMappers: Record< [assetsEndowmentBuilder.targetName]: createMaxRequestTimeMapper( getAssetsCaveatMapper, ), + [messengerEndowmentBuilder.targetName]: getMessengerCaveatMapper, }; // We allow null because a permitted handler does not have an endowment diff --git a/packages/snaps-rpc-methods/src/endowments/messenger.ts b/packages/snaps-rpc-methods/src/endowments/messenger.ts new file mode 100644 index 0000000000..87c71439a3 --- /dev/null +++ b/packages/snaps-rpc-methods/src/endowments/messenger.ts @@ -0,0 +1,136 @@ +import type { + Caveat, + CaveatSpecificationConstraint, + EndowmentGetterParams, + PermissionConstraint, + PermissionSpecificationBuilder, + PermissionValidatorConstraint, + ValidPermissionSpecification, +} from '@metamask/permission-controller'; +import { PermissionType, SubjectType } from '@metamask/permission-controller'; +import { rpcErrors } from '@metamask/rpc-errors'; +import type { MessengerActionsStruct } from '@metamask/snaps-utils'; +import { MessengerScopesStruct, SnapCaveatType } from '@metamask/snaps-utils'; +import type { Infer } from '@metamask/superstruct'; +import type { Json, NonEmptyArray } from '@metamask/utils'; +import { assertStruct, hasProperty, isObject } from '@metamask/utils'; + +import { createGenericPermissionValidator } from './caveats'; +import { SnapEndowments } from './enum'; + +const permissionName = SnapEndowments.Messenger; + +type MessengerEndowmentSpecification = ValidPermissionSpecification<{ + permissionType: PermissionType.Endowment; + targetName: typeof permissionName; + endowmentGetter: (_options?: EndowmentGetterParams) => null; + allowedCaveats: Readonly> | null; + validator: PermissionValidatorConstraint; + subjectTypes: readonly SubjectType[]; +}>; + +/** + * `endowment:messenger` returns nothing; it is intended to be used as a + * flag by the Snaps Platform to detect whether a Snap has the capability to + * use the messenger API. + * + * @param _builderOptions - Optional specification builder options. + * @returns The specification for the messenger endowment. + */ +const specificationBuilder: PermissionSpecificationBuilder< + PermissionType.Endowment, + any, + MessengerEndowmentSpecification +> = (_builderOptions?: unknown) => { + return { + permissionType: PermissionType.Endowment, + targetName: permissionName, + allowedCaveats: [SnapCaveatType.MessengerScopes], + endowmentGetter: (_getterOptions?: EndowmentGetterParams) => null, + validator: createGenericPermissionValidator([ + { type: SnapCaveatType.MessengerScopes }, + ]), + subjectTypes: [SubjectType.Snap], + }; +}; + +export const messengerEndowmentBuilder = Object.freeze({ + targetName: permissionName, + specificationBuilder, +} as const); + +/** + * Map a raw value from the `initialPermissions` to a caveat specification. + * Note that this function does not do any validation, that's handled by the + * PermissionsController when the permission is requested. + * + * @param value - The raw value from the `initialPermissions`. + * @returns The caveat specification. + */ +export function getMessengerCaveatMapper( + value: Json, +): Pick { + if (!value || !isObject(value) || Object.keys(value).length === 0) { + return { caveats: null }; + } + + return { + caveats: [ + { + type: SnapCaveatType.MessengerScopes, + value, + }, + ], + }; +} + +export type MessengerActions = Infer; + +/** + * Getter function to get the {@link MessengerActions} caveat value from a + * permission. + * + * @param permission - The permission to get the caveat value from. + * @returns The caveat value. + */ +export function getMessengerCaveatActions( + permission?: PermissionConstraint, +): MessengerActions | null { + const caveat = permission?.caveats?.find( + (permCaveat) => permCaveat.type === SnapCaveatType.MessengerScopes, + ) as Caveat | undefined; + + return caveat?.value.actions ? caveat.value.actions : null; +} + +/** + * Validates the type of the caveat value. + * + * @param caveat - The caveat to validate. + * @throws If the caveat value is invalid. + */ +function validateCaveat(caveat: Caveat): void { + if (!hasProperty(caveat, 'value') || !isObject(caveat)) { + throw rpcErrors.invalidParams({ + message: 'Expected a plain object.', + }); + } + + const { value } = caveat; + assertStruct( + value, + MessengerScopesStruct, + 'Invalid messenger scopes specified', + rpcErrors.invalidParams, + ); +} + +export const messengerCaveatSpecifications: Record< + SnapCaveatType.MessengerScopes, + CaveatSpecificationConstraint +> = { + [SnapCaveatType.MessengerScopes]: Object.freeze({ + type: SnapCaveatType.MessengerScopes, + validator: (caveat: Caveat) => validateCaveat(caveat), + }), +}; diff --git a/packages/snaps-utils/src/caveats.ts b/packages/snaps-utils/src/caveats.ts index b3c456f1f0..2554df2058 100644 --- a/packages/snaps-utils/src/caveats.ts +++ b/packages/snaps-utils/src/caveats.ts @@ -63,4 +63,9 @@ export enum SnapCaveatType { * Caveat specifying a list of scopes serviced by an endowment. */ ProtocolSnapScopes = 'protocolSnapScopes', + + /** + * Caveat specifying the required messenger actions and events by a Snap. + */ + MessengerScopes = 'messengerScopes', } diff --git a/packages/snaps-utils/src/manifest/validation.ts b/packages/snaps-utils/src/manifest/validation.ts index 51cda496df..938bc5dca4 100644 --- a/packages/snaps-utils/src/manifest/validation.ts +++ b/packages/snaps-utils/src/manifest/validation.ts @@ -192,6 +192,12 @@ export const ProtocolScopesStruct = record( object({ methods: array(string()) }), ); +export const MessengerActionsStruct = size(array(string()), 1, Infinity); + +export const MessengerScopesStruct = object({ + actions: MessengerActionsStruct, +}); + // Utility type to union with for all handler structs export const HandlerCaveatsStruct = object({ maxRequestTime: optional(MaxRequestTimeStruct), @@ -229,6 +235,7 @@ export const PermissionsStruct: Describe = type({ object({ scopes: ProtocolScopesStruct }), ), ), + 'endowment:messenger': optional(MessengerScopesStruct), 'endowment:lifecycle-hooks': optional(HandlerCaveatsStruct), 'endowment:name-lookup': optional( mergeStructs( From 3984d223cac075ae7dbca6eb89bda3201eee21e7 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Tue, 30 Jun 2026 12:14:19 +0200 Subject: [PATCH 02/19] Add RPC method --- .../src/permitted/messengerCall.ts | 146 ++++++++++++++++++ packages/snaps-sdk/src/types/methods/index.ts | 1 + .../src/types/methods/messenger-call.ts | 17 ++ .../snaps-sdk/src/types/methods/methods.ts | 5 + 4 files changed, 169 insertions(+) create mode 100644 packages/snaps-rpc-methods/src/permitted/messengerCall.ts create mode 100644 packages/snaps-sdk/src/types/methods/messenger-call.ts diff --git a/packages/snaps-rpc-methods/src/permitted/messengerCall.ts b/packages/snaps-rpc-methods/src/permitted/messengerCall.ts new file mode 100644 index 0000000000..1f0f3d0392 --- /dev/null +++ b/packages/snaps-rpc-methods/src/permitted/messengerCall.ts @@ -0,0 +1,146 @@ +import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { + ActionConstraint, + EventConstraint, + Messenger, +} from '@metamask/messenger'; +import type { PermissionControllerGetPermissionAction } from '@metamask/permission-controller'; +import { rpcErrors } from '@metamask/rpc-errors'; +import type { + MessengerCallParams, + MessengerCallResult, +} from '@metamask/snaps-sdk'; +import type { InferMatching } from '@metamask/snaps-utils'; +import { + create, + object, + string, + StructError, + array, +} from '@metamask/superstruct'; +import type { PendingJsonRpcResponse } from '@metamask/utils'; +import { JsonStruct } from '@metamask/utils'; +import { SnapEndowments } from 'src'; +import { getMessengerCaveatActions } from 'src/endowments/messenger'; + +import type { + JsonRpcRequestWithOrigin, + SnapControllerGetSnapAction, +} from '../types'; +import type { MethodHooksObject } from '../utils'; + +const hookNames: MethodHooksObject = { + getMessenger: true, +}; + +export type MessengerCallMethodHooks = { + getMessenger: ( + actions: string[], + events: string[], + ) => Messenger; +}; + +export type MessengerCallMethodActions = + | SnapControllerGetSnapAction + | PermissionControllerGetPermissionAction; + +const MessengerCallParametersStruct = object({ + action: string(), + params: array(JsonStruct), +}); + +export type MessengerCallParameters = InferMatching< + typeof MessengerCallParametersStruct, + MessengerCallParams +>; + +/** + * Handler for the `snap_messengerCall` method. + * + * @internal + */ +export const messengerCallHandler = { + implementation: getMessengerCallImplementation, + hookNames, + actionNames: ['SnapController:getSnap', 'PermissionController:getPermission'], +} satisfies MethodHandler< + MessengerCallMethodHooks, + MessengerCallMethodActions, + MessengerCallParameters, + MessengerCallResult, + { origin: string } +>; + +/** + * The `snap_messengerCall` method implementation. This method is used to dispatch + * an action via the messenger. It is only available to preinstalled Snaps. + * + * @param request - The JSON-RPC request object. + * @param response - The JSON-RPC response object. + * @param _next - The `json-rpc-engine` "next" callback. Not used by this + * function. + * @param end - The `json-rpc-engine` "end" callback. + * @param hooks - The RPC method hooks. + * @param hooks.getMessenger - The hook function to create a custom messenger for the Snap. + * @param messenger - The messenger used to call controller actions. + * @returns Nothing. + */ +function getMessengerCallImplementation( + request: JsonRpcRequestWithOrigin, + response: PendingJsonRpcResponse, + _next: unknown, + end: JsonRpcEngineEndCallback, + { getMessenger }: MessengerCallMethodHooks, + messenger: Messenger, +): void { + const snap = messenger.call('SnapController:getSnap', request.origin); + const permission = messenger.call( + 'PermissionController:getPermission', + request.origin, + SnapEndowments.Messenger, + ); + + if (!snap?.preinstalled || !permission) { + return end(rpcErrors.methodNotFound()); + } + + const actions = getMessengerCaveatActions(permission); + + const snapMessenger = getMessenger(actions ?? [], []); + + const { params } = request; + + try { + const { action, params: actionParams } = getValidatedParams(params); + response.result = snapMessenger.call(action, ...actionParams); + } catch (error) { + return end(error); + } + + return end(); +} + +/** + * Validate the parameters for the `snap_messengerCall` method. + * + * @param params - Parameters to validate. + * @returns Validated parameters. + * @throws Throws RPC error if validation fails. + */ +function getValidatedParams(params: unknown): MessengerCallParameters { + try { + return create(params, MessengerCallParametersStruct); + } catch (error) { + if (error instanceof StructError) { + throw rpcErrors.invalidParams({ + message: `Invalid params: ${error.message}.`, + }); + } + + /* istanbul ignore next */ + throw rpcErrors.internal(); + } +} diff --git a/packages/snaps-sdk/src/types/methods/index.ts b/packages/snaps-sdk/src/types/methods/index.ts index 8b3e97b2aa..a1b633843f 100644 --- a/packages/snaps-sdk/src/types/methods/index.ts +++ b/packages/snaps-sdk/src/types/methods/index.ts @@ -20,6 +20,7 @@ export type * from './list-entropy-sources'; export type * from './manage-accounts'; export * from './manage-state'; export type * from './methods'; +export type * from './messenger-call'; export type * from './multichain'; export * from './notify'; export type * from './request-snaps'; diff --git a/packages/snaps-sdk/src/types/methods/messenger-call.ts b/packages/snaps-sdk/src/types/methods/messenger-call.ts new file mode 100644 index 0000000000..7b54e2ff3c --- /dev/null +++ b/packages/snaps-sdk/src/types/methods/messenger-call.ts @@ -0,0 +1,17 @@ +import type { Json } from '@metamask/utils'; + +/** + * The request parameters for the `snap_messengerCall` method. This method is used + * to call an action on the messenger. + * + * Note that this method is only available to preinstalled Snaps. + */ +export type MessengerCallParams = { + action: `${string}:${string}`; + params: Json[]; +}; + +/** + * The result returned by the `snap_messengerCall` method. + */ +export type MessengerCallResult = Json; diff --git a/packages/snaps-sdk/src/types/methods/methods.ts b/packages/snaps-sdk/src/types/methods/methods.ts index 147353fdc9..8665335fc9 100644 --- a/packages/snaps-sdk/src/types/methods/methods.ts +++ b/packages/snaps-sdk/src/types/methods/methods.ts @@ -68,6 +68,10 @@ import type { ManageAccountsResult, } from './manage-accounts'; import type { ManageStateParams, ManageStateResult } from './manage-state'; +import type { + MessengerCallParams, + MessengerCallResult, +} from './messenger-call'; import type { CreateSessionParams, CreateSessionResult, @@ -127,6 +131,7 @@ export type SnapMethods = { snap_listEntropySources: [ListEntropySourcesParams, ListEntropySourcesResult]; snap_manageAccounts: [ManageAccountsParams, ManageAccountsResult]; snap_manageState: [ManageStateParams, ManageStateResult]; + snap_messengerCall: [MessengerCallParams, MessengerCallResult]; snap_notify: [NotifyParams, NotifyResult]; snap_scheduleBackgroundEvent: [ ScheduleBackgroundEventParams, From c62701b475be02587caeffeccd479c3ac7bad8c5 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Tue, 30 Jun 2026 12:16:11 +0200 Subject: [PATCH 03/19] Add missing wiring --- packages/snaps-rpc-methods/src/permitted/index.ts | 10 ++++++++-- packages/snaps-rpc-methods/src/permitted/middleware.ts | 2 ++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/snaps-rpc-methods/src/permitted/index.ts b/packages/snaps-rpc-methods/src/permitted/index.ts index 2d864fe9e1..e0cff45393 100644 --- a/packages/snaps-rpc-methods/src/permitted/index.ts +++ b/packages/snaps-rpc-methods/src/permitted/index.ts @@ -24,6 +24,10 @@ import type { ListEntropySourcesMethodActions, ListEntropySourcesMethodHooks, } from './listEntropySources'; +import type { + MessengerCallMethodActions, + MessengerCallMethodHooks, +} from './messengerCall'; import type { OpenWebSocketMethodActions } from './openWebSocket'; import type { RequestSnapsMethodActions } from './requestSnaps'; import type { ResolveInterfaceMethodActions } from './resolveInterface'; @@ -71,7 +75,8 @@ export type PermittedRpcMethodActions = | StartTraceMethodActions | TrackErrorMethodActions | TrackEventMethodActions - | UpdateInterfaceMethodActions; + | UpdateInterfaceMethodActions + | MessengerCallMethodActions; export type PermittedRpcMethodHooks = GetClientStatusMethodHooks & GetStateMethodHooks & @@ -81,6 +86,7 @@ export type PermittedRpcMethodHooks = GetClientStatusMethodHooks & TrackErrorMethodHooks & StartTraceMethodHooks & EndTraceMethodHooks & - InvokeKeyringMethodHooks; + InvokeKeyringMethodHooks & + MessengerCallMethodHooks; export * from './middleware'; diff --git a/packages/snaps-rpc-methods/src/permitted/middleware.ts b/packages/snaps-rpc-methods/src/permitted/middleware.ts index ea7a1c9ad0..d5fd549208 100644 --- a/packages/snaps-rpc-methods/src/permitted/middleware.ts +++ b/packages/snaps-rpc-methods/src/permitted/middleware.ts @@ -25,6 +25,7 @@ import { getWebSocketsHandler } from './getWebSockets'; import { invokeKeyringHandler } from './invokeKeyring'; import { invokeSnapSugarHandler } from './invokeSnapSugar'; import { listEntropySourcesHandler } from './listEntropySources'; +import { messengerCallHandler } from './messengerCall'; import { openWebSocketHandler } from './openWebSocket'; import { requestSnapsHandler } from './requestSnaps'; import { resolveInterfaceHandler } from './resolveInterface'; @@ -65,6 +66,7 @@ const methodHandlers = { snap_getWebSockets: getWebSocketsHandler, snap_startTrace: startTraceHandler, snap_endTrace: endTraceHandler, + snap_messengerCall: messengerCallHandler, }; /* eslint-enable @typescript-eslint/naming-convention */ From 009166c813042e4ba1eb684735699d27ce63dd97 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Tue, 30 Jun 2026 13:17:41 +0200 Subject: [PATCH 04/19] Add example Snap for testing --- .../packages/preinstalled/snap.manifest.json | 7 +- .../packages/preinstalled/src/index.test.tsx | 12 +++ .../packages/preinstalled/src/index.tsx | 15 ++++ .../snaps-rpc-methods/src/endowments/index.ts | 2 + .../src/permitted/messengerCall.ts | 38 +++++----- packages/snaps-sdk/src/index.ts | 1 + packages/snaps-sdk/src/messenger.ts | 74 +++++++++++++++++++ packages/snaps-simulation/src/simulation.ts | 32 +++++++- 8 files changed, 157 insertions(+), 24 deletions(-) create mode 100644 packages/snaps-sdk/src/messenger.ts diff --git a/packages/examples/packages/preinstalled/snap.manifest.json b/packages/examples/packages/preinstalled/snap.manifest.json index 3437326496..d901676452 100644 --- a/packages/examples/packages/preinstalled/snap.manifest.json +++ b/packages/examples/packages/preinstalled/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "HEK+eFeY5FqLG4XSbZzP7BoXJgXptNV0lHtuxuFEym8=", + "shasum": "4Orr2LFWey4elfpvTf6tdUOBsaVCnahshTHv3v9Bf7I=", "location": { "npm": { "filePath": "dist/bundle.js", @@ -25,7 +25,10 @@ }, "snap_dialog": {}, "endowment:page-settings": {}, - "snap_manageState": {} + "snap_manageState": {}, + "endowment:messenger": { + "actions": ["PhishingController:testOrigin"] + } }, "platformVersion": "11.1.1", "manifestVersion": "0.1" diff --git a/packages/examples/packages/preinstalled/src/index.test.tsx b/packages/examples/packages/preinstalled/src/index.test.tsx index f15dca009a..e6426db3ef 100644 --- a/packages/examples/packages/preinstalled/src/index.test.tsx +++ b/packages/examples/packages/preinstalled/src/index.test.tsx @@ -150,6 +150,18 @@ describe('onRpcRequest', () => { }); }); }); + + describe('messengerCall', () => { + it('calls the PhishingController', async () => { + const { request } = await installSnap(); + + const response = await request({ + method: 'messengerCall', + }); + + expect(response).toRespondWith(false); + }); + }); }); describe('onSettingsPage', () => { diff --git a/packages/examples/packages/preinstalled/src/index.tsx b/packages/examples/packages/preinstalled/src/index.tsx index 51dc440b8a..9f6ad4897a 100644 --- a/packages/examples/packages/preinstalled/src/index.tsx +++ b/packages/examples/packages/preinstalled/src/index.tsx @@ -1,5 +1,6 @@ import { getJsonError, + getMessenger, MethodNotFoundError, UserInputEventType, } from '@metamask/snaps-sdk'; @@ -17,6 +18,11 @@ type SnapState = { setting3?: 'option1' | 'option2'; }; +type PhishingControllerTestOrigin = { + type: 'PhishingController:testOrigin'; + handler: (origin: string) => { result: boolean; type: string }; +}; + /** * Handle incoming JSON-RPC requests from the dapp, sent through the * `wallet_invokeSnap` method. This handler handles the following methods: @@ -97,6 +103,15 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { }); } + case 'messengerCall': { + const messenger = getMessenger(); + const { result } = await messenger.call( + 'PhishingController:testOrigin', + 'https://metamask.io', + ); + return result; + } + default: throw new MethodNotFoundError({ method: request.method }); } diff --git a/packages/snaps-rpc-methods/src/endowments/index.ts b/packages/snaps-rpc-methods/src/endowments/index.ts index 646a6a1c63..79fade8612 100644 --- a/packages/snaps-rpc-methods/src/endowments/index.ts +++ b/packages/snaps-rpc-methods/src/endowments/index.ts @@ -23,6 +23,7 @@ import { import { lifecycleHooksEndowmentBuilder } from './lifecycle-hooks'; import { getMessengerCaveatMapper, + messengerCaveatSpecifications, messengerEndowmentBuilder, } from './messenger'; import { multichainProviderEndowmentBuilder } from './multichain-provider'; @@ -87,6 +88,7 @@ export const endowmentCaveatSpecifications = { ...signatureInsightCaveatSpecifications, ...maxRequestTimeCaveatSpecifications, ...protocolCaveatSpecifications, + ...messengerCaveatSpecifications, }; export const endowmentCaveatMappers: Record< diff --git a/packages/snaps-rpc-methods/src/permitted/messengerCall.ts b/packages/snaps-rpc-methods/src/permitted/messengerCall.ts index 1f0f3d0392..6ad056690a 100644 --- a/packages/snaps-rpc-methods/src/permitted/messengerCall.ts +++ b/packages/snaps-rpc-methods/src/permitted/messengerCall.ts @@ -2,11 +2,7 @@ import type { JsonRpcEngineEndCallback, MethodHandler, } from '@metamask/json-rpc-engine'; -import type { - ActionConstraint, - EventConstraint, - Messenger, -} from '@metamask/messenger'; +import type { Messenger, NamespacedName } from '@metamask/messenger'; import type { PermissionControllerGetPermissionAction } from '@metamask/permission-controller'; import { rpcErrors } from '@metamask/rpc-errors'; import type { @@ -14,18 +10,12 @@ import type { MessengerCallResult, } from '@metamask/snaps-sdk'; import type { InferMatching } from '@metamask/snaps-utils'; -import { - create, - object, - string, - StructError, - array, -} from '@metamask/superstruct'; -import type { PendingJsonRpcResponse } from '@metamask/utils'; -import { JsonStruct } from '@metamask/utils'; -import { SnapEndowments } from 'src'; -import { getMessengerCaveatActions } from 'src/endowments/messenger'; +import { create, object, StructError, array } from '@metamask/superstruct'; +import type { Json, PendingJsonRpcResponse } from '@metamask/utils'; +import { definePattern, JsonStruct } from '@metamask/utils'; +import { SnapEndowments } from '../endowments'; +import { getMessengerCaveatActions } from '../endowments/messenger'; import type { JsonRpcRequestWithOrigin, SnapControllerGetSnapAction, @@ -36,11 +26,16 @@ const hookNames: MethodHooksObject = { getMessenger: true, }; +type GenericMessengerAction = { + type: NamespacedName; + handler: (...args: Json[]) => Promise; +}; + export type MessengerCallMethodHooks = { getMessenger: ( actions: string[], events: string[], - ) => Messenger; + ) => Messenger; }; export type MessengerCallMethodActions = @@ -48,7 +43,7 @@ export type MessengerCallMethodActions = | PermissionControllerGetPermissionAction; const MessengerCallParametersStruct = object({ - action: string(), + action: definePattern<`${string}:${string}`>('MessengerAction', /^.+:.+$/u), params: array(JsonStruct), }); @@ -88,14 +83,14 @@ export const messengerCallHandler = { * @param messenger - The messenger used to call controller actions. * @returns Nothing. */ -function getMessengerCallImplementation( +async function getMessengerCallImplementation( request: JsonRpcRequestWithOrigin, response: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, { getMessenger }: MessengerCallMethodHooks, messenger: Messenger, -): void { +): Promise { const snap = messenger.call('SnapController:getSnap', request.origin); const permission = messenger.call( 'PermissionController:getPermission', @@ -109,13 +104,14 @@ function getMessengerCallImplementation( const actions = getMessengerCaveatActions(permission); + // TODO: Consider if we need to ban any actions. const snapMessenger = getMessenger(actions ?? [], []); const { params } = request; try { const { action, params: actionParams } = getValidatedParams(params); - response.result = snapMessenger.call(action, ...actionParams); + response.result = await snapMessenger.call(action, ...actionParams); } catch (error) { return end(error); } diff --git a/packages/snaps-sdk/src/index.ts b/packages/snaps-sdk/src/index.ts index 776393c235..6ec702e8e0 100644 --- a/packages/snaps-sdk/src/index.ts +++ b/packages/snaps-sdk/src/index.ts @@ -38,5 +38,6 @@ export { assert } from '@metamask/utils'; export * from './errors'; export * from './error-wrappers'; export * from './images'; +export * from './messenger'; export * from './types'; export * from './ui'; diff --git a/packages/snaps-sdk/src/messenger.ts b/packages/snaps-sdk/src/messenger.ts new file mode 100644 index 0000000000..1e7b598425 --- /dev/null +++ b/packages/snaps-sdk/src/messenger.ts @@ -0,0 +1,74 @@ +/** + * A constraint type matching a single messenger action. + */ +type ActionConstraint = { + type: `${string}:${string}`; + handler: (...args: never[]) => unknown; +}; + +/** + * Extract the parameters of a messenger action by its type. + * + * @template Action - The union of actions to extract from. + * @template ActionType - The type of the action to extract the parameters of. + */ +type ExtractActionParameters< + Action extends ActionConstraint, + ActionType extends Action['type'], +> = Action extends { + type: ActionType; + handler: (...args: infer Params) => unknown; +} + ? Params + : never; + +/** + * Extract the response of a messenger action by its type. + * + * @template Action - The union of actions to extract from. + * @template ActionType - The type of the action to extract the response of. + */ +type ExtractActionResponse< + Action extends ActionConstraint, + ActionType extends Action['type'], +> = Action extends { + type: ActionType; + handler: (...args: never[]) => infer Response; +} + ? Response + : never; + +/** + * An asynchronous version of a messenger's `call` method. Calls made from + * within a Snap are dispatched to the client via the `snap_messengerCall` + * method, so the original (synchronous) return type is wrapped in a `Promise`. + * + * @template Action - The union of actions the messenger can call. + */ +export type AsyncMessenger = { + call: ( + actionType: ActionType, + ...params: ExtractActionParameters + ) => Promise>>; +}; + +/** + * Get a messenger that can be used to call actions on the client + * from within a Snap. The returned messenger is asynchronous. + * + * Note that this is only available to preinstalled Snaps. + * + * @template Action - The union of actions the messenger can call. + * @returns An asynchronous messenger. + */ +export function getMessenger< + Action extends ActionConstraint, +>(): AsyncMessenger { + return { + call: async (action, ...params) => + snap.request({ + method: 'snap_messengerCall', + params: { action, params }, + }), + } as AsyncMessenger; +} diff --git a/packages/snaps-simulation/src/simulation.ts b/packages/snaps-simulation/src/simulation.ts index 13f0df3762..07fd7db30d 100644 --- a/packages/snaps-simulation/src/simulation.ts +++ b/packages/snaps-simulation/src/simulation.ts @@ -230,6 +230,13 @@ export type PermittedMiddlewareHooks = { * @returns The keyring methods. */ getAllowedKeyringMethods(): string[]; + + /** + * A hook that returns a specialized messenger for the Snap. + * + * @returns The messenger. + */ + getMessenger(actions: string[], events: string[]): Messenger; }; export type MultichainMiddlewareHooks = { @@ -319,7 +326,11 @@ export async function installSnap< // Set up controllers and JSON-RPC stack. const restrictedHooks = getRestrictedHooks(options, store, runSaga); - const permittedHooks = getPermittedHooks(runSaga); + const permittedHooks = getPermittedHooks( + snapId, + controllerMessenger, + runSaga, + ); const multichainHooks = getMultichainHooks( snapId, @@ -453,10 +464,14 @@ export function getRestrictedHooks( /** * Get the permitted hooks for the simulation. * + * @param snapId - The Snap ID. + * @param controllerMessenger - The controller messenger. * @param runSaga - The run saga function. * @returns The permitted hooks for the simulation. */ export function getPermittedHooks( + snapId: string, + controllerMessenger: RootControllerMessenger, runSaga: RunSagaFunction, ): PermittedMiddlewareHooks { return { @@ -470,6 +485,21 @@ export function getPermittedHooks( trackEvent: getTrackEventImplementation(runSaga), startTrace: getStartTraceImplementation(runSaga), endTrace: getEndTraceImplementation(runSaga), + + getMessenger: (actions, events) => { + const messenger = new Messenger({ + namespace: `${snapId}-messenger`, + parent: controllerMessenger, + }); + + controllerMessenger.delegate({ + actions: actions as never[], + events: events as never[], + messenger, + }); + + return messenger; + }, }; } From 13d5eca6b529f559cf7fe9cc74f6e7ecbe4958e5 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Tue, 30 Jun 2026 14:05:29 +0200 Subject: [PATCH 05/19] Add tests --- packages/snaps-rpc-methods/jest.config.js | 8 +- .../src/endowments/messenger.test.ts | 151 ++++++++++++ .../snaps-rpc-methods/src/permissions.test.ts | 32 ++- .../src/permitted/messengerCall.test.ts | 230 ++++++++++++++++++ .../src/permitted/middleware.test.ts | 1 + .../src/methods/specifications.test.ts | 54 ++-- .../src/middleware/engine.test.ts | 1 + 7 files changed, 442 insertions(+), 35 deletions(-) create mode 100644 packages/snaps-rpc-methods/src/endowments/messenger.test.ts create mode 100644 packages/snaps-rpc-methods/src/permitted/messengerCall.test.ts diff --git a/packages/snaps-rpc-methods/jest.config.js b/packages/snaps-rpc-methods/jest.config.js index 8f39566dfd..bb49bd19e5 100644 --- a/packages/snaps-rpc-methods/jest.config.js +++ b/packages/snaps-rpc-methods/jest.config.js @@ -10,10 +10,10 @@ module.exports = deepmerge(baseConfig, { ], coverageThreshold: { global: { - branches: 97.28, - functions: 98.88, - lines: 99.19, - statements: 98.91, + branches: 97.13, + functions: 98.91, + lines: 99.22, + statements: 98.94, }, }, }); diff --git a/packages/snaps-rpc-methods/src/endowments/messenger.test.ts b/packages/snaps-rpc-methods/src/endowments/messenger.test.ts new file mode 100644 index 0000000000..24b03f3f3f --- /dev/null +++ b/packages/snaps-rpc-methods/src/endowments/messenger.test.ts @@ -0,0 +1,151 @@ +import { PermissionType, SubjectType } from '@metamask/permission-controller'; +import { SnapCaveatType } from '@metamask/snaps-utils'; + +import { SnapEndowments } from './enum'; +import { + getMessengerCaveatMapper, + messengerCaveatSpecifications, + messengerEndowmentBuilder, + getMessengerCaveatActions, +} from './messenger'; + +describe('endowment:messenger', () => { + it('builds the expected permission specification', () => { + const specification = messengerEndowmentBuilder.specificationBuilder({}); + expect(specification).toStrictEqual({ + permissionType: PermissionType.Endowment, + targetName: SnapEndowments.Messenger, + endowmentGetter: expect.any(Function), + allowedCaveats: [ + SnapCaveatType.MessengerScopes, + ], + subjectTypes: [SubjectType.Snap], + validator: expect.any(Function), + }); + + expect(specification.endowmentGetter()).toBeNull(); + }); + + describe('validator', () => { + it('throws if the caveat is not a single "messengerScopes"', () => { + const specification = messengerEndowmentBuilder.specificationBuilder({}); + + expect(() => + specification.validator({ + // @ts-expect-error Missing other required permission types. + caveats: undefined, + }), + ).toThrow('Expected the following caveats: "messengerScopes".'); + + expect(() => + // @ts-expect-error Missing other required permission types. + specification.validator({ + caveats: [{ type: 'foo', value: 'bar' }], + }), + ).toThrow( + 'Expected the following caveats: "messengerScopes", received "foo".', + ); + + expect(() => + // @ts-expect-error Missing other required permission types. + specification.validator({ + caveats: [ + { type: SnapCaveatType.MessengerScopes, value: 'bar' }, + { type: SnapCaveatType.MessengerScopes, value: 'bar' }, + ], + }), + ).toThrow('Duplicate caveats are not allowed.'); + }); + }); +}); + +describe('getMessengerCaveatMapper', () => { + it('maps a value to a caveat', () => { + expect( + getMessengerCaveatMapper({ + actions: ["Foo:bar"], + }), + ).toStrictEqual({ + caveats: [ + { + type: SnapCaveatType.MessengerScopes, + value: { + actions: ["Foo:bar"] + }, + }, + ], + }); + }); + + it('returns null if value is empty', () => { + expect(getMessengerCaveatMapper({})).toStrictEqual({ caveats: null }); + }); +}); + +describe('getMessengerCaveatActions', () => { + it('returns the actions from the caveat', () => { + expect( + // @ts-expect-error Missing other required permission types. + getMessengerCaveatActions({ + caveats: [ + { + type: SnapCaveatType.MessengerScopes, + value: { actions: ["Foo:bar"] } + }, + ], + }), + ).toStrictEqual(["Foo:bar"]); + }); + + it('returns null if no caveat found', () => { + expect( + // @ts-expect-error Missing other required permission types. + getMessengerCaveatActions({ + caveats: null, + }), + ).toBeNull(); + }); +}); + +describe('messengerCaveatSpecifications', () => { + describe('validator', () => { + it('throws if the caveat values are invalid', () => { + expect(() => + messengerCaveatSpecifications[ + SnapCaveatType.MessengerScopes + ].validator?.( + // @ts-expect-error Missing value type. + { + type: SnapCaveatType.MessengerScopes, + }, + ), + ).toThrow('Expected a plain object.'); + + expect(() => + messengerCaveatSpecifications[ + SnapCaveatType.MessengerScopes + ].validator?.({ + type: SnapCaveatType.MessengerScopes, + value: { + foo: 'bar', + }, + }), + ).toThrow( + 'Invalid messenger scopes specified: At path: actions -- Expected an array value, but received: undefined.', + ); + + expect(() => + messengerCaveatSpecifications[ + SnapCaveatType.MessengerScopes + ].validator?.({ + type: SnapCaveatType.MessengerScopes, + value: { + actions: [], + }, + }), + ).toThrow( + 'Invalid messenger scopes specified: At path: actions -- Expected a array with a length between `1` and `Infinity` but received one with a length of `0`.', + ); + }); + }); +}); diff --git a/packages/snaps-rpc-methods/src/permissions.test.ts b/packages/snaps-rpc-methods/src/permissions.test.ts index 068f6b8743..52a2b26780 100644 --- a/packages/snaps-rpc-methods/src/permissions.test.ts +++ b/packages/snaps-rpc-methods/src/permissions.test.ts @@ -1,12 +1,12 @@ -import { Messenger } from '@metamask/messenger'; +import { Messenger } from "@metamask/messenger"; import { buildSnapEndowmentSpecifications, buildSnapRestrictedMethodSpecifications, -} from './permissions'; +} from "./permissions"; -describe('buildSnapEndowmentSpecifications', () => { - it('returns the expected object', () => { +describe("buildSnapEndowmentSpecifications", () => { + it("returns the expected object", () => { const specifications = buildSnapEndowmentSpecifications([]); expect(specifications).toMatchInlineSnapshot(` { @@ -71,6 +71,18 @@ describe('buildSnapEndowmentSpecifications', () => { "targetName": "endowment:lifecycle-hooks", "validator": [Function], }, + "endowment:messenger": { + "allowedCaveats": [ + "messengerScopes", + ], + "endowmentGetter": [Function], + "permissionType": "Endowment", + "subjectTypes": [ + "snap", + ], + "targetName": "endowment:messenger", + "validator": [Function], + }, "endowment:multichain-provider": { "allowedCaveats": null, "endowmentGetter": [Function], @@ -193,8 +205,8 @@ describe('buildSnapEndowmentSpecifications', () => { }); }); -describe('buildSnapRestrictedMethodSpecifications', () => { - it('returns the expected object', () => { +describe("buildSnapRestrictedMethodSpecifications", () => { + it("returns the expected object", () => { const specifications = buildSnapRestrictedMethodSpecifications( [], { @@ -205,7 +217,7 @@ describe('buildSnapRestrictedMethodSpecifications', () => { getSnapKeyring: jest.fn(), getPreferences: jest.fn(), }, - new Messenger({ namespace: 'SnapsRestrictedMethods' }), + new Messenger({ namespace: "SnapsRestrictedMethods" }) ); expect(specifications).toMatchInlineSnapshot(` { @@ -324,9 +336,9 @@ describe('buildSnapRestrictedMethodSpecifications', () => { `); }); - it('excludes the specified permissions', () => { + it("excludes the specified permissions", () => { const specifications = buildSnapRestrictedMethodSpecifications( - ['snap_dialog'], + ["snap_dialog"], { getUnlockPromise: jest.fn(), getClientCryptography: jest.fn(), @@ -335,7 +347,7 @@ describe('buildSnapRestrictedMethodSpecifications', () => { getSnapKeyring: jest.fn(), getPreferences: jest.fn(), }, - new Messenger({ namespace: 'SnapsRestrictedMethods' }), + new Messenger({ namespace: "SnapsRestrictedMethods" }) ); expect(specifications.snap_dialog).toBeUndefined(); diff --git a/packages/snaps-rpc-methods/src/permitted/messengerCall.test.ts b/packages/snaps-rpc-methods/src/permitted/messengerCall.test.ts new file mode 100644 index 0000000000..96395600e7 --- /dev/null +++ b/packages/snaps-rpc-methods/src/permitted/messengerCall.test.ts @@ -0,0 +1,230 @@ +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; +import type { MessengerCallParams, MessengerCallResult } from '@metamask/snaps-sdk'; +import { SnapCaveatType } from '@metamask/snaps-utils'; +import { + MOCK_SNAP_ID, + MockControllerMessenger, + getSnapObject, +} from '@metamask/snaps-utils/test-utils'; +import type { PendingJsonRpcResponse } from '@metamask/utils'; + +import { MessengerCallMethodActions, messengerCallHandler } from './messengerCall'; +import { SnapEndowments } from '../endowments'; +import type { JsonRpcRequestWithOrigin } from '../types'; +import { MOCK_ANY_NAMESPACE, Messenger, MockAnyNamespace } from '@metamask/messenger'; + +type FooBarAction = { + type: 'Foo:bar'; + handler: (parameter: string) => string; +}; + + +describe('snap_messengerCall', () => { + describe('messengerCallHandler', () => { + it('has the expected shape', () => { + expect(messengerCallHandler).toMatchObject({ + implementation: expect.any(Function), + hookNames: { + getMessenger: true, + }, + actionNames: ['SnapController:getSnap', 'PermissionController:getPermission'], + }); + }); + }); + + describe('implementation', () => { + const getMessenger = (preinstalled = true) => { + const messenger = new MockControllerMessenger< + MessengerCallMethodActions, + never + >(); + + messenger.registerActionHandler('SnapController:getSnap', () => ({ + ...getSnapObject(), + preinstalled, + })); + + messenger.registerActionHandler('PermissionController:getPermission', () => ({ + caveats: [ + { + type: SnapCaveatType.MessengerScopes, + value: { actions: ['Foo:bar'] }, + }, + ], + date: 1661166080905, + id: 'VyAsBJiDDKawv_XlNcm13', + invoker: MOCK_SNAP_ID, + parentCapability: SnapEndowments.Messenger, + })); + + jest.spyOn(messenger, 'call'); + + return messenger; + }; + + it('calls an action', async () => { + const { implementation } = messengerCallHandler; + + const snapMessenger = new Messenger({ namespace: MOCK_ANY_NAMESPACE }); + + snapMessenger.registerActionHandler('Foo:bar', (arg) => arg); + + const getSnapMessenger = jest.fn().mockReturnValue(snapMessenger); + const hooks = { getMessenger: getSnapMessenger }; + + const messenger = getMessenger(); + + const engine = new JsonRpcEngine(); + + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); + engine.push((request, response, next, end) => { + const result = implementation( + request as JsonRpcRequestWithOrigin, + response as PendingJsonRpcResponse, + next, + end, + hooks, + messenger, + ); + + result?.catch(end); + }); + + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'snap_messengerCall', + params: { + action: 'Foo:bar', + params: ['baz'], + }, + }); + + expect(response).toStrictEqual({ + jsonrpc: '2.0', + id: 1, + result: 'baz', + }); + expect(getSnapMessenger).toBeCalledWith(["Foo:bar"], []); + }); + + it('throws an error if the Snap is not preinstalled', async () => { + const { implementation } = messengerCallHandler; + + const getSnapMessenger = jest.fn(); + const hooks = { getMessenger: getSnapMessenger }; + + const messenger = getMessenger(false); + + const engine = new JsonRpcEngine(); + + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); + engine.push((request, response, next, end) => { + const result = implementation( + request as JsonRpcRequestWithOrigin, + response as PendingJsonRpcResponse, + next, + end, + hooks, + messenger, + ); + + result?.catch(end); + }); + + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'snap_messengerCall', + params: { + error: { + name: 'TestError', + message: 'This is a test error.', + stack: + 'Error: This is a test error\n at Object. (test.js:1:1)', + }, + }, + }); + + expect(getSnapMessenger).not.toHaveBeenCalled(); + expect(response).toStrictEqual({ + jsonrpc: '2.0', + id: 1, + error: { + code: -32601, + message: 'The method does not exist / is not available.', + stack: expect.any(String), + }, + }); + }); + + it.each([ + [ + { foo: 'bar' }, + 'Invalid params: At path: action -- Expected a value of type `MessengerAction`, but received: `undefined`.', + ], + [ + { action: 1 }, + 'Invalid params: At path: action -- Expected a value of type `MessengerAction`, but received: `1`.', + ], + [ + { action: "Foo:bar" }, + 'Invalid params: At path: params -- Expected an array value, but received: undefined.', + ], + ])( + 'throws an error if the parameters are invalid', + async (params, error) => { + const { implementation } = messengerCallHandler; + + const snapMessenger = new Messenger({ namespace: MOCK_ANY_NAMESPACE }); + + snapMessenger.registerActionHandler('Foo:bar', (arg) => arg); + + const getSnapMessenger = jest.fn().mockReturnValue(snapMessenger); + + const callSpy = jest.spyOn(snapMessenger, 'call'); + + const hooks = { getMessenger: getSnapMessenger }; + + const messenger = getMessenger(); + + const engine = new JsonRpcEngine(); + + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); + engine.push((request, response, next, end) => { + const result = implementation( + request as JsonRpcRequestWithOrigin, + response as PendingJsonRpcResponse, + next, + end, + hooks, + messenger, + ); + + result?.catch(end); + }); + + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'snap_messengerCall', + params, + }); + + expect(callSpy).not.toHaveBeenCalled(); + expect(response).toStrictEqual({ + jsonrpc: '2.0', + id: 1, + error: { + code: -32602, + message: error, + stack: expect.any(String), + }, + }); + }, + ); + }); +}); diff --git a/packages/snaps-rpc-methods/src/permitted/middleware.test.ts b/packages/snaps-rpc-methods/src/permitted/middleware.test.ts index 83f4144450..f287c91b4b 100644 --- a/packages/snaps-rpc-methods/src/permitted/middleware.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/middleware.test.ts @@ -20,6 +20,7 @@ describe('createSnapsMethodMiddleware', () => { trackEvent: jest.fn(), startTrace: jest.fn(), endTrace: jest.fn(), + getMessenger: jest.fn(), }); const getMessenger = () => diff --git a/packages/snaps-simulation/src/methods/specifications.test.ts b/packages/snaps-simulation/src/methods/specifications.test.ts index e5a7ca11f2..bba6d76413 100644 --- a/packages/snaps-simulation/src/methods/specifications.test.ts +++ b/packages/snaps-simulation/src/methods/specifications.test.ts @@ -1,20 +1,20 @@ -import { mnemonicPhraseToBytes } from '@metamask/key-tree'; -import type { MockAnyNamespace } from '@metamask/messenger'; -import { MOCK_ANY_NAMESPACE, Messenger } from '@metamask/messenger'; +import { mnemonicPhraseToBytes } from "@metamask/key-tree"; +import type { MockAnyNamespace } from "@metamask/messenger"; +import { MOCK_ANY_NAMESPACE, Messenger } from "@metamask/messenger"; import { getSnapManifest, MOCK_SNAP_ID, -} from '@metamask/snaps-utils/test-utils'; +} from "@metamask/snaps-utils/test-utils"; import { asyncResolve, getEndowments, getPermissionSpecifications, resolve, -} from './specifications'; -import { DEFAULT_SRP } from '../constants'; -import { getControllers, registerSnap } from '../controllers'; -import { getMockOptions } from '../test-utils/options'; +} from "./specifications"; +import { DEFAULT_SRP } from "../constants"; +import { getControllers, registerSnap } from "../controllers"; +import { getMockOptions } from "../test-utils/options"; const MOCK_HOOKS = { getClientCryptography: jest.fn(), @@ -22,22 +22,22 @@ const MOCK_HOOKS = { getIsLocked: jest.fn(), }; -describe('resolve', () => { - it('returns a function which resolves with the specified result', async () => { +describe("resolve", () => { + it("returns a function which resolves with the specified result", async () => { const result = {}; expect(await resolve(result)()).toBe(result); }); }); -describe('asyncResolve', () => { - it('returns a function which resolves with the specified result', async () => { +describe("asyncResolve", () => { + it("returns a function which resolves with the specified result", async () => { const result = {}; expect(await asyncResolve(result)()).toBe(result); }); }); -describe('getPermissionSpecifications', () => { - it('returns the permission specifications', async () => { +describe("getPermissionSpecifications", () => { + it("returns the permission specifications", async () => { expect( getPermissionSpecifications({ hooks: MOCK_HOOKS, @@ -45,7 +45,7 @@ describe('getPermissionSpecifications', () => { controllerMessenger: new Messenger({ namespace: MOCK_ANY_NAMESPACE, }), - }), + }) ).toMatchInlineSnapshot(` { "endowment:assets": { @@ -118,6 +118,18 @@ describe('getPermissionSpecifications', () => { "targetName": "endowment:lifecycle-hooks", "validator": [Function], }, + "endowment:messenger": { + "allowedCaveats": [ + "messengerScopes", + ], + "endowmentGetter": [Function], + "permissionType": "Endowment", + "subjectTypes": [ + "snap", + ], + "targetName": "endowment:messenger", + "validator": [Function], + }, "endowment:multichain-provider": { "allowedCaveats": null, "endowmentGetter": [Function], @@ -351,10 +363,10 @@ describe('getPermissionSpecifications', () => { }); }); -describe('getEndowments', () => { - it('returns the endowments', async () => { +describe("getEndowments", () => { + it("returns the endowments", async () => { MOCK_HOOKS.getMnemonic.mockResolvedValue( - mnemonicPhraseToBytes(DEFAULT_SRP), + mnemonicPhraseToBytes(DEFAULT_SRP) ); const controllers = getControllers({ @@ -370,15 +382,15 @@ describe('getEndowments', () => { MOCK_SNAP_ID, getSnapManifest({ initialPermissions: { - 'endowment:network-access': {}, + "endowment:network-access": {}, }, }), - controllers, + controllers ); const endowments = await getEndowments( controllers.permissionController, - MOCK_SNAP_ID, + MOCK_SNAP_ID ); expect(endowments).toMatchInlineSnapshot(` [ diff --git a/packages/snaps-simulation/src/middleware/engine.test.ts b/packages/snaps-simulation/src/middleware/engine.test.ts index 55cbe25b44..9dd13499cc 100644 --- a/packages/snaps-simulation/src/middleware/engine.test.ts +++ b/packages/snaps-simulation/src/middleware/engine.test.ts @@ -33,6 +33,7 @@ describe('createJsonRpcEngine', () => { startTrace: jest.fn(), endTrace: jest.fn(), getAllowedKeyringMethods: jest.fn(), + getMessenger: jest.fn(), }, multichainHooks: { getAccounts: jest.fn(), From 84dd48767a65dcc9e291e67f5c612957efe62a61 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Tue, 30 Jun 2026 14:22:39 +0200 Subject: [PATCH 06/19] Add more tests --- .../src/endowments/messenger.test.ts | 14 ++-- .../snaps-rpc-methods/src/permissions.test.ts | 20 +++--- .../src/permitted/messengerCall.test.ts | 66 +++++++++++-------- packages/snaps-sdk/src/messenger.test.ts | 30 +++++++++ .../snaps-simulation/src/simulation.test.ts | 47 +++++++++++-- 5 files changed, 127 insertions(+), 50 deletions(-) create mode 100644 packages/snaps-sdk/src/messenger.test.ts diff --git a/packages/snaps-rpc-methods/src/endowments/messenger.test.ts b/packages/snaps-rpc-methods/src/endowments/messenger.test.ts index 24b03f3f3f..bbf91fb498 100644 --- a/packages/snaps-rpc-methods/src/endowments/messenger.test.ts +++ b/packages/snaps-rpc-methods/src/endowments/messenger.test.ts @@ -16,9 +16,7 @@ describe('endowment:messenger', () => { permissionType: PermissionType.Endowment, targetName: SnapEndowments.Messenger, endowmentGetter: expect.any(Function), - allowedCaveats: [ - SnapCaveatType.MessengerScopes, - ], + allowedCaveats: [SnapCaveatType.MessengerScopes], subjectTypes: [SubjectType.Snap], validator: expect.any(Function), }); @@ -63,14 +61,14 @@ describe('getMessengerCaveatMapper', () => { it('maps a value to a caveat', () => { expect( getMessengerCaveatMapper({ - actions: ["Foo:bar"], + actions: ['Foo:bar'], }), ).toStrictEqual({ caveats: [ { type: SnapCaveatType.MessengerScopes, value: { - actions: ["Foo:bar"] + actions: ['Foo:bar'], }, }, ], @@ -90,11 +88,11 @@ describe('getMessengerCaveatActions', () => { caveats: [ { type: SnapCaveatType.MessengerScopes, - value: { actions: ["Foo:bar"] } + value: { actions: ['Foo:bar'] }, }, ], }), - ).toStrictEqual(["Foo:bar"]); + ).toStrictEqual(['Foo:bar']); }); it('returns null if no caveat found', () => { @@ -134,7 +132,7 @@ describe('messengerCaveatSpecifications', () => { 'Invalid messenger scopes specified: At path: actions -- Expected an array value, but received: undefined.', ); - expect(() => + expect(() => messengerCaveatSpecifications[ SnapCaveatType.MessengerScopes ].validator?.({ diff --git a/packages/snaps-rpc-methods/src/permissions.test.ts b/packages/snaps-rpc-methods/src/permissions.test.ts index 52a2b26780..49c975f646 100644 --- a/packages/snaps-rpc-methods/src/permissions.test.ts +++ b/packages/snaps-rpc-methods/src/permissions.test.ts @@ -1,12 +1,12 @@ -import { Messenger } from "@metamask/messenger"; +import { Messenger } from '@metamask/messenger'; import { buildSnapEndowmentSpecifications, buildSnapRestrictedMethodSpecifications, -} from "./permissions"; +} from './permissions'; -describe("buildSnapEndowmentSpecifications", () => { - it("returns the expected object", () => { +describe('buildSnapEndowmentSpecifications', () => { + it('returns the expected object', () => { const specifications = buildSnapEndowmentSpecifications([]); expect(specifications).toMatchInlineSnapshot(` { @@ -205,8 +205,8 @@ describe("buildSnapEndowmentSpecifications", () => { }); }); -describe("buildSnapRestrictedMethodSpecifications", () => { - it("returns the expected object", () => { +describe('buildSnapRestrictedMethodSpecifications', () => { + it('returns the expected object', () => { const specifications = buildSnapRestrictedMethodSpecifications( [], { @@ -217,7 +217,7 @@ describe("buildSnapRestrictedMethodSpecifications", () => { getSnapKeyring: jest.fn(), getPreferences: jest.fn(), }, - new Messenger({ namespace: "SnapsRestrictedMethods" }) + new Messenger({ namespace: 'SnapsRestrictedMethods' }), ); expect(specifications).toMatchInlineSnapshot(` { @@ -336,9 +336,9 @@ describe("buildSnapRestrictedMethodSpecifications", () => { `); }); - it("excludes the specified permissions", () => { + it('excludes the specified permissions', () => { const specifications = buildSnapRestrictedMethodSpecifications( - ["snap_dialog"], + ['snap_dialog'], { getUnlockPromise: jest.fn(), getClientCryptography: jest.fn(), @@ -347,7 +347,7 @@ describe("buildSnapRestrictedMethodSpecifications", () => { getSnapKeyring: jest.fn(), getPreferences: jest.fn(), }, - new Messenger({ namespace: "SnapsRestrictedMethods" }) + new Messenger({ namespace: 'SnapsRestrictedMethods' }), ); expect(specifications.snap_dialog).toBeUndefined(); diff --git a/packages/snaps-rpc-methods/src/permitted/messengerCall.test.ts b/packages/snaps-rpc-methods/src/permitted/messengerCall.test.ts index 96395600e7..33b2263841 100644 --- a/packages/snaps-rpc-methods/src/permitted/messengerCall.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/messengerCall.test.ts @@ -2,26 +2,26 @@ import { JsonRpcEngine, createOriginMiddleware, } from '@metamask/json-rpc-engine'; -import type { MessengerCallParams, MessengerCallResult } from '@metamask/snaps-sdk'; +import type { MockAnyNamespace } from '@metamask/messenger'; +import { MOCK_ANY_NAMESPACE, Messenger } from '@metamask/messenger'; +import type { MessengerCallParams } from '@metamask/snaps-sdk'; import { SnapCaveatType } from '@metamask/snaps-utils'; import { MOCK_SNAP_ID, MockControllerMessenger, getSnapObject, } from '@metamask/snaps-utils/test-utils'; -import type { PendingJsonRpcResponse } from '@metamask/utils'; -import { MessengerCallMethodActions, messengerCallHandler } from './messengerCall'; +import type { MessengerCallMethodActions } from './messengerCall'; +import { messengerCallHandler } from './messengerCall'; import { SnapEndowments } from '../endowments'; import type { JsonRpcRequestWithOrigin } from '../types'; -import { MOCK_ANY_NAMESPACE, Messenger, MockAnyNamespace } from '@metamask/messenger'; type FooBarAction = { type: 'Foo:bar'; handler: (parameter: string) => string; }; - describe('snap_messengerCall', () => { describe('messengerCallHandler', () => { it('has the expected shape', () => { @@ -30,7 +30,10 @@ describe('snap_messengerCall', () => { hookNames: { getMessenger: true, }, - actionNames: ['SnapController:getSnap', 'PermissionController:getPermission'], + actionNames: [ + 'SnapController:getSnap', + 'PermissionController:getPermission', + ], }); }); }); @@ -47,18 +50,21 @@ describe('snap_messengerCall', () => { preinstalled, })); - messenger.registerActionHandler('PermissionController:getPermission', () => ({ - caveats: [ - { - type: SnapCaveatType.MessengerScopes, - value: { actions: ['Foo:bar'] }, - }, - ], - date: 1661166080905, - id: 'VyAsBJiDDKawv_XlNcm13', - invoker: MOCK_SNAP_ID, - parentCapability: SnapEndowments.Messenger, - })); + messenger.registerActionHandler( + 'PermissionController:getPermission', + () => ({ + caveats: [ + { + type: SnapCaveatType.MessengerScopes, + value: { actions: ['Foo:bar'] }, + }, + ], + date: 1661166080905, + id: 'VyAsBJiDDKawv_XlNcm13', + invoker: MOCK_SNAP_ID, + parentCapability: SnapEndowments.Messenger, + }), + ); jest.spyOn(messenger, 'call'); @@ -68,7 +74,9 @@ describe('snap_messengerCall', () => { it('calls an action', async () => { const { implementation } = messengerCallHandler; - const snapMessenger = new Messenger({ namespace: MOCK_ANY_NAMESPACE }); + const snapMessenger = new Messenger({ + namespace: MOCK_ANY_NAMESPACE, + }); snapMessenger.registerActionHandler('Foo:bar', (arg) => arg); @@ -83,7 +91,7 @@ describe('snap_messengerCall', () => { engine.push((request, response, next, end) => { const result = implementation( request as JsonRpcRequestWithOrigin, - response as PendingJsonRpcResponse, + response, next, end, hooks, @@ -108,7 +116,7 @@ describe('snap_messengerCall', () => { id: 1, result: 'baz', }); - expect(getSnapMessenger).toBeCalledWith(["Foo:bar"], []); + expect(getSnapMessenger).toHaveBeenCalledWith(['Foo:bar'], []); }); it('throws an error if the Snap is not preinstalled', async () => { @@ -125,7 +133,7 @@ describe('snap_messengerCall', () => { engine.push((request, response, next, end) => { const result = implementation( request as JsonRpcRequestWithOrigin, - response as PendingJsonRpcResponse, + response, next, end, hooks, @@ -171,7 +179,7 @@ describe('snap_messengerCall', () => { 'Invalid params: At path: action -- Expected a value of type `MessengerAction`, but received: `1`.', ], [ - { action: "Foo:bar" }, + { action: 'Foo:bar' }, 'Invalid params: At path: params -- Expected an array value, but received: undefined.', ], ])( @@ -179,13 +187,15 @@ describe('snap_messengerCall', () => { async (params, error) => { const { implementation } = messengerCallHandler; - const snapMessenger = new Messenger({ namespace: MOCK_ANY_NAMESPACE }); + const snapMessenger = new Messenger({ + namespace: MOCK_ANY_NAMESPACE, + }); - snapMessenger.registerActionHandler('Foo:bar', (arg) => arg); + snapMessenger.registerActionHandler('Foo:bar', (arg) => arg); - const getSnapMessenger = jest.fn().mockReturnValue(snapMessenger); + const getSnapMessenger = jest.fn().mockReturnValue(snapMessenger); - const callSpy = jest.spyOn(snapMessenger, 'call'); + const callSpy = jest.spyOn(snapMessenger, 'call'); const hooks = { getMessenger: getSnapMessenger }; @@ -197,7 +207,7 @@ describe('snap_messengerCall', () => { engine.push((request, response, next, end) => { const result = implementation( request as JsonRpcRequestWithOrigin, - response as PendingJsonRpcResponse, + response, next, end, hooks, diff --git a/packages/snaps-sdk/src/messenger.test.ts b/packages/snaps-sdk/src/messenger.test.ts new file mode 100644 index 0000000000..4252eb8273 --- /dev/null +++ b/packages/snaps-sdk/src/messenger.test.ts @@ -0,0 +1,30 @@ +import { getMessenger } from './messenger'; +import type { SnapsProvider } from './types'; + +type FooBarAction = { + type: 'Foo:bar'; + handler: (parameter: string) => string; +}; + +describe('getMessenger', () => { + it('calls `snap_messengerCall` with the action and params', async () => { + const request = jest + .fn() + .mockImplementation(async ({ params }) => params.params[0]); + + Object.assign(globalThis, { + snap: { request } as unknown as SnapsProvider, + }); + + const messenger = getMessenger(); + + const result = await messenger.call('Foo:bar', 'baz'); + + expect(result).toBe('baz'); + + expect(request).toHaveBeenCalledWith({ + method: 'snap_messengerCall', + params: { action: 'Foo:bar', params: ['baz'] }, + }); + }); +}); diff --git a/packages/snaps-simulation/src/simulation.test.ts b/packages/snaps-simulation/src/simulation.test.ts index 05342126cb..fe556882f7 100644 --- a/packages/snaps-simulation/src/simulation.test.ts +++ b/packages/snaps-simulation/src/simulation.test.ts @@ -254,29 +254,68 @@ describe('getRestrictedHooks', () => { describe('getPermittedHooks', () => { const { runSaga } = createStore(getMockOptions()); + let controllerMessenger = getRootControllerMessenger(); + + beforeEach(() => { + controllerMessenger = getRootControllerMessenger(); + }); + it('returns the `getUnlockPromise` hook', async () => { - const { getUnlockPromise } = getPermittedHooks(runSaga); + const { getUnlockPromise } = getPermittedHooks( + MOCK_SNAP_ID, + controllerMessenger, + runSaga, + ); expect(await getUnlockPromise(true)).toBeUndefined(); }); it('returns the `getIsActive` hook', async () => { - const { getIsActive } = getPermittedHooks(runSaga); + const { getIsActive } = getPermittedHooks( + MOCK_SNAP_ID, + controllerMessenger, + runSaga, + ); expect(getIsActive()).toBe(true); }); it('returns the `getVersion` hook', async () => { - const { getVersion } = getPermittedHooks(runSaga); + const { getVersion } = getPermittedHooks( + MOCK_SNAP_ID, + controllerMessenger, + runSaga, + ); expect(getVersion()).toBe('13.6.0-flask.0'); }); it('returns the `getAllowedKeyringMethods` hook', async () => { - const { getAllowedKeyringMethods } = getPermittedHooks(runSaga); + const { getAllowedKeyringMethods } = getPermittedHooks( + MOCK_SNAP_ID, + controllerMessenger, + runSaga, + ); expect(getAllowedKeyringMethods()).toStrictEqual([]); }); + + it('returns the `getMessenger` hook', () => { + const { getMessenger } = getPermittedHooks( + MOCK_SNAP_ID, + controllerMessenger, + runSaga, + ); + + const messenger = getMessenger(['PhishingController:testOrigin'], []); + + expect( + messenger.call('PhishingController:testOrigin', 'https://example.com'), + ).toStrictEqual({ + result: false, + type: 'all', + }); + }); }); describe('getMultichainHooks', () => { From 8320a7d1122b504e1289e01049b68f4c081d1089 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 1 Jul 2026 10:53:36 +0200 Subject: [PATCH 07/19] Fix lint --- .../src/methods/specifications.test.ts | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/snaps-simulation/src/methods/specifications.test.ts b/packages/snaps-simulation/src/methods/specifications.test.ts index bba6d76413..e3dba27b23 100644 --- a/packages/snaps-simulation/src/methods/specifications.test.ts +++ b/packages/snaps-simulation/src/methods/specifications.test.ts @@ -1,20 +1,20 @@ -import { mnemonicPhraseToBytes } from "@metamask/key-tree"; -import type { MockAnyNamespace } from "@metamask/messenger"; -import { MOCK_ANY_NAMESPACE, Messenger } from "@metamask/messenger"; +import { mnemonicPhraseToBytes } from '@metamask/key-tree'; +import type { MockAnyNamespace } from '@metamask/messenger'; +import { MOCK_ANY_NAMESPACE, Messenger } from '@metamask/messenger'; import { getSnapManifest, MOCK_SNAP_ID, -} from "@metamask/snaps-utils/test-utils"; +} from '@metamask/snaps-utils/test-utils'; import { asyncResolve, getEndowments, getPermissionSpecifications, resolve, -} from "./specifications"; -import { DEFAULT_SRP } from "../constants"; -import { getControllers, registerSnap } from "../controllers"; -import { getMockOptions } from "../test-utils/options"; +} from './specifications'; +import { DEFAULT_SRP } from '../constants'; +import { getControllers, registerSnap } from '../controllers'; +import { getMockOptions } from '../test-utils/options'; const MOCK_HOOKS = { getClientCryptography: jest.fn(), @@ -22,22 +22,22 @@ const MOCK_HOOKS = { getIsLocked: jest.fn(), }; -describe("resolve", () => { - it("returns a function which resolves with the specified result", async () => { +describe('resolve', () => { + it('returns a function which resolves with the specified result', async () => { const result = {}; expect(await resolve(result)()).toBe(result); }); }); -describe("asyncResolve", () => { - it("returns a function which resolves with the specified result", async () => { +describe('asyncResolve', () => { + it('returns a function which resolves with the specified result', async () => { const result = {}; expect(await asyncResolve(result)()).toBe(result); }); }); -describe("getPermissionSpecifications", () => { - it("returns the permission specifications", async () => { +describe('getPermissionSpecifications', () => { + it('returns the permission specifications', async () => { expect( getPermissionSpecifications({ hooks: MOCK_HOOKS, @@ -45,7 +45,7 @@ describe("getPermissionSpecifications", () => { controllerMessenger: new Messenger({ namespace: MOCK_ANY_NAMESPACE, }), - }) + }), ).toMatchInlineSnapshot(` { "endowment:assets": { @@ -363,10 +363,10 @@ describe("getPermissionSpecifications", () => { }); }); -describe("getEndowments", () => { - it("returns the endowments", async () => { +describe('getEndowments', () => { + it('returns the endowments', async () => { MOCK_HOOKS.getMnemonic.mockResolvedValue( - mnemonicPhraseToBytes(DEFAULT_SRP) + mnemonicPhraseToBytes(DEFAULT_SRP), ); const controllers = getControllers({ @@ -382,15 +382,15 @@ describe("getEndowments", () => { MOCK_SNAP_ID, getSnapManifest({ initialPermissions: { - "endowment:network-access": {}, + 'endowment:network-access': {}, }, }), - controllers + controllers, ); const endowments = await getEndowments( controllers.permissionController, - MOCK_SNAP_ID + MOCK_SNAP_ID, ); expect(endowments).toMatchInlineSnapshot(` [ From 42280a1913a92bd11d58ff9ee1a3774eb49a375f Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 1 Jul 2026 12:30:26 +0200 Subject: [PATCH 08/19] Add test-snaps button --- .../features/snaps/preinstalled/Preinstalled.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/test-snaps/src/features/snaps/preinstalled/Preinstalled.tsx b/packages/test-snaps/src/features/snaps/preinstalled/Preinstalled.tsx index 43b3354874..7eb5f68708 100644 --- a/packages/test-snaps/src/features/snaps/preinstalled/Preinstalled.tsx +++ b/packages/test-snaps/src/features/snaps/preinstalled/Preinstalled.tsx @@ -51,6 +51,13 @@ export const Preinstalled: FunctionComponent = () => { }).catch(logError); }; + const handleMessengerCall = () => { + invokeSnap({ + snapId: PREINSTALLED_SNAP_ID, + method: 'messengerCall', + }).catch(logError); + }; + return ( { > End trace + From 168bd7c9420bea6cb47694b8db3371f22723aec9 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 1 Jul 2026 12:44:47 +0200 Subject: [PATCH 09/19] Add missing test --- .../src/permitted/messengerCall.test.ts | 59 +++++++++++++++++-- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/packages/snaps-rpc-methods/src/permitted/messengerCall.test.ts b/packages/snaps-rpc-methods/src/permitted/messengerCall.test.ts index 33b2263841..b1e2006096 100644 --- a/packages/snaps-rpc-methods/src/permitted/messengerCall.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/messengerCall.test.ts @@ -148,12 +148,59 @@ describe('snap_messengerCall', () => { id: 1, method: 'snap_messengerCall', params: { - error: { - name: 'TestError', - message: 'This is a test error.', - stack: - 'Error: This is a test error\n at Object. (test.js:1:1)', - }, + action: 'Foo:bar', + params: [], + }, + }); + + expect(getSnapMessenger).not.toHaveBeenCalled(); + expect(response).toStrictEqual({ + jsonrpc: '2.0', + id: 1, + error: { + code: -32601, + message: 'The method does not exist / is not available.', + stack: expect.any(String), + }, + }); + }); + + it('throws an error if the Snap does not have the messenger endowment', async () => { + const { implementation } = messengerCallHandler; + + const getSnapMessenger = jest.fn(); + const hooks = { getMessenger: getSnapMessenger }; + + const messenger = getMessenger(true); + + messenger.registerActionHandler( + 'PermissionController:getPermission', + () => undefined, + ); + + const engine = new JsonRpcEngine(); + + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); + engine.push((request, response, next, end) => { + const result = implementation( + request as JsonRpcRequestWithOrigin, + response, + next, + end, + hooks, + messenger, + ); + + result?.catch(end); + }); + + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'snap_messengerCall', + params: { + action: 'Foo:bar', + params: [], }, }); From f0bbfc6f5f8ee7988604a7a61a43716808c9f47e Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 1 Jul 2026 12:52:52 +0200 Subject: [PATCH 10/19] Ban access to some messenger clients --- packages/snaps-rpc-methods/jest.config.js | 6 +- .../src/permitted/messengerCall.test.ts | 63 +++++++++++++++++++ .../src/permitted/messengerCall.ts | 16 ++++- 3 files changed, 81 insertions(+), 4 deletions(-) diff --git a/packages/snaps-rpc-methods/jest.config.js b/packages/snaps-rpc-methods/jest.config.js index bb49bd19e5..49b9744d9b 100644 --- a/packages/snaps-rpc-methods/jest.config.js +++ b/packages/snaps-rpc-methods/jest.config.js @@ -10,10 +10,10 @@ module.exports = deepmerge(baseConfig, { ], coverageThreshold: { global: { - branches: 97.13, - functions: 98.91, + branches: 97.14, + functions: 98.92, lines: 99.22, - statements: 98.94, + statements: 98.95, }, }, }); diff --git a/packages/snaps-rpc-methods/src/permitted/messengerCall.test.ts b/packages/snaps-rpc-methods/src/permitted/messengerCall.test.ts index b1e2006096..e7f3958200 100644 --- a/packages/snaps-rpc-methods/src/permitted/messengerCall.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/messengerCall.test.ts @@ -216,6 +216,69 @@ describe('snap_messengerCall', () => { }); }); + it('throws an error if the Snap calls a disallowed action', async () => { + const { implementation } = messengerCallHandler; + + const getSnapMessenger = jest.fn(); + const hooks = { getMessenger: getSnapMessenger }; + + const messenger = getMessenger(true); + + messenger.registerActionHandler( + 'PermissionController:getPermission', + () => ({ + caveats: [ + { + type: SnapCaveatType.MessengerScopes, + value: { actions: ['PermissionController:getPermission'] }, + }, + ], + date: 1661166080905, + id: 'VyAsBJiDDKawv_XlNcm13', + invoker: MOCK_SNAP_ID, + parentCapability: SnapEndowments.Messenger, + }), + ); + + const engine = new JsonRpcEngine(); + + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); + engine.push((request, response, next, end) => { + const result = implementation( + request as JsonRpcRequestWithOrigin, + response, + next, + end, + hooks, + messenger, + ); + + result?.catch(end); + }); + + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'snap_messengerCall', + params: { + action: 'PermissionController:getPermission', + params: [], + }, + }); + + expect(response).toStrictEqual({ + jsonrpc: '2.0', + id: 1, + error: { + code: -32602, + message: expect.stringContaining( + 'Access to the following messenger clients are disallowed in Snaps:', + ), + stack: expect.any(String), + }, + }); + }); + it.each([ [ { foo: 'bar' }, diff --git a/packages/snaps-rpc-methods/src/permitted/messengerCall.ts b/packages/snaps-rpc-methods/src/permitted/messengerCall.ts index 6ad056690a..9f01bc6989 100644 --- a/packages/snaps-rpc-methods/src/permitted/messengerCall.ts +++ b/packages/snaps-rpc-methods/src/permitted/messengerCall.ts @@ -42,6 +42,8 @@ export type MessengerCallMethodActions = | SnapControllerGetSnapAction | PermissionControllerGetPermissionAction; +const BLOCKED_MESSENGER_CLIENTS = ['KeyringController', 'PermissionController']; + const MessengerCallParametersStruct = object({ action: definePattern<`${string}:${string}`>('MessengerAction', /^.+:.+$/u), params: array(JsonStruct), @@ -104,13 +106,25 @@ async function getMessengerCallImplementation( const actions = getMessengerCaveatActions(permission); - // TODO: Consider if we need to ban any actions. const snapMessenger = getMessenger(actions ?? [], []); const { params } = request; try { const { action, params: actionParams } = getValidatedParams(params); + + if ( + BLOCKED_MESSENGER_CLIENTS.some((namespace) => + action.startsWith(namespace), + ) + ) { + return end( + rpcErrors.invalidParams({ + message: `Access to the following messenger clients are disallowed in Snaps: ${BLOCKED_MESSENGER_CLIENTS.join(', ')}.`, + }), + ); + } + response.result = await snapMessenger.call(action, ...actionParams); } catch (error) { return end(error); From acb89d4c061d43f27621e063c8c14857197f38b3 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 1 Jul 2026 13:08:10 +0200 Subject: [PATCH 11/19] Tweak getMessenger hook type --- packages/snaps-rpc-methods/jest.config.js | 2 +- .../src/permitted/messengerCall.test.ts | 2 +- .../snaps-rpc-methods/src/permitted/messengerCall.ts | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/snaps-rpc-methods/jest.config.js b/packages/snaps-rpc-methods/jest.config.js index 49b9744d9b..aa77775adc 100644 --- a/packages/snaps-rpc-methods/jest.config.js +++ b/packages/snaps-rpc-methods/jest.config.js @@ -10,7 +10,7 @@ module.exports = deepmerge(baseConfig, { ], coverageThreshold: { global: { - branches: 97.14, + branches: 97.38, functions: 98.92, lines: 99.22, statements: 98.95, diff --git a/packages/snaps-rpc-methods/src/permitted/messengerCall.test.ts b/packages/snaps-rpc-methods/src/permitted/messengerCall.test.ts index e7f3958200..a97f01cb44 100644 --- a/packages/snaps-rpc-methods/src/permitted/messengerCall.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/messengerCall.test.ts @@ -116,7 +116,7 @@ describe('snap_messengerCall', () => { id: 1, result: 'baz', }); - expect(getSnapMessenger).toHaveBeenCalledWith(['Foo:bar'], []); + expect(getSnapMessenger).toHaveBeenCalledWith({ actions: ['Foo:bar'] }); }); it('throws an error if the Snap is not preinstalled', async () => { diff --git a/packages/snaps-rpc-methods/src/permitted/messengerCall.ts b/packages/snaps-rpc-methods/src/permitted/messengerCall.ts index 9f01bc6989..1ddc574c53 100644 --- a/packages/snaps-rpc-methods/src/permitted/messengerCall.ts +++ b/packages/snaps-rpc-methods/src/permitted/messengerCall.ts @@ -32,10 +32,10 @@ type GenericMessengerAction = { }; export type MessengerCallMethodHooks = { - getMessenger: ( - actions: string[], - events: string[], - ) => Messenger; + getMessenger: (args: { + actions?: string[] | null; + events?: string[] | null; + }) => Messenger; }; export type MessengerCallMethodActions = @@ -106,7 +106,7 @@ async function getMessengerCallImplementation( const actions = getMessengerCaveatActions(permission); - const snapMessenger = getMessenger(actions ?? [], []); + const snapMessenger = getMessenger({ actions }); const { params } = request; From a0b9c3ecf56d4d18dd977da2da800f426fe31e53 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 1 Jul 2026 13:09:28 +0200 Subject: [PATCH 12/19] Ban ApprovalController too --- packages/snaps-rpc-methods/src/permitted/messengerCall.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/snaps-rpc-methods/src/permitted/messengerCall.ts b/packages/snaps-rpc-methods/src/permitted/messengerCall.ts index 1ddc574c53..4997ad6a36 100644 --- a/packages/snaps-rpc-methods/src/permitted/messengerCall.ts +++ b/packages/snaps-rpc-methods/src/permitted/messengerCall.ts @@ -42,7 +42,11 @@ export type MessengerCallMethodActions = | SnapControllerGetSnapAction | PermissionControllerGetPermissionAction; -const BLOCKED_MESSENGER_CLIENTS = ['KeyringController', 'PermissionController']; +const BLOCKED_MESSENGER_CLIENTS = [ + 'ApprovalController', + 'KeyringController', + 'PermissionController', +]; const MessengerCallParametersStruct = object({ action: definePattern<`${string}:${string}`>('MessengerAction', /^.+:.+$/u), From 920d597615b35a87b7b0787a093acc4fb6bea8c8 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 1 Jul 2026 13:19:53 +0200 Subject: [PATCH 13/19] Fix simulation hook --- packages/snaps-simulation/src/simulation.test.ts | 4 +++- packages/snaps-simulation/src/simulation.ts | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/snaps-simulation/src/simulation.test.ts b/packages/snaps-simulation/src/simulation.test.ts index fe556882f7..8d0496d483 100644 --- a/packages/snaps-simulation/src/simulation.test.ts +++ b/packages/snaps-simulation/src/simulation.test.ts @@ -307,7 +307,9 @@ describe('getPermittedHooks', () => { runSaga, ); - const messenger = getMessenger(['PhishingController:testOrigin'], []); + const messenger = getMessenger({ + actions: ['PhishingController:testOrigin'], + }); expect( messenger.call('PhishingController:testOrigin', 'https://example.com'), diff --git a/packages/snaps-simulation/src/simulation.ts b/packages/snaps-simulation/src/simulation.ts index 07fd7db30d..b38aa0a272 100644 --- a/packages/snaps-simulation/src/simulation.ts +++ b/packages/snaps-simulation/src/simulation.ts @@ -236,7 +236,10 @@ export type PermittedMiddlewareHooks = { * * @returns The messenger. */ - getMessenger(actions: string[], events: string[]): Messenger; + getMessenger(args: { + actions?: string[]; + events?: string[]; + }): Messenger; }; export type MultichainMiddlewareHooks = { @@ -486,7 +489,7 @@ export function getPermittedHooks( startTrace: getStartTraceImplementation(runSaga), endTrace: getEndTraceImplementation(runSaga), - getMessenger: (actions, events) => { + getMessenger: ({ actions, events }) => { const messenger = new Messenger({ namespace: `${snapId}-messenger`, parent: controllerMessenger, From dddebfb299e89ee4f6e11e874586c2a602ace27c Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 1 Jul 2026 14:44:35 +0200 Subject: [PATCH 14/19] Tweak error message --- packages/snaps-rpc-methods/src/endowments/messenger.test.ts | 2 +- packages/snaps-rpc-methods/src/endowments/messenger.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/snaps-rpc-methods/src/endowments/messenger.test.ts b/packages/snaps-rpc-methods/src/endowments/messenger.test.ts index bbf91fb498..de38ceaaa1 100644 --- a/packages/snaps-rpc-methods/src/endowments/messenger.test.ts +++ b/packages/snaps-rpc-methods/src/endowments/messenger.test.ts @@ -117,7 +117,7 @@ describe('messengerCaveatSpecifications', () => { type: SnapCaveatType.MessengerScopes, }, ), - ).toThrow('Expected a plain object.'); + ).toThrow('Expected an object.'); expect(() => messengerCaveatSpecifications[ diff --git a/packages/snaps-rpc-methods/src/endowments/messenger.ts b/packages/snaps-rpc-methods/src/endowments/messenger.ts index 87c71439a3..b68ae8e551 100644 --- a/packages/snaps-rpc-methods/src/endowments/messenger.ts +++ b/packages/snaps-rpc-methods/src/endowments/messenger.ts @@ -112,7 +112,7 @@ export function getMessengerCaveatActions( function validateCaveat(caveat: Caveat): void { if (!hasProperty(caveat, 'value') || !isObject(caveat)) { throw rpcErrors.invalidParams({ - message: 'Expected a plain object.', + message: 'Expected an object.', }); } From 16342f875c8ede4f8ee8196bbba1ad898cd2f555 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 1 Jul 2026 14:57:43 +0200 Subject: [PATCH 15/19] Make actions optional in struct --- packages/snaps-utils/src/manifest/validation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/snaps-utils/src/manifest/validation.ts b/packages/snaps-utils/src/manifest/validation.ts index 938bc5dca4..c1c9dc8a8c 100644 --- a/packages/snaps-utils/src/manifest/validation.ts +++ b/packages/snaps-utils/src/manifest/validation.ts @@ -195,7 +195,7 @@ export const ProtocolScopesStruct = record( export const MessengerActionsStruct = size(array(string()), 1, Infinity); export const MessengerScopesStruct = object({ - actions: MessengerActionsStruct, + actions: optional(MessengerActionsStruct), }); // Utility type to union with for all handler structs From ec45717953a3e08bb92fc2a73e3dad950e9c6524 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 1 Jul 2026 15:43:52 +0200 Subject: [PATCH 16/19] Update error message assertion --- packages/snaps-rpc-methods/src/endowments/messenger.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/snaps-rpc-methods/src/endowments/messenger.test.ts b/packages/snaps-rpc-methods/src/endowments/messenger.test.ts index de38ceaaa1..cd290890ec 100644 --- a/packages/snaps-rpc-methods/src/endowments/messenger.test.ts +++ b/packages/snaps-rpc-methods/src/endowments/messenger.test.ts @@ -129,7 +129,7 @@ describe('messengerCaveatSpecifications', () => { }, }), ).toThrow( - 'Invalid messenger scopes specified: At path: actions -- Expected an array value, but received: undefined.', + 'Invalid messenger scopes specified: At path: foo -- Expected a value of type `never`, but received: `"bar"`', ); expect(() => From f1e92f070a872dbdd3f51b371dec6df21243e3b4 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 2 Jul 2026 11:06:29 +0200 Subject: [PATCH 17/19] Use Messenger in getMessenger --- .../packages/preinstalled/package.json | 1 + .../packages/preinstalled/src/index.tsx | 5 +- packages/snaps-sdk/package.json | 1 + packages/snaps-sdk/src/messenger.test.ts | 6 +- packages/snaps-sdk/src/messenger.ts | 81 ++++++++----------- yarn.lock | 2 + 6 files changed, 46 insertions(+), 50 deletions(-) diff --git a/packages/examples/packages/preinstalled/package.json b/packages/examples/packages/preinstalled/package.json index ced673ddcb..de1218e89d 100644 --- a/packages/examples/packages/preinstalled/package.json +++ b/packages/examples/packages/preinstalled/package.json @@ -46,6 +46,7 @@ "@jest/globals": "^29.5.0", "@lavamoat/allow-scripts": "^4.0.0", "@metamask/auto-changelog": "^6.1.1", + "@metamask/messenger": "^1.2.0", "@metamask/snaps-cli": "workspace:^", "@metamask/snaps-jest": "workspace:^", "@metamask/snaps-sdk": "workspace:^", diff --git a/packages/examples/packages/preinstalled/src/index.tsx b/packages/examples/packages/preinstalled/src/index.tsx index 9f6ad4897a..dcff9876ab 100644 --- a/packages/examples/packages/preinstalled/src/index.tsx +++ b/packages/examples/packages/preinstalled/src/index.tsx @@ -1,3 +1,4 @@ +import type { Messenger } from '@metamask/messenger'; import { getJsonError, getMessenger, @@ -23,6 +24,8 @@ type PhishingControllerTestOrigin = { handler: (origin: string) => { result: boolean; type: string }; }; +type SnapMessenger = Messenger; + /** * Handle incoming JSON-RPC requests from the dapp, sent through the * `wallet_invokeSnap` method. This handler handles the following methods: @@ -104,7 +107,7 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { } case 'messengerCall': { - const messenger = getMessenger(); + const messenger = getMessenger(); const { result } = await messenger.call( 'PhishingController:testOrigin', 'https://metamask.io', diff --git a/packages/snaps-sdk/package.json b/packages/snaps-sdk/package.json index 99acf0f9a6..9c8ec4f6b6 100644 --- a/packages/snaps-sdk/package.json +++ b/packages/snaps-sdk/package.json @@ -93,6 +93,7 @@ }, "dependencies": { "@metamask/key-tree": "^10.1.1", + "@metamask/messenger": "^1.2.0", "@metamask/providers": "^22.1.1", "@metamask/rpc-errors": "^7.0.3", "@metamask/superstruct": "^3.3.0", diff --git a/packages/snaps-sdk/src/messenger.test.ts b/packages/snaps-sdk/src/messenger.test.ts index 4252eb8273..1487b403ab 100644 --- a/packages/snaps-sdk/src/messenger.test.ts +++ b/packages/snaps-sdk/src/messenger.test.ts @@ -1,3 +1,5 @@ +import type { Messenger } from '@metamask/messenger'; + import { getMessenger } from './messenger'; import type { SnapsProvider } from './types'; @@ -6,6 +8,8 @@ type FooBarAction = { handler: (parameter: string) => string; }; +type FooMessenger = Messenger<'Foo', FooBarAction>; + describe('getMessenger', () => { it('calls `snap_messengerCall` with the action and params', async () => { const request = jest @@ -16,7 +20,7 @@ describe('getMessenger', () => { snap: { request } as unknown as SnapsProvider, }); - const messenger = getMessenger(); + const messenger = getMessenger(); const result = await messenger.call('Foo:bar', 'baz'); diff --git a/packages/snaps-sdk/src/messenger.ts b/packages/snaps-sdk/src/messenger.ts index 1e7b598425..fe31f7103b 100644 --- a/packages/snaps-sdk/src/messenger.ts +++ b/packages/snaps-sdk/src/messenger.ts @@ -1,55 +1,37 @@ -/** - * A constraint type matching a single messenger action. - */ -type ActionConstraint = { - type: `${string}:${string}`; - handler: (...args: never[]) => unknown; -}; - -/** - * Extract the parameters of a messenger action by its type. - * - * @template Action - The union of actions to extract from. - * @template ActionType - The type of the action to extract the parameters of. - */ -type ExtractActionParameters< - Action extends ActionConstraint, - ActionType extends Action['type'], -> = Action extends { - type: ActionType; - handler: (...args: infer Params) => unknown; -} - ? Params - : never; +import type { + ActionConstraint, + EventConstraint, + ExtractActionParameters, + ExtractActionResponse, + Messenger, + MessengerActions, + NamespacedName, +} from '@metamask/messenger'; +import type { Json } from '@metamask/utils'; /** - * Extract the response of a messenger action by its type. - * - * @template Action - The union of actions to extract from. - * @template ActionType - The type of the action to extract the response of. + * A constraint type matching any messenger. Used to constrain the generic + * messenger type accepted by {@link getMessenger} and {@link AsyncMessenger}. */ -type ExtractActionResponse< - Action extends ActionConstraint, - ActionType extends Action['type'], -> = Action extends { - type: ActionType; - handler: (...args: never[]) => infer Response; -} - ? Response - : never; +type MessengerConstraint = Messenger; /** * An asynchronous version of a messenger's `call` method. Calls made from * within a Snap are dispatched to the client via the `snap_messengerCall` * method, so the original (synchronous) return type is wrapped in a `Promise`. * - * @template Action - The union of actions the messenger can call. + * @template MessengerType - The messenger the Snap can call actions on. */ -export type AsyncMessenger = { - call: ( +export type AsyncMessenger = { + call: ['type']>( actionType: ActionType, - ...params: ExtractActionParameters - ) => Promise>>; + ...params: ExtractActionParameters< + MessengerActions, + ActionType + > + ) => Promise< + Awaited, ActionType>> + >; }; /** @@ -58,17 +40,20 @@ export type AsyncMessenger = { * * Note that this is only available to preinstalled Snaps. * - * @template Action - The union of actions the messenger can call. + * @template MessengerType - The messenger the Snap can call actions on. This is + * a `Messenger` type from `@metamask/messenger`; the callable actions are + * derived from it automatically. * @returns An asynchronous messenger. */ export function getMessenger< - Action extends ActionConstraint, ->(): AsyncMessenger { + MessengerType extends MessengerConstraint, +>(): AsyncMessenger { return { - call: async (action, ...params) => - snap.request({ + async call(action: NamespacedName, ...params: Json[]) { + return snap.request({ method: 'snap_messengerCall', params: { action, params }, - }), - } as AsyncMessenger; + }); + }, + } as AsyncMessenger; } diff --git a/yarn.lock b/yarn.lock index 3f0d47a2b9..56d57e9e37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4177,6 +4177,7 @@ __metadata: "@jest/globals": "npm:^29.5.0" "@lavamoat/allow-scripts": "npm:^4.0.0" "@metamask/auto-changelog": "npm:^6.1.1" + "@metamask/messenger": "npm:^1.2.0" "@metamask/snaps-cli": "workspace:^" "@metamask/snaps-jest": "workspace:^" "@metamask/snaps-sdk": "workspace:^" @@ -4768,6 +4769,7 @@ __metadata: "@lavamoat/allow-scripts": "npm:^4.0.0" "@metamask/auto-changelog": "npm:^6.1.1" "@metamask/key-tree": "npm:^10.1.1" + "@metamask/messenger": "npm:^1.2.0" "@metamask/providers": "npm:^22.1.1" "@metamask/rpc-errors": "npm:^7.0.3" "@metamask/superstruct": "npm:^3.3.0" From 7c2d8d0be44db6fd8274bed1def5f1dbf6648a41 Mon Sep 17 00:00:00 2001 From: MetaMask Bot Date: Thu, 2 Jul 2026 10:29:20 +0000 Subject: [PATCH 18/19] Update LavaMoat policies --- .../lavamoat/webpack/iframe/policy.json | 2 +- .../lavamoat/webpack/node-process/policy.json | 2 +- .../lavamoat/webpack/node-thread/policy.json | 2 +- .../lavamoat/webpack/webview/policy.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/snaps-execution-environments/lavamoat/webpack/iframe/policy.json b/packages/snaps-execution-environments/lavamoat/webpack/iframe/policy.json index 56b910bd38..e1c41fe5ab 100644 --- a/packages/snaps-execution-environments/lavamoat/webpack/iframe/policy.json +++ b/packages/snaps-execution-environments/lavamoat/webpack/iframe/policy.json @@ -2,7 +2,7 @@ "resources": { "@metamask/json-rpc-engine": { "packages": { - "@metamask/snaps-utils>@metamask/messenger": true, + "@metamask/snaps-sdk>@metamask/messenger": true, "@metamask/rpc-errors": true, "@metamask/providers>@metamask/safe-event-emitter": true, "@metamask/utils": true diff --git a/packages/snaps-execution-environments/lavamoat/webpack/node-process/policy.json b/packages/snaps-execution-environments/lavamoat/webpack/node-process/policy.json index 261dab6ff3..12ca2abd10 100644 --- a/packages/snaps-execution-environments/lavamoat/webpack/node-process/policy.json +++ b/packages/snaps-execution-environments/lavamoat/webpack/node-process/policy.json @@ -2,7 +2,7 @@ "resources": { "@metamask/json-rpc-engine": { "packages": { - "@metamask/snaps-utils>@metamask/messenger": true, + "@metamask/snaps-sdk>@metamask/messenger": true, "@metamask/rpc-errors": true, "@metamask/providers>@metamask/safe-event-emitter": true, "@metamask/utils": true diff --git a/packages/snaps-execution-environments/lavamoat/webpack/node-thread/policy.json b/packages/snaps-execution-environments/lavamoat/webpack/node-thread/policy.json index 261dab6ff3..12ca2abd10 100644 --- a/packages/snaps-execution-environments/lavamoat/webpack/node-thread/policy.json +++ b/packages/snaps-execution-environments/lavamoat/webpack/node-thread/policy.json @@ -2,7 +2,7 @@ "resources": { "@metamask/json-rpc-engine": { "packages": { - "@metamask/snaps-utils>@metamask/messenger": true, + "@metamask/snaps-sdk>@metamask/messenger": true, "@metamask/rpc-errors": true, "@metamask/providers>@metamask/safe-event-emitter": true, "@metamask/utils": true diff --git a/packages/snaps-execution-environments/lavamoat/webpack/webview/policy.json b/packages/snaps-execution-environments/lavamoat/webpack/webview/policy.json index 56b910bd38..e1c41fe5ab 100644 --- a/packages/snaps-execution-environments/lavamoat/webpack/webview/policy.json +++ b/packages/snaps-execution-environments/lavamoat/webpack/webview/policy.json @@ -2,7 +2,7 @@ "resources": { "@metamask/json-rpc-engine": { "packages": { - "@metamask/snaps-utils>@metamask/messenger": true, + "@metamask/snaps-sdk>@metamask/messenger": true, "@metamask/rpc-errors": true, "@metamask/providers>@metamask/safe-event-emitter": true, "@metamask/utils": true From 61fb96e09ca24439b586a38d822aac8bb97b3796 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Fri, 3 Jul 2026 10:48:55 +0200 Subject: [PATCH 19/19] Block direct access to APIs already exposed to Snaps --- packages/snaps-rpc-methods/src/permitted/messengerCall.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/snaps-rpc-methods/src/permitted/messengerCall.ts b/packages/snaps-rpc-methods/src/permitted/messengerCall.ts index 4997ad6a36..aeef727fa8 100644 --- a/packages/snaps-rpc-methods/src/permitted/messengerCall.ts +++ b/packages/snaps-rpc-methods/src/permitted/messengerCall.ts @@ -46,6 +46,14 @@ const BLOCKED_MESSENGER_CLIENTS = [ 'ApprovalController', 'KeyringController', 'PermissionController', + 'SnapController', + 'WebSocketService', + 'CronjobController', + 'SnapInsightsController', + 'SnapInterfaceController', + 'MultichainRoutingService', + 'ExecutionService', + 'SnapRegistryController', ]; const MessengerCallParametersStruct = object({