-
-
Notifications
You must be signed in to change notification settings - Fork 277
refactor(permission-controller)!: Add messenger alternative to restricted method hooks #8551
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1564307
a44dfea
6fd37f5
4c74826
1e2fe74
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -484,15 +484,19 @@ export type PermissionSpecificationConstraint = | |
| * Options for {@link PermissionSpecificationBuilder} functions. | ||
| */ | ||
| type PermissionSpecificationBuilderOptions< | ||
| FactoryHooks extends Record<string, unknown>, | ||
| MethodHooks extends Record<string, unknown>, | ||
| ValidatorHooks extends Record<string, unknown>, | ||
| SpecMessenger = unknown, | ||
| > = { | ||
| targetName?: string; | ||
| allowedCaveats?: Readonly<NonEmptyArray<string>> | null; | ||
| factoryHooks?: FactoryHooks; | ||
| methodHooks?: MethodHooks; | ||
| validatorHooks?: ValidatorHooks; | ||
| /** | ||
| * A messenger scoped to this permission specification. The messenger is | ||
| * expected to have exactly the actions declared by the spec's `actionNames` | ||
| * delegated to it; {@link createRestrictedMethodMessenger} is the canonical | ||
| * way to construct it. | ||
| */ | ||
| messenger?: SpecMessenger; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be nice if we could infer the messenger ala
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's too much work to refactor. We should just do #4238 instead. |
||
| }; | ||
|
|
||
| /** | ||
|
|
@@ -504,35 +508,13 @@ type PermissionSpecificationBuilderOptions< | |
| export type PermissionSpecificationBuilder< | ||
| Type extends PermissionType, | ||
| Options extends PermissionSpecificationBuilderOptions< | ||
| Record<string, unknown>, | ||
| Record<string, unknown>, | ||
| Record<string, unknown> | ||
| >, | ||
| Specification extends PermissionSpecificationConstraint & { | ||
| permissionType: Type; | ||
| }, | ||
| > = (options: Options) => Specification; | ||
|
|
||
| /** | ||
| * A restricted method permission export object, containing the | ||
| * {@link PermissionSpecificationBuilder} function and "hook name" objects. | ||
| */ | ||
| export type PermissionSpecificationBuilderExportConstraint = { | ||
| targetName: string; | ||
| specificationBuilder: PermissionSpecificationBuilder< | ||
| PermissionType, | ||
| PermissionSpecificationBuilderOptions< | ||
| Record<string, unknown>, | ||
| Record<string, unknown>, | ||
| Record<string, unknown> | ||
| >, | ||
| PermissionSpecificationConstraint | ||
| >; | ||
| factoryHookNames?: Record<string, true>; | ||
| methodHookNames?: Record<string, true>; | ||
| validatorHookNames?: Record<string, true>; | ||
| }; | ||
|
|
||
| type ValidRestrictedMethodSpecification< | ||
| Specification extends RestrictedMethodSpecificationConstraint, | ||
| > = | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| import { Messenger } from '@metamask/messenger'; | ||
|
|
||
| import { createRestrictedMethodMessenger } from './createRestrictedMethodMessenger'; | ||
|
|
||
| type FooAction = { | ||
| type: 'Foo:ping'; | ||
| handler: () => string; | ||
| }; | ||
|
|
||
| type BarAction = { | ||
| type: 'Bar:double'; | ||
| handler: (n: number) => number; | ||
| }; | ||
|
|
||
| type RootActions = FooAction | BarAction; | ||
|
|
||
| const getRootMessenger = (): Messenger<'Root', RootActions> => { | ||
| const messenger = new Messenger<'Root', RootActions>({ namespace: 'Root' }); | ||
| const fooMessenger = new Messenger<'Foo', FooAction, never, typeof messenger>( | ||
| { | ||
| namespace: 'Foo', | ||
| parent: messenger, | ||
| }, | ||
| ); | ||
| const barMessenger = new Messenger<'Bar', BarAction, never, typeof messenger>( | ||
| { | ||
| namespace: 'Bar', | ||
| parent: messenger, | ||
| }, | ||
| ); | ||
| fooMessenger.registerActionHandler('Foo:ping', () => 'pong'); | ||
| barMessenger.registerActionHandler('Bar:double', (value) => value * 2); | ||
| return messenger; | ||
| }; | ||
|
|
||
| describe('createRestrictedMethodMessenger', () => { | ||
| it('returns undefined when actionNames is omitted', () => { | ||
| const rootMessenger = getRootMessenger(); | ||
|
|
||
| expect( | ||
| createRestrictedMethodMessenger({ | ||
| rootMessenger, | ||
| namespace: 'wallet_example', | ||
| }), | ||
| ).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('returns undefined when actionNames is empty', () => { | ||
| const rootMessenger = getRootMessenger(); | ||
|
|
||
| expect( | ||
| createRestrictedMethodMessenger({ | ||
| rootMessenger, | ||
| namespace: 'wallet_example', | ||
| // @ts-expect-error An empty array is rejected by the type system, but | ||
| // the runtime handles it as a no-op. | ||
| actionNames: [], | ||
| }), | ||
| ).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('exposes the requested action on the returned messenger', () => { | ||
| const rootMessenger = getRootMessenger(); | ||
|
|
||
| const messenger = createRestrictedMethodMessenger({ | ||
| rootMessenger, | ||
| namespace: 'wallet_example', | ||
| actionNames: ['Foo:ping'] as const, | ||
| }); | ||
|
|
||
| expect(messenger.call('Foo:ping')).toBe('pong'); | ||
| }); | ||
|
|
||
| it('uses the provided namespace for the returned messenger', () => { | ||
| const rootMessenger = getRootMessenger(); | ||
|
|
||
| const messenger = createRestrictedMethodMessenger({ | ||
| rootMessenger, | ||
| namespace: 'wallet_example', | ||
| actionNames: ['Foo:ping'] as const, | ||
| }); | ||
|
|
||
| // Registering an action under a different namespace must fail with an | ||
| // error that names the messenger's configured namespace. | ||
| expect(() => | ||
| // @ts-expect-error Deliberately registering outside the child's action | ||
| // surface to probe its namespace. | ||
| messenger.registerActionHandler('Other:noop', () => undefined), | ||
| ).toThrow(/wallet_example/u); | ||
| }); | ||
|
|
||
| it('rejects calls to actions that were not requested', () => { | ||
| const rootMessenger = getRootMessenger(); | ||
|
|
||
| const messenger = createRestrictedMethodMessenger({ | ||
| rootMessenger, | ||
| namespace: 'wallet_example', | ||
| actionNames: ['Foo:ping'] as const, | ||
| }); | ||
|
|
||
| expect(() => | ||
| // @ts-expect-error Intentionally calling an undelegated action. | ||
| messenger.call('Bar:double', 2), | ||
| ).toThrow(/Bar:double/u); | ||
| }); | ||
|
|
||
| it('exposes every requested action when multiple are delegated', () => { | ||
| const rootMessenger = getRootMessenger(); | ||
|
|
||
| const messenger = createRestrictedMethodMessenger({ | ||
| rootMessenger, | ||
| namespace: 'wallet_example', | ||
| actionNames: ['Foo:ping', 'Bar:double'] as const, | ||
| }); | ||
|
|
||
| expect(messenger.call('Foo:ping')).toBe('pong'); | ||
| expect(messenger.call('Bar:double', 3)).toBe(6); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| import type { | ||
| ActionConstraint, | ||
| EventConstraint, | ||
| MessengerActions, | ||
| } from '@metamask/messenger'; | ||
| import { Messenger } from '@metamask/messenger'; | ||
|
|
||
| /** | ||
| * The subset of `RootMessenger`'s actions selected by `DelegatedActions`. | ||
| */ | ||
| type SelectedActions< | ||
| RootMessenger extends Messenger<string, ActionConstraint, EventConstraint>, | ||
| DelegatedActions extends readonly MessengerActions<RootMessenger>['type'][], | ||
| > = Extract< | ||
| MessengerActions<RootMessenger>, | ||
| { type: DelegatedActions[number] } | ||
| >; | ||
|
|
||
| /** | ||
| * Create a child messenger scoped to a restricted-method permission | ||
| * specification, delegating only the spec's declared actions from the root | ||
| * messenger. This produces a minimally-scoped messenger whose action surface | ||
| * matches exactly what the spec has declared it needs. | ||
| * | ||
| * Returns `undefined` when `actionNames` is omitted — there is nothing to | ||
| * scope, and the builder can be invoked without a messenger. | ||
| * | ||
| * @param args - The arguments. | ||
| * @param args.rootMessenger - The root messenger to delegate actions from. | ||
| * @param args.namespace - The namespace for the scoped child messenger, | ||
| * typically the spec's `targetName`. | ||
| * @param args.actionNames - The action types the specification requires, | ||
| * typically the spec's declared `actionNames`. Must be a non-empty tuple of | ||
| * action types that exist on the root messenger. | ||
| * @returns A scoped child messenger with the requested actions delegated, or | ||
| * `undefined` if no actions were requested. | ||
| */ | ||
| export function createRestrictedMethodMessenger< | ||
| Namespace extends string, | ||
| RootMessenger extends Messenger<string, ActionConstraint, EventConstraint>, | ||
| DelegatedActions extends readonly [ | ||
| MessengerActions<RootMessenger>['type'], | ||
| ...MessengerActions<RootMessenger>['type'][], | ||
| ], | ||
| >(args: { | ||
| rootMessenger: RootMessenger; | ||
| namespace: Namespace; | ||
| actionNames: DelegatedActions; | ||
| }): Messenger< | ||
| Namespace, | ||
| SelectedActions<RootMessenger, DelegatedActions>, | ||
| never, | ||
| RootMessenger | ||
| >; | ||
|
|
||
| export function createRestrictedMethodMessenger< | ||
| Namespace extends string, | ||
| RootMessenger extends Messenger<string, ActionConstraint, EventConstraint>, | ||
| >(args: { | ||
| rootMessenger: RootMessenger; | ||
| namespace: Namespace; | ||
| actionNames?: undefined; | ||
| }): undefined; | ||
|
|
||
| export function createRestrictedMethodMessenger< | ||
| Namespace extends string, | ||
| RootMessenger extends Messenger<string, ActionConstraint, EventConstraint>, | ||
| DelegatedActions extends [ | ||
| MessengerActions<RootMessenger>['type'], | ||
| ...MessengerActions<RootMessenger>['type'][], | ||
| ], | ||
| >({ | ||
| rootMessenger, | ||
| namespace, | ||
| actionNames, | ||
| }: { | ||
| rootMessenger: RootMessenger; | ||
| namespace: Namespace; | ||
| actionNames?: DelegatedActions; | ||
| }): | ||
| | Messenger< | ||
| Namespace, | ||
| SelectedActions<RootMessenger, DelegatedActions>, | ||
| never, | ||
| RootMessenger | ||
| > | ||
| | undefined { | ||
| if (!actionNames?.length) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const restrictedMethodMessenger = new Messenger< | ||
| Namespace, | ||
| SelectedActions<RootMessenger, DelegatedActions>, | ||
| never, | ||
| RootMessenger | ||
| >({ | ||
| namespace, | ||
| parent: rootMessenger, | ||
| }); | ||
|
|
||
| rootMessenger.delegate({ | ||
| actions: actionNames, | ||
| messenger: restrictedMethodMessenger, | ||
| }); | ||
|
|
||
| return restrictedMethodMessenger; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More generally do we still want to keep
PermittedHandlerExportor should that be replaced byMethodHandler?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be an improvement but it's orthogonal to this PR. Also not entirely clear to me if now is the time to force these methods onto
JsonRpcEngineV2.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is worth it to decide on this before we roll out this change. It can be done in a follow-up PR though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably just ship a non-breaking
/v2version of these methods if we do it.