11import { ObservableStore } from '@metamask/obs-store' ;
22import EventEmitter from '@metamask/safe-event-emitter' ;
3- import { serializeError } from 'eth-rpc-errors' ;
3+ import { ethErrors , serializeError } from 'eth-rpc-errors' ;
44import { IOcapLdCapability } from 'rpc-cap/dist/src/@types/ocap-ld' ;
55import { IRequestedPermissions } from 'rpc-cap/dist/src/@types' ;
6-
76import { WorkerController , SetupWorkerConnection } from '../workers/WorkerController' ;
87import { CommandResponse } from '../workers/CommandEngine' ;
98import { INLINE_PLUGINS } from './inlinePlugins' ;
109
1110export const PLUGIN_PREFIX = 'wallet_plugin_' ;
11+ export const PLUGIN_PREFIX_REGEX = new RegExp ( `^${ PLUGIN_PREFIX } ` , 'u' ) ;
1212
1313const SERIALIZABLE_PLUGIN_PROPERTIES = new Set ( [
1414 'initialPermissions' ,
@@ -30,10 +30,17 @@ export interface Plugin extends SerializablePlugin {
3030// The plugin is the callee
3131export type PluginRpcHook = ( origin : string , request : Record < string , unknown > ) => Promise < CommandResponse > ;
3232
33+ export type ProcessPluginReturnType = SerializablePlugin | { error : ReturnType < typeof serializeError > } ;
34+ export interface InstalledPlugins {
35+ [ pluginName : string ] : ProcessPluginReturnType ;
36+ }
37+
3338// Types that probably should be defined elsewhere in prod
3439type RemoveAllPermissionsFunction = ( pluginIds : string [ ] ) => void ;
3540type CloseAllConnectionsFunction = ( domain : string ) => void ;
3641type RequestPermissionsFunction = ( domain : string , requestedPermissions : IRequestedPermissions ) => IOcapLdCapability [ ] ;
42+ type HasPermissionFunction = ( domain : string , permissionName : string ) => boolean ;
43+ type GetPermissionsFunction = ( domain : string ) => IOcapLdCapability [ ] ;
3744
3845interface StoredPlugins {
3946 [ pluginId : string ] : Plugin ;
@@ -56,6 +63,8 @@ interface PluginControllerArgs {
5663 setupWorkerPluginProvider : SetupWorkerConnection ;
5764 closeAllConnections : CloseAllConnectionsFunction ;
5865 requestPermissions : RequestPermissionsFunction ;
66+ getPermissions : GetPermissionsFunction ;
67+ hasPermission : HasPermissionFunction ;
5968 workerUrl : URL ;
6069}
6170
@@ -84,6 +93,10 @@ export class PluginController extends EventEmitter {
8493
8594 private _requestPermissions : RequestPermissionsFunction ;
8695
96+ private _getPermissions : GetPermissionsFunction ;
97+
98+ private _hasPermission : HasPermissionFunction ;
99+
87100 private _pluginsBeingAdded : Map < string , Promise < Plugin > > ;
88101
89102 constructor ( {
@@ -92,19 +105,22 @@ export class PluginController extends EventEmitter {
92105 removeAllPermissionsFor,
93106 closeAllConnections,
94107 requestPermissions,
108+ getPermissions,
109+ hasPermission,
95110 workerUrl,
96111 } : PluginControllerArgs ) {
97-
98112 super ( ) ;
99113 const _initState : PluginControllerState = {
100114 plugins : { } ,
101115 pluginStates : { } ,
102116 ...initState ,
103117 } ;
118+
104119 this . store = new ObservableStore ( {
105120 plugins : { } ,
106121 pluginStates : { } ,
107122 } ) ;
123+
108124 this . memStore = new ObservableStore ( {
109125 inlinePluginIsRunning : false ,
110126 plugins : { } ,
@@ -120,6 +136,8 @@ export class PluginController extends EventEmitter {
120136 this . _removeAllPermissionsFor = removeAllPermissionsFor ;
121137 this . _closeAllConnections = closeAllConnections ;
122138 this . _requestPermissions = requestPermissions ;
139+ this . _getPermissions = getPermissions ;
140+ this . _hasPermission = hasPermission ;
123141
124142 this . _pluginRpcHooks = new Map ( ) ;
125143 this . _pluginsBeingAdded = new Map ( ) ;
@@ -195,16 +213,16 @@ export class PluginController extends EventEmitter {
195213 * @param pluginName - The name of the plugin to get.
196214 */
197215 getSerializable ( pluginName : string ) : SerializablePlugin | null {
198-
199216 const plugin = this . get ( pluginName ) ;
200217
201218 return plugin
202- ? Object . keys ( plugin ) . reduce ( ( acc , key ) => {
219+ // The cast to "any" of the accumulator object is due to a TypeScript bug
220+ ? Object . keys ( plugin ) . reduce ( ( serialized , key ) => {
203221 if ( SERIALIZABLE_PLUGIN_PROPERTIES . has ( key as keyof Plugin ) ) {
204- acc [ key ] = plugin [ key as keyof SerializablePlugin ] ;
222+ serialized [ key ] = plugin [ key as keyof SerializablePlugin ] ;
205223 }
206224
207- return acc ;
225+ return serialized ;
208226 } , { } as any ) as SerializablePlugin
209227 : null ;
210228 }
@@ -221,7 +239,6 @@ export class PluginController extends EventEmitter {
221239 newPluginState : unknown ,
222240 ) : Promise < void > {
223241 const state = this . store . getState ( ) ;
224-
225242 const newPluginStates = { ...state . pluginStates , [ pluginName ] : newPluginState } ;
226243
227244 this . updateState ( {
@@ -274,7 +291,6 @@ export class PluginController extends EventEmitter {
274291 * @param {Array<string> } pluginName - The name of the plugins.
275292 */
276293 removePlugins ( pluginNames : string [ ] ) : void {
277-
278294 if ( ! Array . isArray ( pluginNames ) ) {
279295 throw new Error ( 'Expected Array of plugin names.' ) ;
280296 }
@@ -301,17 +317,57 @@ export class PluginController extends EventEmitter {
301317 } ) ;
302318 }
303319
320+ getPermittedPlugins ( origin : string ) : InstalledPlugins {
321+ return this . _getPermissions ( origin ) . reduce (
322+ ( permittedPlugins , perm ) => {
323+ if ( perm . parentCapability . startsWith ( PLUGIN_PREFIX ) ) {
324+
325+ const pluginName = perm . parentCapability . replace ( PLUGIN_PREFIX_REGEX , '' ) ;
326+ const plugin = this . getSerializable ( pluginName ) ;
327+
328+ permittedPlugins [ pluginName ] = plugin || {
329+ error : serializeError ( new Error ( 'Plugin permitted but not installed.' ) ) ,
330+ } ;
331+ }
332+ return permittedPlugins ;
333+ } ,
334+ { } as InstalledPlugins ,
335+ ) ;
336+ }
337+
338+ async installPlugins ( origin : string , requestedPlugins : IRequestedPermissions ) : Promise < InstalledPlugins > {
339+ const result : InstalledPlugins = { } ;
340+
341+ // use a for-loop so that we can return an object and await the resolution
342+ // of each call to processRequestedPlugin
343+ await Promise . all ( Object . keys ( requestedPlugins ) . map ( async ( pluginName ) => {
344+ const permissionName = PLUGIN_PREFIX + pluginName ;
345+
346+ if ( this . _hasPermission ( origin , permissionName ) ) {
347+ // attempt to install and run the plugin, storing any errors that
348+ // occur during the process
349+ result [ pluginName ] = {
350+ ...( await this . processRequestedPlugin ( pluginName ) ) ,
351+ } ;
352+ } else {
353+ // only allow the installation of permitted plugins
354+ result [ pluginName ] = {
355+ error : ethErrors . provider . unauthorized (
356+ `Not authorized to install plugin '${ pluginName } '. Request the permission for the plugin before attempting to install it.` ,
357+ ) ,
358+ } ;
359+ }
360+ } ) ) ;
361+ return result ;
362+ }
363+
304364 /**
305365 * Adds, authorizes, and runs the given plugin with a plugin provider.
306366 * Results from this method should be efficiently serializable.
307367 *
308368 * @param - pluginName - The name of the plugin.
309369 */
310- async processRequestedPlugin ( pluginName : string ) : Promise <
311- SerializablePlugin |
312- { error : ReturnType < typeof serializeError > }
313- > {
314-
370+ async processRequestedPlugin ( pluginName : string ) : Promise < ProcessPluginReturnType > {
315371 // if the plugin is already installed and active, just return it
316372 const plugin = this . get ( pluginName ) ;
317373 if ( plugin ?. isActive ) {
@@ -326,7 +382,6 @@ export class PluginController extends EventEmitter {
326382 await this . _startPluginInWorker ( pluginName , sourceCode ) ;
327383
328384 return this . getSerializable ( pluginName ) as SerializablePlugin ;
329-
330385 } catch ( err ) {
331386 console . warn ( `Error when adding plugin:` , err ) ;
332387 return { error : serializeError ( err ) } ;
@@ -358,7 +413,6 @@ export class PluginController extends EventEmitter {
358413 * @param [sourceUrl] - The URL of the source code.
359414 */
360415 async _add ( pluginName : string , sourceUrl ?: string ) : Promise < Plugin > {
361-
362416 const _sourceUrl = sourceUrl || pluginName ;
363417
364418 if ( ! pluginName || typeof pluginName !== 'string' ) {
@@ -511,9 +565,3 @@ export class PluginController extends EventEmitter {
511565 } ) ;
512566 }
513567}
514-
515- // const createGetDomainMetadataFunction = (pluginName) => {
516- // return async () => {
517- // return { name: pluginName }
518- // }
519- // }
0 commit comments