From d9085483f3b0f907dc274a57f163e22b57018e66 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 20 Apr 2026 15:43:03 -0700 Subject: [PATCH 1/9] Revert "feat: Expose `createPermissionMiddleware` via messenger (#8502)" This reverts commit 9efdcfe054e368c61c0097551110e8f7f98686c1. --- packages/permission-controller/CHANGELOG.md | 4 - ...ermissionController-method-action-types.ts | 25 ---- .../src/PermissionController.ts | 99 ++++------------ packages/permission-controller/src/index.ts | 1 - .../src/permission-middleware.ts | 109 ++++++++++++++++++ 5 files changed, 131 insertions(+), 107 deletions(-) create mode 100644 packages/permission-controller/src/permission-middleware.ts diff --git a/packages/permission-controller/CHANGELOG.md b/packages/permission-controller/CHANGELOG.md index b3bc6c6d099..e1f31c07fff 100644 --- a/packages/permission-controller/CHANGELOG.md +++ b/packages/permission-controller/CHANGELOG.md @@ -7,10 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Added - -- Expose `createPermissionMiddleware` through the messenger ([#8502](https://github.com/MetaMask/core/pull/8502)) - ### Changed - Bump `@metamask/controller-utils` from `^11.19.0` to `^11.20.0` ([#8344](https://github.com/MetaMask/core/pull/8344)) diff --git a/packages/permission-controller/src/PermissionController-method-action-types.ts b/packages/permission-controller/src/PermissionController-method-action-types.ts index ed47c13ab00..6c575a78ac5 100644 --- a/packages/permission-controller/src/PermissionController-method-action-types.ts +++ b/packages/permission-controller/src/PermissionController-method-action-types.ts @@ -13,30 +13,6 @@ export type PermissionControllerClearStateAction = { handler: PermissionController['clearState']; }; -/** - * Creates a permission middleware function. Like any {@link JsonRpcEngine} - * middleware, each middleware will only receive requests from a particular - * subject / origin. - * - * The middlewares returned will pass through requests for - * unrestricted methods, and attempt to execute restricted methods. If a method - * is neither restricted nor unrestricted, a "method not found" error will be - * returned. - * If a method is restricted, the middleware will first attempt to retrieve the - * subject's permission for that method. If the permission is found, the method - * will be executed. Otherwise, an "unauthorized" error will be returned. - * - * The middleware **must** be added in the correct place in the middleware - * stack in order for it to work. See the README for an example. - * - * @param subject The permission subject. - * @returns A `json-rpc-engine` middleware. - */ -export type PermissionControllerCreatePermissionMiddlewareAction = { - type: `PermissionController:createPermissionMiddleware`; - handler: PermissionController['createPermissionMiddleware']; -}; - /** * Gets a list of all origins of subjects. * @@ -298,7 +274,6 @@ export type PermissionControllerGetEndowmentsAction = { */ export type PermissionControllerMethodActions = | PermissionControllerClearStateAction - | PermissionControllerCreatePermissionMiddlewareAction | PermissionControllerGetSubjectNamesAction | PermissionControllerGetPermissionsAction | PermissionControllerHasPermissionAction diff --git a/packages/permission-controller/src/PermissionController.ts b/packages/permission-controller/src/PermissionController.ts index 619fea0ee56..43e00ed8eb8 100644 --- a/packages/permission-controller/src/PermissionController.ts +++ b/packages/permission-controller/src/PermissionController.ts @@ -16,11 +16,6 @@ import { isPlainObject, isValidJson, } from '@metamask/controller-utils'; -import { - AsyncJsonRpcEngineNextCallback, - createAsyncMiddleware, - JsonRpcMiddleware, -} from '@metamask/json-rpc-engine'; import type { Messenger, ActionConstraint, @@ -28,12 +23,7 @@ import type { } from '@metamask/messenger'; import { JsonRpcError } from '@metamask/rpc-errors'; import { hasProperty } from '@metamask/utils'; -import type { - Json, - JsonRpcRequest, - Mutable, - PendingJsonRpcResponse, -} from '@metamask/utils'; +import type { Json, Mutable } from '@metamask/utils'; import deepFreeze from 'deep-freeze-strict'; import { castDraft, produce as immerProduce } from 'immer'; import type { Draft } from 'immer'; @@ -103,6 +93,7 @@ import { hasSpecificationType, PermissionType, } from './Permission'; +import { getPermissionMiddlewareFactory } from './permission-middleware'; import type { PermissionControllerMethodActions } from './PermissionController-method-action-types'; import type { SubjectMetadataControllerGetSubjectMetadataAction } from './SubjectMetadataController-method-action-types'; import { collectUniqueAndPairedCaveats, MethodNames } from './utils'; @@ -199,7 +190,6 @@ const MESSENGER_EXPOSED_METHODS = [ 'revokePermissions', 'updateCaveat', 'getCaveat', - 'createPermissionMiddleware', ] as const; /** @@ -674,6 +664,18 @@ export class PermissionController< return this.#unrestrictedMethods; } + /** + * Returns a `json-rpc-engine` middleware function factory, so that the rules + * described by the state of this controller can be applied to incoming + * JSON-RPC requests. + * + * The middleware **must** be added in the correct place in the middleware + * stack in order for it to work. See the README for an example. + */ + public createPermissionMiddleware: ReturnType< + typeof getPermissionMiddlewareFactory + >; + /** * Constructs the PermissionController. * @@ -742,6 +744,14 @@ export class PermissionController< this, MESSENGER_EXPOSED_METHODS, ); + + this.createPermissionMiddleware = getPermissionMiddlewareFactory({ + executeRestrictedMethod: this.#executeRestrictedMethod.bind(this), + getRestrictedMethod: this.getRestrictedMethod.bind(this), + isUnrestrictedMethod: this.unrestrictedMethods.has.bind( + this.unrestrictedMethods, + ), + }); } /** @@ -914,71 +924,6 @@ export class PermissionController< return specification; } - /** - * Creates a permission middleware function. Like any {@link JsonRpcEngine} - * middleware, each middleware will only receive requests from a particular - * subject / origin. - * - * The middlewares returned will pass through requests for - * unrestricted methods, and attempt to execute restricted methods. If a method - * is neither restricted nor unrestricted, a "method not found" error will be - * returned. - * If a method is restricted, the middleware will first attempt to retrieve the - * subject's permission for that method. If the permission is found, the method - * will be executed. Otherwise, an "unauthorized" error will be returned. - * - * The middleware **must** be added in the correct place in the middleware - * stack in order for it to work. See the README for an example. - * - * @param subject The permission subject. - * @returns A `json-rpc-engine` middleware. - */ - public createPermissionMiddleware( - subject: PermissionSubjectMetadata, - ): JsonRpcMiddleware { - const { origin } = subject; - if (typeof origin !== 'string' || !origin) { - throw new Error('The subject "origin" must be a non-empty string.'); - } - - const permissionsMiddleware = async ( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - next: AsyncJsonRpcEngineNextCallback, - ): Promise => { - const { method, params } = req; - - // Skip registered unrestricted methods. - if (this.#unrestrictedMethods.has(method)) { - return next(); - } - - // This will throw if no restricted method implementation is found. - const methodImplementation = this.getRestrictedMethod(method, origin); - - // This will throw if the permission does not exist. - const result = await this.#executeRestrictedMethod( - methodImplementation, - subject, - method, - params, - ); - - if (result === undefined) { - res.error = internalError( - `Request for method "${req.method}" returned undefined result.`, - { request: req }, - ); - return undefined; - } - - res.result = result; - return undefined; - }; - - return createAsyncMiddleware(permissionsMiddleware); - } - /** * Gets the implementation of the specified restricted method. * diff --git a/packages/permission-controller/src/index.ts b/packages/permission-controller/src/index.ts index bf76d3151b5..29130935379 100644 --- a/packages/permission-controller/src/index.ts +++ b/packages/permission-controller/src/index.ts @@ -18,7 +18,6 @@ export type { PermissionControllerRequestPermissionsAction, PermissionControllerRequestPermissionsIncrementalAction, PermissionControllerGetEndowmentsAction, - PermissionControllerCreatePermissionMiddlewareAction, } from './PermissionController-method-action-types'; export type { ExtractSpecifications, diff --git a/packages/permission-controller/src/permission-middleware.ts b/packages/permission-controller/src/permission-middleware.ts new file mode 100644 index 00000000000..618dc7a619f --- /dev/null +++ b/packages/permission-controller/src/permission-middleware.ts @@ -0,0 +1,109 @@ +import { createAsyncMiddleware } from '@metamask/json-rpc-engine'; +import type { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + JsonRpcEngine, + JsonRpcMiddleware, + AsyncJsonRpcEngineNextCallback, +} from '@metamask/json-rpc-engine'; +import type { + Json, + PendingJsonRpcResponse, + JsonRpcRequest, +} from '@metamask/utils'; + +import type { + GenericPermissionController, + PermissionSubjectMetadata, + RestrictedMethod, + RestrictedMethodParameters, +} from '.'; +import { internalError } from './errors'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import type { PermissionController } from './PermissionController'; + +type ExecuteRestrictedMethod = ( + methodImplementation: RestrictedMethod, + subject: PermissionSubjectMetadata, + method: string, + params?: RestrictedMethodParameters, +) => ReturnType>; + +type PermissionMiddlewareFactoryOptions = { + executeRestrictedMethod: ExecuteRestrictedMethod; + getRestrictedMethod: GenericPermissionController['getRestrictedMethod']; + isUnrestrictedMethod: (method: string) => boolean; +}; + +/** + * Creates a permission middleware function factory. Intended for internal use + * in the {@link PermissionController}. Like any {@link JsonRpcEngine} + * middleware, each middleware will only receive requests from a particular + * subject / origin. However, each middleware also requires access to some + * `PermissionController` internals, which is why this "factory factory" exists. + * + * The middlewares returned by the factory will pass through requests for + * unrestricted methods, and attempt to execute restricted methods. If a method + * is neither restricted nor unrestricted, a "method not found" error will be + * returned. + * If a method is restricted, the middleware will first attempt to retrieve the + * subject's permission for that method. If the permission is found, the method + * will be executed. Otherwise, an "unauthorized" error will be returned. + * + * @param options - Options bag. + * @param options.executeRestrictedMethod - `PermissionController.executeRestrictedMethod`. + * @param options.getRestrictedMethod - {@link PermissionController.getRestrictedMethod}. + * @param options.isUnrestrictedMethod - A function that checks whether a + * particular method is unrestricted. + * @returns A permission middleware factory function. + */ +export function getPermissionMiddlewareFactory({ + executeRestrictedMethod, + getRestrictedMethod, + isUnrestrictedMethod, +}: PermissionMiddlewareFactoryOptions) { + return function createPermissionMiddleware( + subject: PermissionSubjectMetadata, + ): JsonRpcMiddleware { + const { origin } = subject; + if (typeof origin !== 'string' || !origin) { + throw new Error('The subject "origin" must be a non-empty string.'); + } + + const permissionsMiddleware = async ( + req: JsonRpcRequest, + res: PendingJsonRpcResponse, + next: AsyncJsonRpcEngineNextCallback, + ): Promise => { + const { method, params } = req; + + // Skip registered unrestricted methods. + if (isUnrestrictedMethod(method)) { + return next(); + } + + // This will throw if no restricted method implementation is found. + const methodImplementation = getRestrictedMethod(method, origin); + + // This will throw if the permission does not exist. + const result = await executeRestrictedMethod( + methodImplementation, + subject, + method, + params, + ); + + if (result === undefined) { + res.error = internalError( + `Request for method "${req.method}" returned undefined result.`, + { request: req }, + ); + return undefined; + } + + res.result = result; + return undefined; + }; + + return createAsyncMiddleware(permissionsMiddleware); + }; +} From d7c09994bf77a9dbb048bf00f61a83dcf779039b Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:20:32 -0700 Subject: [PATCH 2/9] refactor(permission-controller): decouple permission middleware via messenger actions Replace the hook-based permission middleware factory with a standalone `createPermissionMiddleware` export that dispatches through messenger actions (`PermissionController:executeRestrictedMethod` and `PermissionController:hasUnrestrictedMethod`). Removes the `createPermissionMiddleware` property from `PermissionController`. Co-Authored-By: Claude Opus 4.7 --- .../permission-controller/ARCHITECTURE.md | 2 +- packages/permission-controller/CHANGELOG.md | 6 + ...ermissionController-method-action-types.ts | 45 ++++- .../src/PermissionController.test.ts | 163 ++++++++++++++---- .../src/PermissionController.ts | 38 ++-- packages/permission-controller/src/index.ts | 22 ++- .../src/permission-middleware.ts | 137 ++++++--------- 7 files changed, 259 insertions(+), 154 deletions(-) diff --git a/packages/permission-controller/ARCHITECTURE.md b/packages/permission-controller/ARCHITECTURE.md index 83119eed959..ca309f89044 100644 --- a/packages/permission-controller/ARCHITECTURE.md +++ b/packages/permission-controller/ARCHITECTURE.md @@ -335,7 +335,7 @@ const origin = getOrigin(subject); const engine = new JsonRpcEngine(); engine.push(/* your various middleware*/); -engine.push(permissionController.createPermissionMiddleware({ origin })); +engine.push(createPermissionMiddleware({ messenger, subject: { origin } })); // Your middleware stack is now permissioned engine.push(/* your other various middleware*/); ``` diff --git a/packages/permission-controller/CHANGELOG.md b/packages/permission-controller/CHANGELOG.md index e1f31c07fff..93263b2fea7 100644 --- a/packages/permission-controller/CHANGELOG.md +++ b/packages/permission-controller/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **BREAKING:** Decouple the permission middleware from `PermissionController` and expose it as a standalone `createPermissionMiddleware` export + - Removes the `createPermissionMiddleware` property from `PermissionController`. Consumers should instead import `createPermissionMiddleware` from `@metamask/permission-controller` and call it with a messenger and subject metadata. The messenger must have the `PermissionController:executeRestrictedMethod` and `PermissionController:hasUnrestrictedMethod` actions delegated to it. + - Adds a new `PermissionMiddlewareActions` type describing the messenger actions required by the middleware. + - Exposes additional `PermissionController` methods through its messenger: `PermissionController:executeRestrictedMethod` and `PermissionController:hasUnrestrictedMethod`. Corresponding action types (`PermissionControllerExecuteRestrictedMethodAction` and `PermissionControllerHasUnrestrictedMethodAction`) are exported as well. + - Adds a public `PermissionController.hasUnrestrictedMethod(method)` predicate backing the new action. + - When a restricted method returns `undefined`, the middleware now propagates the plain `Error` thrown by `PermissionController.executeRestrictedMethod` (with the message `Internal request for method "" as origin "" returned no result.`) rather than wrapping it into a custom `internalError` with a `request` data payload. The JSON-RPC engine serializes this as a standard internal error response. - Bump `@metamask/controller-utils` from `^11.19.0` to `^11.20.0` ([#8344](https://github.com/MetaMask/core/pull/8344)) - Bump `@metamask/messenger` from `^1.0.0` to `^1.1.1` ([#8364](https://github.com/MetaMask/core/pull/8364), [#8373](https://github.com/MetaMask/core/pull/8373)) - Bump `@metamask/base-controller` from `^9.0.1` to `^9.1.0` ([#8457](https://github.com/MetaMask/core/pull/8457)) diff --git a/packages/permission-controller/src/PermissionController-method-action-types.ts b/packages/permission-controller/src/PermissionController-method-action-types.ts index 6c575a78ac5..50519fdda30 100644 --- a/packages/permission-controller/src/PermissionController-method-action-types.ts +++ b/packages/permission-controller/src/PermissionController-method-action-types.ts @@ -5,6 +5,17 @@ import type { PermissionController } from './PermissionController'; +/** + * Checks whether the given method is an unrestricted method. + * + * @param method - The name of the method to check. + * @returns Whether the method is unrestricted. + */ +export type PermissionControllerHasUnrestrictedMethodAction = { + type: `PermissionController:hasUnrestrictedMethod`; + handler: PermissionController['hasUnrestrictedMethod']; +}; + /** * Clears the state of the controller. */ @@ -269,10 +280,41 @@ export type PermissionControllerGetEndowmentsAction = { handler: PermissionController['getEndowments']; }; +/** + * Executes a restricted method as the subject with the given origin. + * The specified params, if any, will be passed to the method implementation. + * + * ATTN: Great caution should be exercised in the use of this method. + * Methods that cause side effects or affect application state should + * be avoided. + * + * This method will first attempt to retrieve the requested restricted method + * implementation, throwing if it does not exist. The method will then be + * invoked as though the subject with the specified origin had invoked it with + * the specified parameters. This means that any existing caveats will be + * applied to the restricted method, and this method will throw if the + * restricted method or its caveat decorators throw. + * + * In addition, this method will throw if the subject does not have a + * permission for the specified restricted method. + * + * @param origin - The origin of the subject to execute the method on behalf + * of. + * @param targetName - The name of the method to execute. This must be a valid + * permission target name. + * @param params - The parameters to pass to the method implementation. + * @returns The result of the executed method. + */ +export type PermissionControllerExecuteRestrictedMethodAction = { + type: `PermissionController:executeRestrictedMethod`; + handler: PermissionController['executeRestrictedMethod']; +}; + /** * Union of all PermissionController action types. */ export type PermissionControllerMethodActions = + | PermissionControllerHasUnrestrictedMethodAction | PermissionControllerClearStateAction | PermissionControllerGetSubjectNamesAction | PermissionControllerGetPermissionsAction @@ -287,4 +329,5 @@ export type PermissionControllerMethodActions = | PermissionControllerGrantPermissionsIncrementalAction | PermissionControllerRequestPermissionsAction | PermissionControllerRequestPermissionsIncrementalAction - | PermissionControllerGetEndowmentsAction; + | PermissionControllerGetEndowmentsAction + | PermissionControllerExecuteRestrictedMethodAction; diff --git a/packages/permission-controller/src/PermissionController.test.ts b/packages/permission-controller/src/PermissionController.test.ts index b7f7460f632..3a9e82959d4 100644 --- a/packages/permission-controller/src/PermissionController.test.ts +++ b/packages/permission-controller/src/PermissionController.test.ts @@ -26,6 +26,7 @@ import { PermissionControllerMessenger, PermissionControllerOptions, PermissionControllerState, + PermissionMiddlewareActions, PermissionOptions, PermissionsRequest, RestrictedMethodOptions, @@ -35,6 +36,7 @@ import { import { CaveatMutatorOperation, constructPermission, + createPermissionMiddleware, MethodNames, PermissionController, PermissionType, @@ -634,6 +636,32 @@ function getPermissionControllerMessenger( return messenger; } +/** + * Gets a messenger scoped to the actions required by the permission + * middleware, delegated from the given root messenger. + * + * @param rootMessenger - The root messenger to delegate from. + * @returns A messenger suitable for passing to `createPermissionMiddleware`. + */ +function getPermissionMiddlewareMessenger( + rootMessenger: RootMessenger, +): Messenger<'PermissionMiddleware', PermissionMiddlewareActions> { + const messenger = new Messenger< + 'PermissionMiddleware', + PermissionMiddlewareActions, + never, + RootMessenger + >({ namespace: 'PermissionMiddleware', parent: rootMessenger }); + rootMessenger.delegate({ + actions: [ + 'PermissionController:executeRestrictedMethod', + 'PermissionController:hasUnrestrictedMethod', + ], + messenger, + }); + return messenger; +} + /** * Gets the default unrestricted methods array. * Used as a default in {@link getPermissionControllerOptions}. @@ -6223,8 +6251,42 @@ describe('PermissionController', () => { }); describe('permission middleware', () => { + /** + * Builds a permission controller and a messenger suitable for passing to + * `createPermissionMiddleware` that are wired to the same root messenger. + * + * @param opts - Permission controller options overrides. + * @returns The controller and the middleware messenger. + */ + const setup = ( + opts?: Record, + ): { + controller: PermissionController< + DefaultPermissionSpecifications, + DefaultCaveatSpecifications + >; + middlewareMessenger: Messenger< + 'PermissionMiddleware', + PermissionMiddlewareActions + >; + } => { + const rootMessenger = getRootMessenger(); + const controller = new PermissionController< + DefaultPermissionSpecifications, + DefaultCaveatSpecifications + >( + getPermissionControllerOptions({ + messenger: getPermissionControllerMessenger(rootMessenger), + ...opts, + }), + ); + const middlewareMessenger = + getPermissionMiddlewareMessenger(rootMessenger); + return { controller, middlewareMessenger }; + }; + it('executes a restricted method', async () => { - const controller = getDefaultPermissionController(); + const { controller, middlewareMessenger } = setup(); const origin = 'metamask.io'; controller.grantPermissions({ @@ -6235,7 +6297,12 @@ describe('PermissionController', () => { }); const engine = new JsonRpcEngine(); - engine.push(controller.createPermissionMiddleware({ origin })); + engine.push( + createPermissionMiddleware({ + messenger: middlewareMessenger, + subject: { origin }, + }), + ); const response = await engine.handle({ jsonrpc: '2.0', @@ -6248,7 +6315,7 @@ describe('PermissionController', () => { }); it('executes a restricted method with a caveat', async () => { - const controller = getDefaultPermissionController(); + const { controller, middlewareMessenger } = setup(); const origin = 'metamask.io'; controller.grantPermissions({ @@ -6261,7 +6328,12 @@ describe('PermissionController', () => { }); const engine = new JsonRpcEngine(); - engine.push(controller.createPermissionMiddleware({ origin })); + engine.push( + createPermissionMiddleware({ + messenger: middlewareMessenger, + subject: { origin }, + }), + ); const response = await engine.handle({ jsonrpc: '2.0', @@ -6274,7 +6346,7 @@ describe('PermissionController', () => { }); it('executes a restricted method with multiple caveats', async () => { - const controller = getDefaultPermissionController(); + const { controller, middlewareMessenger } = setup(); const origin = 'metamask.io'; controller.grantPermissions({ @@ -6290,7 +6362,12 @@ describe('PermissionController', () => { }); const engine = new JsonRpcEngine(); - engine.push(controller.createPermissionMiddleware({ origin })); + engine.push( + createPermissionMiddleware({ + messenger: middlewareMessenger, + subject: { origin }, + }), + ); const response = await engine.handle({ jsonrpc: '2.0', @@ -6303,11 +6380,16 @@ describe('PermissionController', () => { }); it('passes through unrestricted methods', async () => { - const controller = getDefaultPermissionController(); + const { middlewareMessenger } = setup(); const origin = 'metamask.io'; const engine = new JsonRpcEngine(); - engine.push(controller.createPermissionMiddleware({ origin })); + engine.push( + createPermissionMiddleware({ + messenger: middlewareMessenger, + subject: { origin }, + }), + ); engine.push((_req, res, _next, end) => { res.result = 'success'; end(); @@ -6324,13 +6406,16 @@ describe('PermissionController', () => { }); it('throws an error if the subject has an invalid "origin" property', async () => { - const controller = getDefaultPermissionController(); + const { middlewareMessenger } = setup(); ['', null, undefined, 2].forEach((invalidOrigin) => { expect(() => - controller.createPermissionMiddleware({ - // @ts-expect-error Intentional destructive testing - origin: invalidOrigin, + createPermissionMiddleware({ + messenger: middlewareMessenger, + subject: { + // @ts-expect-error Intentional destructive testing + origin: invalidOrigin, + }, }), ).toThrow( new Error('The subject "origin" must be a non-empty string.'), @@ -6339,11 +6424,16 @@ describe('PermissionController', () => { }); it('returns an error if the subject does not have the requisite permission', async () => { - const controller = getDefaultPermissionController(); + const { middlewareMessenger } = setup(); const origin = 'metamask.io'; const engine = new JsonRpcEngine(); - engine.push(controller.createPermissionMiddleware({ origin })); + engine.push( + createPermissionMiddleware({ + messenger: middlewareMessenger, + subject: { origin }, + }), + ); const request: JsonRpcRequest<[]> = { jsonrpc: '2.0', @@ -6369,11 +6459,16 @@ describe('PermissionController', () => { }); it('returns an error if the method does not exist', async () => { - const controller = getDefaultPermissionController(); + const { middlewareMessenger } = setup(); const origin = 'metamask.io'; const engine = new JsonRpcEngine(); - engine.push(controller.createPermissionMiddleware({ origin })); + engine.push( + createPermissionMiddleware({ + messenger: middlewareMessenger, + subject: { origin }, + }), + ); const request: JsonRpcRequest<[]> = { jsonrpc: '2.0', @@ -6401,14 +6496,9 @@ describe('PermissionController', () => { permissionSpecifications.wallet_doubleNumber.methodImplementation = (): undefined => undefined; - const controller = new PermissionController< - DefaultPermissionSpecifications, - DefaultCaveatSpecifications - >( - getPermissionControllerOptions({ - permissionSpecifications, - }), - ); + const { controller, middlewareMessenger } = setup({ + permissionSpecifications, + }); const origin = 'metamask.io'; controller.grantPermissions({ @@ -6419,7 +6509,12 @@ describe('PermissionController', () => { }); const engine = new JsonRpcEngine(); - engine.push(controller.createPermissionMiddleware({ origin })); + engine.push( + createPermissionMiddleware({ + messenger: middlewareMessenger, + subject: { origin }, + }), + ); const request: JsonRpcRequest<[]> = { jsonrpc: '2.0', @@ -6427,21 +6522,17 @@ describe('PermissionController', () => { method: PermissionNames.wallet_doubleNumber, }; - const expectedError = errors.internalError( - `Request for method "${PermissionNames.wallet_doubleNumber}" returned undefined result.`, - { request: { ...request } }, - ); - const response = await engine.handle(request); assertIsJsonRpcFailure(response); const { error } = response; - expect(error.message).toStrictEqual(expectedError.message); - // @ts-expect-error We do expect this property to exist. - expect(error.data?.cause).toBeNull(); - // @ts-expect-error Intentional destructive testing - delete error.data.cause; - expect(error).toMatchObject(expect.objectContaining(expectedError)); + // The public `executeRestrictedMethod` throws a plain `Error` when the + // restricted method returns `undefined`; the JSON-RPC engine wraps it + // as an internal error response. + expect(error.message).toBe( + `Internal request for method "${PermissionNames.wallet_doubleNumber}" as origin "${origin}" returned no result.`, + ); + expect(error.code).toBe(-32603); }); }); diff --git a/packages/permission-controller/src/PermissionController.ts b/packages/permission-controller/src/PermissionController.ts index 43e00ed8eb8..22a425dde1b 100644 --- a/packages/permission-controller/src/PermissionController.ts +++ b/packages/permission-controller/src/PermissionController.ts @@ -93,7 +93,6 @@ import { hasSpecificationType, PermissionType, } from './Permission'; -import { getPermissionMiddlewareFactory } from './permission-middleware'; import type { PermissionControllerMethodActions } from './PermissionController-method-action-types'; import type { SubjectMetadataControllerGetSubjectMetadataAction } from './SubjectMetadataController-method-action-types'; import { collectUniqueAndPairedCaveats, MethodNames } from './utils'; @@ -176,11 +175,13 @@ const controllerName = 'PermissionController'; const MESSENGER_EXPOSED_METHODS = [ 'clearState', + 'executeRestrictedMethod', 'getEndowments', 'getSubjectNames', 'getPermissions', 'hasPermission', 'hasPermissions', + 'hasUnrestrictedMethod', 'grantPermissions', 'grantPermissionsIncremental', 'requestPermissions', @@ -665,16 +666,14 @@ export class PermissionController< } /** - * Returns a `json-rpc-engine` middleware function factory, so that the rules - * described by the state of this controller can be applied to incoming - * JSON-RPC requests. + * Checks whether the given method is an unrestricted method. * - * The middleware **must** be added in the correct place in the middleware - * stack in order for it to work. See the README for an example. + * @param method - The name of the method to check. + * @returns Whether the method is unrestricted. */ - public createPermissionMiddleware: ReturnType< - typeof getPermissionMiddlewareFactory - >; + hasUnrestrictedMethod(method: string): boolean { + return this.#unrestrictedMethods.has(method); + } /** * Constructs the PermissionController. @@ -744,14 +743,6 @@ export class PermissionController< this, MESSENGER_EXPOSED_METHODS, ); - - this.createPermissionMiddleware = getPermissionMiddlewareFactory({ - executeRestrictedMethod: this.#executeRestrictedMethod.bind(this), - getRestrictedMethod: this.getRestrictedMethod.bind(this), - isUnrestrictedMethod: this.unrestrictedMethods.has.bind( - this.unrestrictedMethods, - ), - }); } /** @@ -929,8 +920,7 @@ export class PermissionController< * * A JSON-RPC error is thrown if the method does not exist. * - * @see {@link PermissionController.executeRestrictedMethod} and - * {@link PermissionController.createPermissionMiddleware} for internal usage. + * @see {@link PermissionController.executeRestrictedMethod} for internal usage. * @param method - The name of the restricted method. * @param origin - The origin associated with the request for the restricted * method, if any. @@ -2794,17 +2784,15 @@ export class PermissionController< } /** - * An internal method used in the controller's `json-rpc-engine` middleware - * and {@link PermissionController.executeRestrictedMethod}. Calls the - * specified restricted method implementation after decorating it with the - * caveats of its permission. Throws if the subject does not have the + * An internal method used in {@link PermissionController.executeRestrictedMethod}. + * Calls the specified restricted method implementation after decorating it + * with the caveats of its permission. Throws if the subject does not have the * requisite permission. * * ATTN: Parameter validation is the responsibility of the caller, or * the restricted method implementation in the case of `params`. * - * @see {@link PermissionController.executeRestrictedMethod} and - * {@link PermissionController.createPermissionMiddleware} for usage. + * @see {@link PermissionController.executeRestrictedMethod} for usage. * @param methodImplementation - The implementation of the method to call. * @param subject - Metadata about the subject that made the request. * @param method - The method name diff --git a/packages/permission-controller/src/index.ts b/packages/permission-controller/src/index.ts index 29130935379..a3a82a42a37 100644 --- a/packages/permission-controller/src/index.ts +++ b/packages/permission-controller/src/index.ts @@ -4,21 +4,27 @@ export * from './Permission'; export * from './PermissionController'; export type { PermissionControllerClearStateAction, - PermissionControllerGetSubjectNamesAction, + PermissionControllerExecuteRestrictedMethodAction, + PermissionControllerGetCaveatAction, + PermissionControllerGetEndowmentsAction, PermissionControllerGetPermissionsAction, + PermissionControllerGetSubjectNamesAction, + PermissionControllerGrantPermissionsAction, + PermissionControllerGrantPermissionsIncrementalAction, PermissionControllerHasPermissionAction, PermissionControllerHasPermissionsAction, + PermissionControllerHasUnrestrictedMethodAction, + PermissionControllerRequestPermissionsAction, + PermissionControllerRequestPermissionsIncrementalAction, PermissionControllerRevokeAllPermissionsAction, - PermissionControllerRevokePermissionsAction, PermissionControllerRevokePermissionForAllSubjectsAction, - PermissionControllerGetCaveatAction, + PermissionControllerRevokePermissionsAction, PermissionControllerUpdateCaveatAction, - PermissionControllerGrantPermissionsAction, - PermissionControllerGrantPermissionsIncrementalAction, - PermissionControllerRequestPermissionsAction, - PermissionControllerRequestPermissionsIncrementalAction, - PermissionControllerGetEndowmentsAction, } from './PermissionController-method-action-types'; +export { + createPermissionMiddleware, + type PermissionMiddlewareActions, +} from './permission-middleware'; export type { ExtractSpecifications, HandlerMiddlewareFunction, diff --git a/packages/permission-controller/src/permission-middleware.ts b/packages/permission-controller/src/permission-middleware.ts index 618dc7a619f..162edcbfb2a 100644 --- a/packages/permission-controller/src/permission-middleware.ts +++ b/packages/permission-controller/src/permission-middleware.ts @@ -1,109 +1,80 @@ import { createAsyncMiddleware } from '@metamask/json-rpc-engine'; import type { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - JsonRpcEngine, JsonRpcMiddleware, AsyncJsonRpcEngineNextCallback, } from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; import type { Json, PendingJsonRpcResponse, JsonRpcRequest, } from '@metamask/utils'; +import type { RestrictedMethodParameters } from './Permission'; +import type { PermissionSubjectMetadata } from './PermissionController'; import type { - GenericPermissionController, - PermissionSubjectMetadata, - RestrictedMethod, - RestrictedMethodParameters, -} from '.'; -import { internalError } from './errors'; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import type { PermissionController } from './PermissionController'; + PermissionControllerExecuteRestrictedMethodAction, + PermissionControllerHasUnrestrictedMethodAction, +} from './PermissionController-method-action-types'; -type ExecuteRestrictedMethod = ( - methodImplementation: RestrictedMethod, - subject: PermissionSubjectMetadata, - method: string, - params?: RestrictedMethodParameters, -) => ReturnType>; +/** + * The set of messenger actions required by the permission middleware. + */ +export type PermissionMiddlewareActions = + | PermissionControllerExecuteRestrictedMethodAction + | PermissionControllerHasUnrestrictedMethodAction; -type PermissionMiddlewareFactoryOptions = { - executeRestrictedMethod: ExecuteRestrictedMethod; - getRestrictedMethod: GenericPermissionController['getRestrictedMethod']; - isUnrestrictedMethod: (method: string) => boolean; +type CreatePermissionMiddlewareOptions = { + messenger: Messenger; + subject: PermissionSubjectMetadata; }; /** - * Creates a permission middleware function factory. Intended for internal use - * in the {@link PermissionController}. Like any {@link JsonRpcEngine} - * middleware, each middleware will only receive requests from a particular - * subject / origin. However, each middleware also requires access to some - * `PermissionController` internals, which is why this "factory factory" exists. + * Creates a JSON-RPC middleware that enforces permissions for a single subject. * - * The middlewares returned by the factory will pass through requests for - * unrestricted methods, and attempt to execute restricted methods. If a method - * is neither restricted nor unrestricted, a "method not found" error will be - * returned. - * If a method is restricted, the middleware will first attempt to retrieve the - * subject's permission for that method. If the permission is found, the method - * will be executed. Otherwise, an "unauthorized" error will be returned. + * The middleware passes through unrestricted methods, and otherwise dispatches + * restricted methods to the `PermissionController` via messenger actions. If + * the subject lacks the required permission, or if the method does not exist, + * the corresponding error is propagated to the JSON-RPC response. * * @param options - Options bag. - * @param options.executeRestrictedMethod - `PermissionController.executeRestrictedMethod`. - * @param options.getRestrictedMethod - {@link PermissionController.getRestrictedMethod}. - * @param options.isUnrestrictedMethod - A function that checks whether a - * particular method is unrestricted. - * @returns A permission middleware factory function. + * @param options.messenger - A messenger with the + * `PermissionController:executeRestrictedMethod` and + * `PermissionController:hasUnrestrictedMethod` actions. + * @param options.subject - The subject for which to create the middleware. + * @returns A `json-rpc-engine` middleware. */ -export function getPermissionMiddlewareFactory({ - executeRestrictedMethod, - getRestrictedMethod, - isUnrestrictedMethod, -}: PermissionMiddlewareFactoryOptions) { - return function createPermissionMiddleware( - subject: PermissionSubjectMetadata, - ): JsonRpcMiddleware { - const { origin } = subject; - if (typeof origin !== 'string' || !origin) { - throw new Error('The subject "origin" must be a non-empty string.'); - } - - const permissionsMiddleware = async ( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - next: AsyncJsonRpcEngineNextCallback, - ): Promise => { - const { method, params } = req; - - // Skip registered unrestricted methods. - if (isUnrestrictedMethod(method)) { - return next(); - } - - // This will throw if no restricted method implementation is found. - const methodImplementation = getRestrictedMethod(method, origin); - - // This will throw if the permission does not exist. - const result = await executeRestrictedMethod( - methodImplementation, - subject, - method, - params, - ); +export function createPermissionMiddleware({ + messenger, + subject, +}: CreatePermissionMiddlewareOptions): JsonRpcMiddleware< + RestrictedMethodParameters, + Json +> { + const { origin } = subject; + if (typeof origin !== 'string' || !origin) { + throw new Error('The subject "origin" must be a non-empty string.'); + } - if (result === undefined) { - res.error = internalError( - `Request for method "${req.method}" returned undefined result.`, - { request: req }, - ); - return undefined; - } + const permissionsMiddleware = async ( + req: JsonRpcRequest, + res: PendingJsonRpcResponse, + next: AsyncJsonRpcEngineNextCallback, + ): Promise => { + const { method, params } = req; - res.result = result; - return undefined; - }; + if (messenger.call('PermissionController:hasUnrestrictedMethod', method)) { + return next(); + } - return createAsyncMiddleware(permissionsMiddleware); + res.result = await messenger.call( + 'PermissionController:executeRestrictedMethod', + origin, + method, + params, + ); + return undefined; }; + + return createAsyncMiddleware(permissionsMiddleware); } From 8bed7cc58c3e99538a081b5aeb1831e888801dbf Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:38:19 -0700 Subject: [PATCH 3/9] refactor(permission-controller): make getRestrictedMethod private Now that the permission middleware invokes restricted methods through the messenger, `getRestrictedMethod` has no remaining external consumers and is made `#`-private. Its caller signature is tightened so `requestingOrigin` is required, eliminating a dead optional-origin branch in `#getTypedPermissionSpecification`. Co-Authored-By: Claude Opus 4.7 --- packages/permission-controller/CHANGELOG.md | 1 + .../src/PermissionController.test.ts | 30 ------------------- .../src/PermissionController.ts | 19 +++++------- 3 files changed, 9 insertions(+), 41 deletions(-) diff --git a/packages/permission-controller/CHANGELOG.md b/packages/permission-controller/CHANGELOG.md index 93263b2fea7..2dd57543369 100644 --- a/packages/permission-controller/CHANGELOG.md +++ b/packages/permission-controller/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Adds a new `PermissionMiddlewareActions` type describing the messenger actions required by the middleware. - Exposes additional `PermissionController` methods through its messenger: `PermissionController:executeRestrictedMethod` and `PermissionController:hasUnrestrictedMethod`. Corresponding action types (`PermissionControllerExecuteRestrictedMethodAction` and `PermissionControllerHasUnrestrictedMethodAction`) are exported as well. - Adds a public `PermissionController.hasUnrestrictedMethod(method)` predicate backing the new action. + - Removes the public `getRestrictedMethod` method from `PermissionController`. Restricted methods should be invoked via `executeRestrictedMethod` instead. - When a restricted method returns `undefined`, the middleware now propagates the plain `Error` thrown by `PermissionController.executeRestrictedMethod` (with the message `Internal request for method "" as origin "" returned no result.`) rather than wrapping it into a custom `internalError` with a `request` data payload. The JSON-RPC engine serializes this as a standard internal error response. - Bump `@metamask/controller-utils` from `^11.19.0` to `^11.20.0` ([#8344](https://github.com/MetaMask/core/pull/8344)) - Bump `@metamask/messenger` from `^1.0.0` to `^1.1.1` ([#8364](https://github.com/MetaMask/core/pull/8364), [#8373](https://github.com/MetaMask/core/pull/8373)) diff --git a/packages/permission-controller/src/PermissionController.test.ts b/packages/permission-controller/src/PermissionController.test.ts index 3a9e82959d4..bf0280c51a2 100644 --- a/packages/permission-controller/src/PermissionController.test.ts +++ b/packages/permission-controller/src/PermissionController.test.ts @@ -957,36 +957,6 @@ describe('PermissionController', () => { }); }); - describe('getRestrictedMethod', () => { - it('gets the implementation of a restricted method', async () => { - const controller = getDefaultPermissionController(); - const method = controller.getRestrictedMethod( - PermissionNames.wallet_getSecretArray, - ); - - expect( - await method({ - method: 'wallet_getSecretArray', - context: { origin: 'github.com' }, - }), - ).toStrictEqual(['a', 'b', 'c']); - }); - - it('throws an error if the requested permission target is not a restricted method', () => { - const controller = getDefaultPermissionController(); - expect(() => - controller.getRestrictedMethod(PermissionNames.endowmentAnySubject), - ).toThrow(errors.methodNotFound(PermissionNames.endowmentAnySubject)); - }); - - it('throws an error if the method does not exist', () => { - const controller = getDefaultPermissionController(); - expect(() => controller.getRestrictedMethod('foo')).toThrow( - errors.methodNotFound('foo'), - ); - }); - }); - describe('getSubjectNames', () => { it('gets all subject names', () => { const controller = getDefaultPermissionController(); diff --git a/packages/permission-controller/src/PermissionController.ts b/packages/permission-controller/src/PermissionController.ts index 22a425dde1b..5c01eae5d57 100644 --- a/packages/permission-controller/src/PermissionController.ts +++ b/packages/permission-controller/src/PermissionController.ts @@ -882,22 +882,19 @@ export class PermissionController< * @template Type - The type of the permission specification to get. * @param permissionType - The type of the permission specification to get. * @param targetName - The name of the permission whose specification to get. - * @param requestingOrigin - The origin of the requesting subject, if any. - * Will be added to any thrown errors. + * @param requestingOrigin - The origin of the requesting subject. Will be + * added to any thrown errors. * @returns The specification object corresponding to the given type and * target name. */ #getTypedPermissionSpecification( permissionType: Type, targetName: string, - requestingOrigin?: string, + requestingOrigin: string, ): ControllerPermissionSpecification & { permissionType: Type } { const failureError = permissionType === PermissionType.RestrictedMethod - ? methodNotFound( - targetName, - requestingOrigin ? { origin: requestingOrigin } : undefined, - ) + ? methodNotFound(targetName, { origin: requestingOrigin }) : new EndowmentPermissionDoesNotExistError( targetName, requestingOrigin, @@ -923,12 +920,12 @@ export class PermissionController< * @see {@link PermissionController.executeRestrictedMethod} for internal usage. * @param method - The name of the restricted method. * @param origin - The origin associated with the request for the restricted - * method, if any. + * method. * @returns The restricted method implementation. */ - getRestrictedMethod( + #getRestrictedMethod( method: string, - origin?: string, + origin: string, ): RestrictedMethod { return this.#getTypedPermissionSpecification( PermissionType.RestrictedMethod, @@ -2765,7 +2762,7 @@ export class PermissionController< params?: RestrictedMethodParameters, ): Promise { // Throws if the method does not exist - const methodImplementation = this.getRestrictedMethod(targetName, origin); + const methodImplementation = this.#getRestrictedMethod(targetName, origin); const result = await this.#executeRestrictedMethod( methodImplementation, From 136e41f102ff99ddedcbad931eb25bc22d7d870a Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:50:30 -0700 Subject: [PATCH 4/9] feat(permission-controller): add createPermissionMiddlewareV2 Add a `JsonRpcEngineV2` variant of the standalone permission middleware factory that uses the same messenger actions as the v1 factory. The existing `createPermissionMiddleware` is marked `@deprecated` in favor of the v2 variant. Co-Authored-By: Claude Opus 4.7 --- packages/permission-controller/CHANGELOG.md | 10 +- .../src/PermissionController.test.ts | 98 +++++++++++++++++++ packages/permission-controller/src/index.ts | 1 + .../src/permission-middleware.ts | 63 ++++++++++-- 4 files changed, 165 insertions(+), 7 deletions(-) diff --git a/packages/permission-controller/CHANGELOG.md b/packages/permission-controller/CHANGELOG.md index 2dd57543369..ce2962ae8b3 100644 --- a/packages/permission-controller/CHANGELOG.md +++ b/packages/permission-controller/CHANGELOG.md @@ -7,9 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `createPermissionMiddlewareV2`, a `JsonRpcEngineV2` variant of the standalone permission middleware factory ([#8532](https://github.com/MetaMask/core/pull/8532)). + ### Changed -- **BREAKING:** Decouple the permission middleware from `PermissionController` and expose it as a standalone `createPermissionMiddleware` export +- **BREAKING:** Decouple the permission middleware from `PermissionController` and expose it as a standalone `createPermissionMiddleware` export ([#8532](https://github.com/MetaMask/core/pull/8532)) - Removes the `createPermissionMiddleware` property from `PermissionController`. Consumers should instead import `createPermissionMiddleware` from `@metamask/permission-controller` and call it with a messenger and subject metadata. The messenger must have the `PermissionController:executeRestrictedMethod` and `PermissionController:hasUnrestrictedMethod` actions delegated to it. - Adds a new `PermissionMiddlewareActions` type describing the messenger actions required by the middleware. - Exposes additional `PermissionController` methods through its messenger: `PermissionController:executeRestrictedMethod` and `PermissionController:hasUnrestrictedMethod`. Corresponding action types (`PermissionControllerExecuteRestrictedMethodAction` and `PermissionControllerHasUnrestrictedMethodAction`) are exported as well. @@ -20,6 +24,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/messenger` from `^1.0.0` to `^1.1.1` ([#8364](https://github.com/MetaMask/core/pull/8364), [#8373](https://github.com/MetaMask/core/pull/8373)) - Bump `@metamask/base-controller` from `^9.0.1` to `^9.1.0` ([#8457](https://github.com/MetaMask/core/pull/8457)) +### Deprecated + +- Deprecate `createPermissionMiddleware` in favor of `createPermissionMiddlewareV2`, which targets `JsonRpcEngineV2` ([#8532](https://github.com/MetaMask/core/pull/8532)). + ## [12.3.0] ### Added diff --git a/packages/permission-controller/src/PermissionController.test.ts b/packages/permission-controller/src/PermissionController.test.ts index bf0280c51a2..bb836ddd6c3 100644 --- a/packages/permission-controller/src/PermissionController.test.ts +++ b/packages/permission-controller/src/PermissionController.test.ts @@ -1,6 +1,7 @@ import { deriveStateFromMetadata } from '@metamask/base-controller'; import { isPlainObject } from '@metamask/controller-utils'; import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2'; import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger'; import type { MessengerActions, @@ -37,6 +38,7 @@ import { CaveatMutatorOperation, constructPermission, createPermissionMiddleware, + createPermissionMiddlewareV2, MethodNames, PermissionController, PermissionType, @@ -6504,6 +6506,102 @@ describe('PermissionController', () => { ); expect(error.code).toBe(-32603); }); + + describe('v2', () => { + it('executes a restricted method', async () => { + const { controller, middlewareMessenger } = setup(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: {}, + }, + }); + + const engine = JsonRpcEngineV2.create({ + middleware: [ + createPermissionMiddlewareV2({ + messenger: middlewareMessenger, + subject: { origin }, + }), + ], + }); + + const result = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: PermissionNames.wallet_getSecretArray, + }); + + expect(result).toStrictEqual(['a', 'b', 'c']); + }); + + it('passes through unrestricted methods', async () => { + const { middlewareMessenger } = setup(); + const origin = 'metamask.io'; + + const engine = JsonRpcEngineV2.create({ + middleware: [ + createPermissionMiddlewareV2({ + messenger: middlewareMessenger, + subject: { origin }, + }), + () => 'success', + ], + }); + + const result = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'wallet_unrestrictedMethod', + }); + + expect(result).toBe('success'); + }); + + it('throws an error if the subject has an invalid "origin" property', () => { + const { middlewareMessenger } = setup(); + + ['', null, undefined, 2].forEach((invalidOrigin) => { + expect(() => + createPermissionMiddlewareV2({ + messenger: middlewareMessenger, + subject: { + // @ts-expect-error Intentional destructive testing + origin: invalidOrigin, + }, + }), + ).toThrow( + new Error('The subject "origin" must be a non-empty string.'), + ); + }); + }); + + it('throws if the subject does not have the requisite permission', async () => { + const { middlewareMessenger } = setup(); + const origin = 'metamask.io'; + + const engine = JsonRpcEngineV2.create({ + middleware: [ + createPermissionMiddlewareV2({ + messenger: middlewareMessenger, + subject: { origin }, + }), + ], + }); + + await expect( + engine.handle({ + jsonrpc: '2.0', + id: 1, + method: PermissionNames.wallet_getSecretArray, + }), + ).rejects.toThrow( + 'Unauthorized to perform action. Try requesting the required permission(s) first.', + ); + }); + }); }); describe('metadata', () => { diff --git a/packages/permission-controller/src/index.ts b/packages/permission-controller/src/index.ts index a3a82a42a37..758510cc35a 100644 --- a/packages/permission-controller/src/index.ts +++ b/packages/permission-controller/src/index.ts @@ -23,6 +23,7 @@ export type { } from './PermissionController-method-action-types'; export { createPermissionMiddleware, + createPermissionMiddlewareV2, type PermissionMiddlewareActions, } from './permission-middleware'; export type { diff --git a/packages/permission-controller/src/permission-middleware.ts b/packages/permission-controller/src/permission-middleware.ts index 162edcbfb2a..9864dad8009 100644 --- a/packages/permission-controller/src/permission-middleware.ts +++ b/packages/permission-controller/src/permission-middleware.ts @@ -1,13 +1,14 @@ import { createAsyncMiddleware } from '@metamask/json-rpc-engine'; import type { - JsonRpcMiddleware, AsyncJsonRpcEngineNextCallback, + JsonRpcMiddleware, } from '@metamask/json-rpc-engine'; +import type { JsonRpcMiddleware as JsonRpcMiddlewareV2 } from '@metamask/json-rpc-engine/v2'; import type { Messenger } from '@metamask/messenger'; import type { Json, - PendingJsonRpcResponse, JsonRpcRequest, + PendingJsonRpcResponse, } from '@metamask/utils'; import type { RestrictedMethodParameters } from './Permission'; @@ -37,6 +38,7 @@ type CreatePermissionMiddlewareOptions = { * the subject lacks the required permission, or if the method does not exist, * the corresponding error is propagated to the JSON-RPC response. * + * @deprecated Use {@link createPermissionMiddlewareV2} with `JsonRpcEngineV2`. * @param options - Options bag. * @param options.messenger - A messenger with the * `PermissionController:executeRestrictedMethod` and @@ -51,10 +53,7 @@ export function createPermissionMiddleware({ RestrictedMethodParameters, Json > { - const { origin } = subject; - if (typeof origin !== 'string' || !origin) { - throw new Error('The subject "origin" must be a non-empty string.'); - } + const origin = validateOrigin(subject); const permissionsMiddleware = async ( req: JsonRpcRequest, @@ -78,3 +77,55 @@ export function createPermissionMiddleware({ return createAsyncMiddleware(permissionsMiddleware); } + +/** + * Creates a `JsonRpcEngineV2` middleware that enforces permissions for a + * single subject. + * + * The middleware passes through unrestricted methods, and otherwise dispatches + * restricted methods to the `PermissionController` via messenger actions. If + * the subject lacks the required permission, or if the method does not exist, + * the corresponding error is thrown. + * + * @param options - Options bag. + * @param options.messenger - A messenger with the + * `PermissionController:executeRestrictedMethod` and + * `PermissionController:hasUnrestrictedMethod` actions. + * @param options.subject - The subject for which to create the middleware. + * @returns A `JsonRpcEngineV2` middleware. + */ +export function createPermissionMiddlewareV2({ + messenger, + subject, +}: CreatePermissionMiddlewareOptions): JsonRpcMiddlewareV2 { + const origin = validateOrigin(subject); + + return async ({ request, next }) => { + const { method, params } = request; + + if (messenger.call('PermissionController:hasUnrestrictedMethod', method)) { + return next(); + } + + return messenger.call( + 'PermissionController:executeRestrictedMethod', + origin, + method, + params as RestrictedMethodParameters, + ); + }; +} + +/** + * Validates that the subject has a non-empty origin. + * + * @param subject - The subject to validate. + * @returns The subject's origin. + */ +function validateOrigin(subject: PermissionSubjectMetadata): string { + const { origin } = subject; + if (typeof origin !== 'string' || !origin) { + throw new Error('The subject "origin" must be a non-empty string.'); + } + return origin; +} From ba77ed1f7cbca7aa0e74d16c4822a44ce9776ecf Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:54:16 -0700 Subject: [PATCH 5/9] docs: Update changelog --- packages/permission-controller/CHANGELOG.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/permission-controller/CHANGELOG.md b/packages/permission-controller/CHANGELOG.md index ce2962ae8b3..40c5fc74e7d 100644 --- a/packages/permission-controller/CHANGELOG.md +++ b/packages/permission-controller/CHANGELOG.md @@ -9,24 +9,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `createPermissionMiddlewareV2`, a `JsonRpcEngineV2` variant of the standalone permission middleware factory ([#8532](https://github.com/MetaMask/core/pull/8532)). +- Add `createPermissionMiddlewareV2`, a `JsonRpcEngineV2` variant of the standalone permission middleware factory ([#8532](https://github.com/MetaMask/core/pull/8532)) ### Changed -- **BREAKING:** Decouple the permission middleware from `PermissionController` and expose it as a standalone `createPermissionMiddleware` export ([#8532](https://github.com/MetaMask/core/pull/8532)) - - Removes the `createPermissionMiddleware` property from `PermissionController`. Consumers should instead import `createPermissionMiddleware` from `@metamask/permission-controller` and call it with a messenger and subject metadata. The messenger must have the `PermissionController:executeRestrictedMethod` and `PermissionController:hasUnrestrictedMethod` actions delegated to it. - - Adds a new `PermissionMiddlewareActions` type describing the messenger actions required by the middleware. - - Exposes additional `PermissionController` methods through its messenger: `PermissionController:executeRestrictedMethod` and `PermissionController:hasUnrestrictedMethod`. Corresponding action types (`PermissionControllerExecuteRestrictedMethodAction` and `PermissionControllerHasUnrestrictedMethodAction`) are exported as well. - - Adds a public `PermissionController.hasUnrestrictedMethod(method)` predicate backing the new action. - - Removes the public `getRestrictedMethod` method from `PermissionController`. Restricted methods should be invoked via `executeRestrictedMethod` instead. - - When a restricted method returns `undefined`, the middleware now propagates the plain `Error` thrown by `PermissionController.executeRestrictedMethod` (with the message `Internal request for method "" as origin "" returned no result.`) rather than wrapping it into a custom `internalError` with a `request` data payload. The JSON-RPC engine serializes this as a standard internal error response. +- **BREAKING:** Decouple the permission middleware from `PermissionController` and expose it as a standalone function ([#8532](https://github.com/MetaMask/core/pull/8532)) + - Removes the `createPermissionMiddleware` property from `PermissionController`. Consumers should instead import `createPermissionMiddlewareV2` from `@metamask/permission-controller` and call it with a messenger and subject metadata. + - A `createPermissionMiddleware` function is exported for legacy `JsonRpcEngine` compatibility. - Bump `@metamask/controller-utils` from `^11.19.0` to `^11.20.0` ([#8344](https://github.com/MetaMask/core/pull/8344)) - Bump `@metamask/messenger` from `^1.0.0` to `^1.1.1` ([#8364](https://github.com/MetaMask/core/pull/8364), [#8373](https://github.com/MetaMask/core/pull/8373)) - Bump `@metamask/base-controller` from `^9.0.1` to `^9.1.0` ([#8457](https://github.com/MetaMask/core/pull/8457)) ### Deprecated -- Deprecate `createPermissionMiddleware` in favor of `createPermissionMiddlewareV2`, which targets `JsonRpcEngineV2` ([#8532](https://github.com/MetaMask/core/pull/8532)). +- Deprecate `createPermissionMiddleware` in favor of `createPermissionMiddlewareV2`, which targets `JsonRpcEngineV2` ([#8532](https://github.com/MetaMask/core/pull/8532)) ## [12.3.0] @@ -197,7 +193,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ["Are the Types Wrong?"](https://arethetypeswrong.github.io/) tool as ["masquerading as CJS"](https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseCJS.md). All of the ATTW checks now pass. -- Remove chunk files ([#4648](https://github.com/MetaMask/core/pull/4648)). +- Remove chunk files ([#4648](https://github.com/MetaMask/core/pull/4648)) - Previously, the build tool we used to generate JavaScript files extracted common code to "chunk" files. While this was intended to make this package more tree-shakeable, it also made debugging more difficult for our From f04c5170789f28a57a3ad24c829e1cf520ca6d8e Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 20 Apr 2026 19:28:02 -0700 Subject: [PATCH 6/9] chore: lint --- packages/permission-controller/src/PermissionController.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/permission-controller/src/PermissionController.test.ts b/packages/permission-controller/src/PermissionController.test.ts index bb836ddd6c3..bd3a122e756 100644 --- a/packages/permission-controller/src/PermissionController.test.ts +++ b/packages/permission-controller/src/PermissionController.test.ts @@ -6547,7 +6547,7 @@ describe('PermissionController', () => { messenger: middlewareMessenger, subject: { origin }, }), - () => 'success', + (): string => 'success', ], }); From 2c37556b96f1fadb4b54b5bf0b3deaed81270674 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 21 Apr 2026 14:02:29 -0700 Subject: [PATCH 7/9] refactor: Update defensive undefined result error --- .../permission-controller/src/PermissionController.test.ts | 4 ++-- packages/permission-controller/src/PermissionController.ts | 4 +++- packages/permission-controller/src/permission-middleware.ts | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/permission-controller/src/PermissionController.test.ts b/packages/permission-controller/src/PermissionController.test.ts index bd3a122e756..eee8d8c1f18 100644 --- a/packages/permission-controller/src/PermissionController.test.ts +++ b/packages/permission-controller/src/PermissionController.test.ts @@ -5628,7 +5628,7 @@ describe('PermissionController', () => { ), ).rejects.toThrow( new Error( - `Internal request for method "${PermissionNames.wallet_doubleNumber}" as origin "${origin}" returned no result.`, + `Request for method "${PermissionNames.wallet_doubleNumber}" as origin "${origin}" returned no result.`, ), ); }); @@ -6502,7 +6502,7 @@ describe('PermissionController', () => { // restricted method returns `undefined`; the JSON-RPC engine wraps it // as an internal error response. expect(error.message).toBe( - `Internal request for method "${PermissionNames.wallet_doubleNumber}" as origin "${origin}" returned no result.`, + `Request for method "${PermissionNames.wallet_doubleNumber}" as origin "${origin}" returned no result.`, ); expect(error.code).toBe(-32603); }); diff --git a/packages/permission-controller/src/PermissionController.ts b/packages/permission-controller/src/PermissionController.ts index 5c01eae5d57..97fb405b3cb 100644 --- a/packages/permission-controller/src/PermissionController.ts +++ b/packages/permission-controller/src/PermissionController.ts @@ -2771,9 +2771,11 @@ export class PermissionController< params, ); + // This is impossible if the restricted method implementation is typed correctly, + // but we maintain it for backwards compatibility. if (result === undefined) { throw new Error( - `Internal request for method "${targetName}" as origin "${origin}" returned no result.`, + `Request for method "${targetName}" as origin "${origin}" returned no result.`, ); } diff --git a/packages/permission-controller/src/permission-middleware.ts b/packages/permission-controller/src/permission-middleware.ts index 9864dad8009..75c3253a8c4 100644 --- a/packages/permission-controller/src/permission-middleware.ts +++ b/packages/permission-controller/src/permission-middleware.ts @@ -111,7 +111,7 @@ export function createPermissionMiddlewareV2({ 'PermissionController:executeRestrictedMethod', origin, method, - params as RestrictedMethodParameters, + params, ); }; } From 97cca8af8b94cc5b8e91357c618bf5860088271c Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 21 Apr 2026 14:58:57 -0700 Subject: [PATCH 8/9] refactor(permission-controller): address PR review feedback - Add v2 middleware tests for caveats, method-not-found, and undefined result - Add direct messenger action tests for hasUnrestrictedMethod and executeRestrictedMethod - Clarify changelog wording for standalone createPermissionMiddleware - Update ARCHITECTURE.md middleware example to use V2 and introduce the messenger - Clarify hasUnrestrictedMethod JSDoc about unknown methods --- .../permission-controller/ARCHITECTURE.md | 21 +- packages/permission-controller/CHANGELOG.md | 4 +- ...ermissionController-method-action-types.ts | 2 +- .../src/PermissionController.test.ts | 187 ++++++++++++++++++ .../src/PermissionController.ts | 7 +- .../src/permission-middleware.ts | 2 +- 6 files changed, 212 insertions(+), 11 deletions(-) diff --git a/packages/permission-controller/ARCHITECTURE.md b/packages/permission-controller/ARCHITECTURE.md index ca309f89044..786a4c31cc0 100644 --- a/packages/permission-controller/ARCHITECTURE.md +++ b/packages/permission-controller/ARCHITECTURE.md @@ -326,6 +326,13 @@ const permissionController = new PermissionController({ ### Adding the permission middleware +The permission middleware is created via `createPermissionMiddlewareV2` for +`JsonRpcEngineV2`, or via the deprecated `createPermissionMiddleware` for the +legacy `JsonRpcEngine`. Both factories take a messenger with the +`PermissionController:executeRestrictedMethod` and +`PermissionController:hasUnrestrictedMethod` actions, typically obtained by +delegating them from a root messenger to a subject-scoped messenger. + ```typescript // This should take place where a middleware stack is created for a particular // subject. @@ -333,11 +340,15 @@ const permissionController = new PermissionController({ // The subject could be a port, stream, socket, etc. const origin = getOrigin(subject); -const engine = new JsonRpcEngine(); -engine.push(/* your various middleware*/); -engine.push(createPermissionMiddleware({ messenger, subject: { origin } })); -// Your middleware stack is now permissioned -engine.push(/* your other various middleware*/); +// `messenger` is a messenger delegated the two actions listed above, e.g. +// via `rootMessenger.delegate({ actions: [...], messenger: subjectMessenger })`. +const engine = JsonRpcEngineV2.create({ + middleware: [ + /* your various middleware */ + createPermissionMiddlewareV2({ messenger, subject: { origin } }), + /* your other various middleware */ + ], +}); ``` ### Calling a restricted method internally diff --git a/packages/permission-controller/CHANGELOG.md b/packages/permission-controller/CHANGELOG.md index 40c5fc74e7d..f5eedc432cf 100644 --- a/packages/permission-controller/CHANGELOG.md +++ b/packages/permission-controller/CHANGELOG.md @@ -14,8 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - **BREAKING:** Decouple the permission middleware from `PermissionController` and expose it as a standalone function ([#8532](https://github.com/MetaMask/core/pull/8532)) - - Removes the `createPermissionMiddleware` property from `PermissionController`. Consumers should instead import `createPermissionMiddlewareV2` from `@metamask/permission-controller` and call it with a messenger and subject metadata. - - A `createPermissionMiddleware` function is exported for legacy `JsonRpcEngine` compatibility. + - The standalone `createPermissionMiddleware` replaces the former `PermissionController.prototype.createPermissionMiddleware`; it is imported from `@metamask/permission-controller` and called with a messenger and subject metadata, and targets the legacy `JsonRpcEngine`. + - New integrations should prefer `createPermissionMiddlewareV2`, which targets `JsonRpcEngineV2`. - Bump `@metamask/controller-utils` from `^11.19.0` to `^11.20.0` ([#8344](https://github.com/MetaMask/core/pull/8344)) - Bump `@metamask/messenger` from `^1.0.0` to `^1.1.1` ([#8364](https://github.com/MetaMask/core/pull/8364), [#8373](https://github.com/MetaMask/core/pull/8373)) - Bump `@metamask/base-controller` from `^9.0.1` to `^9.1.0` ([#8457](https://github.com/MetaMask/core/pull/8457)) diff --git a/packages/permission-controller/src/PermissionController-method-action-types.ts b/packages/permission-controller/src/PermissionController-method-action-types.ts index 50519fdda30..df69fba2cc8 100644 --- a/packages/permission-controller/src/PermissionController-method-action-types.ts +++ b/packages/permission-controller/src/PermissionController-method-action-types.ts @@ -73,7 +73,7 @@ export type PermissionControllerHasPermissionsAction = { /** * Revokes all permissions from the specified origin. * - * Throws an error of the origin has no permissions. + * Throws an error if the origin has no permissions. * * @param origin - The origin whose permissions to revoke. */ diff --git a/packages/permission-controller/src/PermissionController.test.ts b/packages/permission-controller/src/PermissionController.test.ts index eee8d8c1f18..e5193ffad8a 100644 --- a/packages/permission-controller/src/PermissionController.test.ts +++ b/packages/permission-controller/src/PermissionController.test.ts @@ -6220,6 +6220,66 @@ describe('PermissionController', () => { .caveats[0], ); }); + + it('action: PermissionController:hasUnrestrictedMethod', () => { + const messenger = getRootMessenger(); + const options = getPermissionControllerOptions({ + messenger: getPermissionControllerMessenger(messenger), + }); + // eslint-disable-next-line no-new + new PermissionController< + DefaultPermissionSpecifications, + DefaultCaveatSpecifications + >(options); + + expect( + messenger.call( + 'PermissionController:hasUnrestrictedMethod', + 'wallet_unrestrictedMethod', + ), + ).toBe(true); + + expect( + messenger.call( + 'PermissionController:hasUnrestrictedMethod', + PermissionNames.wallet_getSecretArray, + ), + ).toBe(false); + + expect( + messenger.call( + 'PermissionController:hasUnrestrictedMethod', + 'wallet_unknownMethod', + ), + ).toBe(false); + }); + + it('action: PermissionController:executeRestrictedMethod', async () => { + const messenger = getRootMessenger(); + const options = getPermissionControllerOptions({ + messenger: getPermissionControllerMessenger(messenger), + }); + const controller = new PermissionController< + DefaultPermissionSpecifications, + DefaultCaveatSpecifications + >(options); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: {}, + }, + }); + + const result = await messenger.call( + 'PermissionController:executeRestrictedMethod', + origin, + PermissionNames.wallet_getSecretArray, + ); + + expect(result).toStrictEqual(['a', 'b', 'c']); + }); }); describe('permission middleware', () => { @@ -6601,6 +6661,133 @@ describe('PermissionController', () => { 'Unauthorized to perform action. Try requesting the required permission(s) first.', ); }); + + it('executes a restricted method with a caveat', async () => { + const { controller, middlewareMessenger } = setup(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: { + caveats: [ + { type: CaveatTypes.filterArrayResponse, value: ['b'] }, + ], + }, + }, + }); + + const engine = JsonRpcEngineV2.create({ + middleware: [ + createPermissionMiddlewareV2({ + messenger: middlewareMessenger, + subject: { origin }, + }), + ], + }); + + const result = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: PermissionNames.wallet_getSecretArray, + }); + + expect(result).toStrictEqual(['b']); + }); + + it('executes a restricted method with multiple caveats', async () => { + const { controller, middlewareMessenger } = setup(); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_getSecretArray]: { + caveats: [ + { type: CaveatTypes.filterArrayResponse, value: ['a', 'c'] }, + { type: CaveatTypes.reverseArrayResponse, value: null }, + ], + }, + }, + }); + + const engine = JsonRpcEngineV2.create({ + middleware: [ + createPermissionMiddlewareV2({ + messenger: middlewareMessenger, + subject: { origin }, + }), + ], + }); + + const result = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: PermissionNames.wallet_getSecretArray, + }); + + expect(result).toStrictEqual(['c', 'a']); + }); + + it('throws if the method does not exist', async () => { + const { middlewareMessenger } = setup(); + const origin = 'metamask.io'; + + const engine = JsonRpcEngineV2.create({ + middleware: [ + createPermissionMiddlewareV2({ + messenger: middlewareMessenger, + subject: { origin }, + }), + ], + }); + + await expect( + engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'wallet_foo', + }), + ).rejects.toThrow(errors.methodNotFound('wallet_foo', { origin })); + }); + + it('throws if the restricted method returns undefined', async () => { + const permissionSpecifications = getDefaultPermissionSpecifications(); + // @ts-expect-error Intentional destructive testing + permissionSpecifications.wallet_doubleNumber.methodImplementation = + (): undefined => undefined; + + const { controller, middlewareMessenger } = setup({ + permissionSpecifications, + }); + const origin = 'metamask.io'; + + controller.grantPermissions({ + subject: { origin }, + approvedPermissions: { + [PermissionNames.wallet_doubleNumber]: {}, + }, + }); + + const engine = JsonRpcEngineV2.create({ + middleware: [ + createPermissionMiddlewareV2({ + messenger: middlewareMessenger, + subject: { origin }, + }), + ], + }); + + await expect( + engine.handle({ + jsonrpc: '2.0', + id: 1, + method: PermissionNames.wallet_doubleNumber, + }), + ).rejects.toThrow( + `Request for method "${PermissionNames.wallet_doubleNumber}" as origin "${origin}" returned no result.`, + ); + }); }); }); diff --git a/packages/permission-controller/src/PermissionController.ts b/packages/permission-controller/src/PermissionController.ts index 97fb405b3cb..44d1729478a 100644 --- a/packages/permission-controller/src/PermissionController.ts +++ b/packages/permission-controller/src/PermissionController.ts @@ -666,7 +666,10 @@ export class PermissionController< } /** - * Checks whether the given method is an unrestricted method. + * Checks whether the given method was declared as unrestricted at + * construction time. Methods unknown to the controller return `false` and + * would be treated as restricted by callers such as the permission + * middleware. * * @param method - The name of the method to check. * @returns Whether the method is unrestricted. @@ -1014,7 +1017,7 @@ export class PermissionController< /** * Revokes all permissions from the specified origin. * - * Throws an error of the origin has no permissions. + * Throws an error if the origin has no permissions. * * @param origin - The origin whose permissions to revoke. */ diff --git a/packages/permission-controller/src/permission-middleware.ts b/packages/permission-controller/src/permission-middleware.ts index 75c3253a8c4..907509cb12b 100644 --- a/packages/permission-controller/src/permission-middleware.ts +++ b/packages/permission-controller/src/permission-middleware.ts @@ -25,7 +25,7 @@ export type PermissionMiddlewareActions = | PermissionControllerExecuteRestrictedMethodAction | PermissionControllerHasUnrestrictedMethodAction; -type CreatePermissionMiddlewareOptions = { +export type CreatePermissionMiddlewareOptions = { messenger: Messenger; subject: PermissionSubjectMetadata; }; From 9eb56a601457f12b63c037e713bc33211dbb0cc1 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:04:54 -0700 Subject: [PATCH 9/9] chore: lint --- .../src/PermissionController-method-action-types.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/permission-controller/src/PermissionController-method-action-types.ts b/packages/permission-controller/src/PermissionController-method-action-types.ts index df69fba2cc8..76312a01d3a 100644 --- a/packages/permission-controller/src/PermissionController-method-action-types.ts +++ b/packages/permission-controller/src/PermissionController-method-action-types.ts @@ -6,7 +6,10 @@ import type { PermissionController } from './PermissionController'; /** - * Checks whether the given method is an unrestricted method. + * Checks whether the given method was declared as unrestricted at + * construction time. Methods unknown to the controller return `false` and + * would be treated as restricted by callers such as the permission + * middleware. * * @param method - The name of the method to check. * @returns Whether the method is unrestricted.