Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7f406f0
Add gator permissions controller to extension
V00D00-child Sep 3, 2025
114ffef
bugfix: GatorPermissionsController Initialization Error
V00D00-child Sep 3, 2025
e5d632f
Add GatorPermissionsController to CONTROLLER_MESSENGERS
V00D00-child Sep 3, 2025
2379b7f
Merge branch 'main' into feat/gator-permissions-controller-integration
V00D00-child Sep 4, 2025
5b2e114
Drop npm preview of gator-permissions-controller for initial publishe…
V00D00-child Sep 4, 2025
5230e87
Merge branch 'main' into feat/gator-permissions-controller-integration
V00D00-child Sep 4, 2025
77b3d8f
Drop manual policies update to allow metamaskbot to update policies
V00D00-child Sep 4, 2025
0770130
Update LavaMoat policies
metamaskbot Sep 4, 2025
07ae174
Bugfix: Make GatorPermissionsController API Methods Available in all …
V00D00-child Sep 4, 2025
9bf3f9a
Merge branch 'main' into feat/gator-permissions-controller-integration
V00D00-child Sep 4, 2025
3ac4548
Resolve lint errors due to different BaseController versions
V00D00-child Sep 4, 2025
92ac2f4
Fix lockfile dedupe
V00D00-child Sep 4, 2025
e567aef
Merge branch 'main' into feat/gator-permissions-controller-integration
V00D00-child Sep 4, 2025
a61d905
Update LavaMoat policies
metamaskbot Sep 4, 2025
6068da5
Merge branch 'main' into feat/gator-permissions-controller-integration
V00D00-child Sep 5, 2025
fc0e1e1
Add GatorPermissionsController to e2e state-snapshot
V00D00-child Sep 5, 2025
a8381cb
Add GatorPermissionsController state props to metamask e2e state
V00D00-child Sep 5, 2025
ad5b560
Fix typo in test
V00D00-child Sep 5, 2025
9312634
Fix lint errors and only include GatorPermissionsController in state-…
V00D00-child Sep 5, 2025
6983ff6
Use GATOR_PERMISSIONS_PROVIDER_SNAP_ID build var. Remove enableGatorP…
V00D00-child Sep 8, 2025
eebfa3b
Merge branch 'main' into feat/gator-permissions-controller-integration
V00D00-child Sep 8, 2025
d9f98ba
bugfix: Invalid SnapId Environment Variable Causes Errors
V00D00-child Sep 8, 2025
3de87bc
Ensure gatorPermissionsProviderSnapId is set undefined in e2e test
V00D00-child Sep 8, 2025
0468526
Remove gatorPermissionsProviderSnapId from e2e test to allow it to po…
V00D00-child Sep 8, 2025
dc5f231
Only valid snapId is set otherwise we rely on default value set in co…
V00D00-child Sep 8, 2025
ad7fa04
Throws error when snapId is invalid
V00D00-child Sep 9, 2025
3f1d820
Merge branch 'main' into feat/gator-permissions-controller-integration
V00D00-child Sep 9, 2025
c5012cd
Use empty string for GATOR_PERMISSIONS_PROVIDER_SNAP_ID
V00D00-child Sep 9, 2025
a891f16
Remove throwing error
V00D00-child Sep 9, 2025
9ea607a
Reject invalid GATOR_PERMISSIONS_PROVIDER_SNAP_ID configuration (#35775)
jeffsmale90 Sep 10, 2025
42e94bd
Merge branch 'main' into feat/gator-permissions-controller-integration
jeffsmale90 Sep 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/scripts/controller-init/controller-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { RemoteFeatureFlagController } from '@metamask/remote-feature-flag-contr
import { AccountTreeController } from '@metamask/account-tree-controller';
import { SeedlessOnboardingController } from '@metamask/seedless-onboarding-controller';
import { EncryptionKey } from '@metamask/browser-passworder';
import { GatorPermissionsController } from '@metamask/gator-permissions-controller';
import { ShieldController } from '@metamask/shield-controller';
import OnboardingController from '../controllers/onboarding';
import { PreferencesController } from '../controllers/preferences-controller';
Expand All @@ -65,6 +66,7 @@ export type Controller =
| DeFiPositionsController
| ExecutionService
| GasFeeController
| GatorPermissionsController
| JsonSnapsRegistry
| KeyringController
| MetaMetricsController
Expand Down Expand Up @@ -114,6 +116,7 @@ export type ControllerFlatState = AccountsController['state'] &
DeFiPositionsController['state'] &
DelegationController['state'] &
GasFeeController['state'] &
GatorPermissionsController['state'] &
JsonSnapsRegistry['state'] &
KeyringController['state'] &
MultichainAssetsController['state'] &
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { GatorPermissionsController } from '@metamask/gator-permissions-controller';
import { Messenger } from '@metamask/base-controller';
import { buildControllerInitRequestMock } from '../test/utils';
import type { ControllerInitRequest } from '../types';
import { isGatorPermissionsFeatureEnabled } from '../../../../shared/modules/environment';
import {
getGatorPermissionsControllerMessenger,
GatorPermissionsControllerMessenger,
} from '../messengers/gator-permissions';
import { GatorPermissionsControllerInit } from './gator-permissions-controller-init';

jest.mock('@metamask/gator-permissions-controller');
jest.mock('../../../../shared/modules/environment');

function buildInitRequestMock(): jest.Mocked<
ControllerInitRequest<GatorPermissionsControllerMessenger>
> {
const baseControllerMessenger = new Messenger();

return {
...buildControllerInitRequestMock(),
controllerMessenger: getGatorPermissionsControllerMessenger(
baseControllerMessenger,
),
initMessenger: undefined,
};
}

describe('GatorPermissionsControllerInit', () => {
const MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID = 'npm:mock-snap-id';
const GatorPermissionsControllerClassMock = jest.mocked(
GatorPermissionsController,
);
const originalGatorPermissionProviderSnapId =
process.env.GATOR_PERMISSIONS_PROVIDER_SNAP_ID;

beforeEach(() => {
jest.resetAllMocks();
jest.mocked(isGatorPermissionsFeatureEnabled).mockReturnValue(true);
process.env.GATOR_PERMISSIONS_PROVIDER_SNAP_ID =
MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID;
});

afterEach(() => {
if (originalGatorPermissionProviderSnapId) {
process.env.GATOR_PERMISSIONS_PROVIDER_SNAP_ID =
originalGatorPermissionProviderSnapId;
} else {
delete process.env.GATOR_PERMISSIONS_PROVIDER_SNAP_ID;
}
});

it('returns controller instance', () => {
const requestMock = buildInitRequestMock();
expect(
GatorPermissionsControllerInit(requestMock).controller,
).toBeInstanceOf(GatorPermissionsController);
});

it('initializes with correct messenger and state(gator permissions feature enabled)', () => {
const requestMock = buildInitRequestMock();
jest.mocked(isGatorPermissionsFeatureEnabled).mockReturnValue(true);
GatorPermissionsControllerInit(requestMock);

expect(GatorPermissionsControllerClassMock).toHaveBeenCalledWith({
messenger: requestMock.controllerMessenger,
state: {
isGatorPermissionsEnabled: true,
gatorPermissionsProviderSnapId: MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID,
...requestMock.persistedState.GatorPermissionsController,
},
});
});
Comment thread
cursor[bot] marked this conversation as resolved.

it('initializes with correct messenger and state(gator permissions feature disabled)', () => {
const requestMock = buildInitRequestMock();
jest.mocked(isGatorPermissionsFeatureEnabled).mockReturnValue(false);
GatorPermissionsControllerInit(requestMock);

expect(GatorPermissionsControllerClassMock).toHaveBeenCalledWith({
messenger: requestMock.controllerMessenger,
state: {
isGatorPermissionsEnabled: false,
gatorPermissionsProviderSnapId: MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID,
...requestMock.persistedState.GatorPermissionsController,
},
});
});

it('returns correct API methods', () => {
const requestMock = buildInitRequestMock();
const result = GatorPermissionsControllerInit(requestMock);

expect(result.api).toEqual({
fetchAndUpdateGatorPermissions: expect.any(Function),
});
});

it('handles undefined persistedState.GatorPermissionsController', () => {
const requestMock = buildInitRequestMock();
requestMock.persistedState.GatorPermissionsController = undefined;
jest.mocked(isGatorPermissionsFeatureEnabled).mockReturnValue(true);

GatorPermissionsControllerInit(requestMock);

expect(GatorPermissionsControllerClassMock).toHaveBeenCalledWith({
messenger: requestMock.controllerMessenger,
state: {
isGatorPermissionsEnabled: true,
gatorPermissionsProviderSnapId: MOCK_GATOR_PERMISSIONS_PROVIDER_SNAP_ID,
},
});
});

it('resolves the default when GATOR_PERMISSIONS_PROVIDER_SNAP_ID is not specified', () => {
const requestMock = buildInitRequestMock();

delete process.env.GATOR_PERMISSIONS_PROVIDER_SNAP_ID;

GatorPermissionsControllerInit(requestMock);

expect(GatorPermissionsControllerClassMock).toHaveBeenCalledWith({
messenger: requestMock.controllerMessenger,
state: {
isGatorPermissionsEnabled: true,
},
});

const calledWithState =
GatorPermissionsControllerClassMock.mock.calls[0][0].state;

expect(calledWithState).toEqual({
isGatorPermissionsEnabled: true,
});

// GatorPermissionsController requires that the key does not exist if the snap id is not specified
expect(
Object.prototype.hasOwnProperty.call(
calledWithState,
'gatorPermissionsProviderSnapId',
),
).toBe(false);
});

describe('GATOR_PERMISSIONS_PROVIDER_SNAP_ID incorrectly specified', () => {
['', ' ', 'invalid-snap-id'].forEach((invalidSnapId) => {
it(`throws when provided invalid GATOR_PERMISSIONS_PROVIDER_SNAP_ID: ${invalidSnapId}`, () => {
const requestMock = buildInitRequestMock();

process.env.GATOR_PERMISSIONS_PROVIDER_SNAP_ID =
invalidSnapId as string;

expect(() => GatorPermissionsControllerInit(requestMock)).toThrow(
'GATOR_PERMISSIONS_PROVIDER_SNAP_ID must be set to a valid snap id',
);

expect(GatorPermissionsControllerClassMock).not.toHaveBeenCalled();
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {
GatorPermissionsController,
GatorPermissionsControllerState,
} from '@metamask/gator-permissions-controller';
import { assertIsValidSnapId } from '@metamask/snaps-utils';
import { ControllerInitFunction } from '../types';
import { isGatorPermissionsFeatureEnabled } from '../../../../shared/modules/environment';
import { GatorPermissionsControllerMessenger } from '../messengers/gator-permissions';

const generateDefaultGatorPermissionsControllerState =
(): Partial<GatorPermissionsControllerState> => {
const gatorPermissionsProviderSnapId =
process.env.GATOR_PERMISSIONS_PROVIDER_SNAP_ID;

// if GATOR_PERMISSIONS_PROVIDER_SNAP_ID is not specified, GatorPermissionsController will initialize it's default
if (gatorPermissionsProviderSnapId !== undefined) {
try {
assertIsValidSnapId(gatorPermissionsProviderSnapId);
} catch (error) {
throw new Error(
'GATOR_PERMISSIONS_PROVIDER_SNAP_ID must be set to a valid snap id',
{
cause: error,
},
);
}
}

const isGatorPermissionsEnabled = isGatorPermissionsFeatureEnabled();

const state: Partial<GatorPermissionsControllerState> = {
isGatorPermissionsEnabled,
};
Comment thread
cursor[bot] marked this conversation as resolved.

if (gatorPermissionsProviderSnapId) {
state.gatorPermissionsProviderSnapId = gatorPermissionsProviderSnapId;
}

return state;
};

export const GatorPermissionsControllerInit: ControllerInitFunction<
GatorPermissionsController,
GatorPermissionsControllerMessenger
> = ({ controllerMessenger, persistedState }) => {
const controller = new GatorPermissionsController({
// Type mismatch due to different BaseController versions, GatorPermissionsController uses 8.3.0 while extension uses 8.2.0.
// We can remove once extension BaseController version is updated to 8.3.0.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
messenger: controllerMessenger as any,
state: {
...generateDefaultGatorPermissionsControllerState(),
...persistedState.GatorPermissionsController,
},
});

return {
controller,
api: {
fetchAndUpdateGatorPermissions:
controller.fetchAndUpdateGatorPermissions.bind(controller),
},
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Messenger, RestrictedMessenger } from '@metamask/base-controller';
import { getGatorPermissionsControllerMessenger } from './gator-permissions-controller-messenger';

describe('getGatorPermissionsControllerMessenger', () => {
it('returns a restricted messenger', () => {
const messenger = new Messenger<never, never>();
const gatorPermissionsControllerMessenger =
getGatorPermissionsControllerMessenger(messenger);

expect(gatorPermissionsControllerMessenger).toBeInstanceOf(
RestrictedMessenger,
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Messenger } from '@metamask/base-controller';
import { GatorPermissionsControllerStateChangeEvent } from '@metamask/gator-permissions-controller';
import { HandleSnapRequest, HasSnap } from '@metamask/snaps-controllers';

export type GatorPermissionsControllerMessenger = ReturnType<
typeof getGatorPermissionsControllerMessenger
>;

type MessengerActions = HandleSnapRequest | HasSnap;
type MessengerEvents = GatorPermissionsControllerStateChangeEvent;

/**
* Get a restricted messenger for the Gator Permissions controller. This is scoped to the
* actions and events that the Gator Permissions controller is allowed to handle.
*
* @param messenger - The messenger to restrict.
* @returns The restricted messenger.
*/
export function getGatorPermissionsControllerMessenger(
messenger: Messenger<MessengerActions, MessengerEvents>,
) {
return messenger.getRestricted({
name: 'GatorPermissionsController',
allowedActions: ['SnapController:handleRequest', 'SnapController:has'],
allowedEvents: [],
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { getGatorPermissionsControllerMessenger } from './gator-permissions-controller-messenger';
export type { GatorPermissionsControllerMessenger } from './gator-permissions-controller-messenger';
5 changes: 5 additions & 0 deletions app/scripts/controller-init/messengers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import {
getShieldControllerInitMessenger,
getShieldControllerMessenger,
} from './shield/shield-controller-messenger';
import { getGatorPermissionsControllerMessenger } from './gator-permissions/gator-permissions-controller-messenger';

export const CONTROLLER_MESSENGERS = {
AuthenticationController: {
Expand All @@ -85,6 +86,10 @@ export const CONTROLLER_MESSENGERS = {
getMessenger: getExecutionServiceMessenger,
getInitMessenger: noop,
},
GatorPermissionsController: {
getMessenger: getGatorPermissionsControllerMessenger,
getInitMessenger: noop,
},
InstitutionalSnapController: {
getMessenger: getInstitutionalSnapControllerMessenger,
getInitMessenger: noop,
Expand Down
12 changes: 12 additions & 0 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@ import {
isSendBundleSupported,
} from './lib/transaction/sentinel-api';
import { ShieldControllerInit } from './controller-init/shield/shield-controller-init';
import { GatorPermissionsControllerInit } from './controller-init/gator-permissions/gator-permissions-controller-init';

import { forwardRequestToSnap } from './lib/forwardRequestToSnap';

Expand Down Expand Up @@ -1962,6 +1963,7 @@ export default class MetamaskController extends EventEmitter {
SeedlessOnboardingController: SeedlessOnboardingControllerInit,
NetworkOrderController: NetworkOrderControllerInit,
ShieldController: ShieldControllerInit,
GatorPermissionsController: GatorPermissionsControllerInit,
};

const {
Expand Down Expand Up @@ -2022,6 +2024,8 @@ export default class MetamaskController extends EventEmitter {
controllersByName.SeedlessOnboardingController;
this.networkOrderController = controllersByName.NetworkOrderController;
this.shieldController = controllersByName.ShieldController;
this.gatorPermissionsController =
controllersByName.GatorPermissionsController;

this.getSecurityAlertsConfig = () => {
return async (url) => {
Expand Down Expand Up @@ -2228,6 +2232,7 @@ export default class MetamaskController extends EventEmitter {
NetworkOrderController: this.networkOrderController,
AccountOrderController: this.accountOrderController,
GasFeeController: this.gasFeeController,
GatorPermissionsController: this.gatorPermissionsController,
TokenListController: this.tokenListController,
TokensController: this.tokensController,
TokenBalancesController: this.tokenBalancesController,
Expand Down Expand Up @@ -3441,6 +3446,7 @@ export default class MetamaskController extends EventEmitter {
ensController,
tokenListController,
gasFeeController,
gatorPermissionsController,
metaMetricsController,
networkController,
multichainNetworkController,
Expand Down Expand Up @@ -3901,6 +3907,12 @@ export default class MetamaskController extends EventEmitter {
syncSeedPhrases: this.syncSeedPhrases.bind(this),
changePassword: this.changePassword.bind(this),

// GatorPermissionsController
fetchAndUpdateGatorPermissions:
gatorPermissionsController.fetchAndUpdateGatorPermissions.bind(
gatorPermissionsController,
),

// KeyringController
setLocked: this.setLocked.bind(this),
createNewVaultAndKeychain: this.createNewVaultAndKeychain.bind(this),
Expand Down
6 changes: 2 additions & 4 deletions builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,6 @@ buildTypes:
- GOOGLE_CLIENT_ID_REF: GOOGLE_FLASK_CLIENT_ID
- APPLE_CLIENT_ID_REF: APPLE_FLASK_CLIENT_ID

# EIP-7715 Readable Permissions
- PERMISSIONS_KERNEL_SNAP_ID: ''
- GATOR_PERMISSIONS_ENABLED: false
isPrerelease: true
manifestOverrides: ./app/build-types/flask/manifest/
buildNameOverride: MetaMask Flask
Expand Down Expand Up @@ -330,8 +327,9 @@ env:
- METAMASK_RAMP_API_CONTENT_BASE_URL: https://on-ramp-content.api.cx.metamask.io

# EIP-7715 Readable Permissions
- PERMISSIONS_KERNEL_SNAP_ID: ''
- GATOR_PERMISSIONS_ENABLED: false
- PERMISSIONS_KERNEL_SNAP_ID: 'npm:@metamask/permissions-kernel-snap'
- GATOR_PERMISSIONS_PROVIDER_SNAP_ID: 'npm:@metamask/gator-permissions-snap'

###
# Meta variables
Expand Down
Loading
Loading