/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim: set sts=2 sw=2 et tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /** * This file handles addon logic that is independent of the chrome process and * may run in all web content and extension processes. * * Don't put contentscript logic here, use ExtensionContent.jsm instead. */ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; const lazy = {}; XPCOMUtils.defineLazyServiceGetter( lazy, "finalizationService", "@mozilla.org/toolkit/finalizationwitness;1", "nsIFinalizationWitnessService" ); ChromeUtils.defineESModuleGetters(lazy, { ExtensionContent: "resource://gre/modules/ExtensionContent.sys.mjs", ExtensionPageChild: "resource://gre/modules/ExtensionPageChild.sys.mjs", ExtensionProcessScript: "resource://gre/modules/ExtensionProcessScript.sys.mjs", NativeApp: "resource://gre/modules/NativeMessaging.sys.mjs", }); import { ExtensionCommon } from "resource://gre/modules/ExtensionCommon.sys.mjs"; import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs"; const { DefaultMap, ExtensionError, LimitedSet, getUniqueId } = ExtensionUtils; const { redefineGetter, EventEmitter, EventManager, LocalAPIImplementation, LocaleData, NoCloneSpreadArgs, SchemaAPIInterface, withHandlingUserInput, } = ExtensionCommon; const { sharedData } = Services.cpmm; const MSG_SET_ENABLED = "Extension:ActivityLog:SetEnabled"; const MSG_LOG = "Extension:ActivityLog:DoLog"; export const ExtensionActivityLogChild = { _initialized: false, enabledExtensions: new Set(), init() { if (this._initialized) { return; } this._initialized = true; Services.cpmm.addMessageListener(MSG_SET_ENABLED, this); this.enabledExtensions = new Set( Services.cpmm.sharedData.get("extensions/logging") ); }, receiveMessage({ name, data }) { if (name === MSG_SET_ENABLED) { if (data.value) { this.enabledExtensions.add(data.id); } else { this.enabledExtensions.delete(data.id); } } }, async log(context, type, name, data) { this.init(); let { id } = context.extension; if (this.enabledExtensions.has(id)) { this._sendActivity({ timeStamp: Date.now(), id, viewType: context.viewType, type, name, data, browsingContextId: context.browsingContextId, }); } }, _sendActivity(data) { Services.cpmm.sendAsyncMessage(MSG_LOG, data); }, }; // A helper to allow us to distinguish trusted errors from unsanitized errors. // Extensions can create plain objects with arbitrary properties (such as // mozWebExtLocation), but not create instances of ExtensionErrorHolder. class ExtensionErrorHolder { constructor(trustedErrorObject) { this.trustedErrorObject = trustedErrorObject; } } /** * A finalization witness helper that wraps a sendMessage response and * guarantees to either get the promise resolved, or rejected when the * wrapped promise goes out of scope. */ const StrongPromise = { stillAlive: new Map(), wrap(promise, location) { let id = String(getUniqueId()); let witness = lazy.finalizationService.make( "extensions-onMessage-witness", id ); return new Promise((resolve, reject) => { this.stillAlive.set(id, { reject, location }); promise.then(resolve, reject).finally(() => { this.stillAlive.delete(id); witness.forget(); }); }); }, observe(subject, topic, id) { let message = "Promised response from onMessage listener went out of scope"; let { reject, location } = this.stillAlive.get(id); reject(new ExtensionErrorHolder({ message, mozWebExtLocation: location })); this.stillAlive.delete(id); }, }; Services.obs.addObserver(StrongPromise, "extensions-onMessage-witness"); // Simple single-event emitter-like helper, exposes the EventManager api. class SimpleEventAPI extends EventManager { constructor(context, name) { let fires = new Set(); let register = fire => { fires.add(fire); fire.location = context.getCaller(); return () => fires.delete(fire); }; super({ context, name, register }); this.fires = fires; } /** @returns {any} */ emit(...args) { return [...this.fires].map(fire => fire.asyncWithoutClone(...args)); } } // runtime.OnMessage event helper, handles custom async/sendResponse logic. class MessageEvent extends SimpleEventAPI { emit(holder, sender) { if (!this.fires.size || !this.context.active) { return { received: false }; } sender = Cu.cloneInto(sender, this.context.cloneScope); let message = holder.deserialize(this.context.cloneScope); let responses = [...this.fires] .map(fire => this.wrapResponse(fire, message, sender)) .filter(x => x !== undefined); return !responses.length ? { received: true, response: false } : Promise.race(responses).then( value => ({ response: true, value }), error => Promise.reject(this.unwrapOrSanitizeError(error)) ); } unwrapOrSanitizeError(error) { if (error instanceof ExtensionErrorHolder) { return error.trustedErrorObject; } // If not a wrapped error, sanitize it and convert to ExtensionError, so // that context.normalizeError will use the error message. return new ExtensionError(error?.message ?? "An unexpected error occurred"); } wrapResponse(fire, message, sender) { let response, sendResponse; let promise = new Promise(resolve => { sendResponse = Cu.exportFunction(value => { resolve(value); response = promise; }, this.context.cloneScope); }); let result; try { result = fire.raw(message, sender, sendResponse); } catch (e) { return Promise.reject(e); } if ( result && typeof result === "object" && Cu.getClassName(result, true) === "Promise" && this.context.principal.subsumes(Cu.getObjectPrincipal(result)) ) { return StrongPromise.wrap(result, fire.location); } else if (result === true) { return StrongPromise.wrap(promise, fire.location); } return response; } } function holdMessage(name, anonymizedName, data, native = null) { if (native && AppConstants.platform !== "android") { data = lazy.NativeApp.encodeMessage(native.context, data); } return new StructuredCloneHolder(name, anonymizedName, data); } // Implements the runtime.Port extension API object. class Port { /** * @param {BaseContext} context The context that owns this port. * @param {number} portId Uniquely identifies this port's channel. * @param {string} name Arbitrary port name as defined by the addon. * @param {boolean} native Is this a Port for native messaging. * @param {object} sender The `Port.sender` property. */ constructor(context, portId, name, native, sender) { this.context = context; this.name = name; this.sender = sender; this.holdMessage = native ? (name, anonymizedName, data) => holdMessage(name, anonymizedName, data, this) : holdMessage; this.conduit = context.openConduit(this, { portId, native, source: !sender, recv: ["PortMessage", "PortDisconnect"], send: ["PortMessage"], }); this.initEventManagers(); } initEventManagers() { const { context } = this; this.onMessage = new SimpleEventAPI(context, "Port.onMessage"); this.onDisconnect = new SimpleEventAPI(context, "Port.onDisconnect"); } getAPI() { // Public Port object handed to extensions from `connect()` and `onConnect`. return { name: this.name, sender: this.sender, error: null, onMessage: this.onMessage.api(), onDisconnect: this.onDisconnect.api(), postMessage: this.sendPortMessage.bind(this), disconnect: () => this.conduit.close(), }; } recvPortMessage({ holder }) { this.onMessage.emit(holder.deserialize(this.api), this.api); } recvPortDisconnect({ error = null }) { this.conduit.close(); if (this.context.active) { this.api.error = error && this.context.normalizeError(error); this.onDisconnect.emit(this.api); } } sendPortMessage(json) { if (this.conduit.actor) { return this.conduit.sendPortMessage({ holder: this.holdMessage( `Port/${this.context.extension.id}/sendPortMessage/${this.name}`, `Port/${this.context.extension.id}/sendPortMessage/`, json ), }); } throw new this.context.Error("Attempt to postMessage on disconnected port"); } get api() { const scope = this.context.cloneScope; const value = Cu.cloneInto(this.getAPI(), scope, { cloneFunctions: true }); return redefineGetter(this, "api", value); } } /** * Each extension context gets its own Messenger object. It handles the * basics of sendMessage, onMessage, connect and onConnect. */ class Messenger { constructor(context) { this.context = context; this.conduit = context.openConduit(this, { childId: context.childManager.id, query: ["NativeMessage", "RuntimeMessage", "PortConnect"], recv: ["RuntimeMessage", "PortConnect"], }); this.initEventManagers(); } initEventManagers() { const { context } = this; this.onConnect = new SimpleEventAPI(context, "runtime.onConnect"); this.onConnectEx = new SimpleEventAPI(context, "runtime.onConnectExternal"); this.onMessage = new MessageEvent(context, "runtime.onMessage"); this.onMessageEx = new MessageEvent(context, "runtime.onMessageExternal"); } sendNativeMessage(nativeApp, json) { let holder = holdMessage( `Messenger/${this.context.extension.id}/sendNativeMessage/${nativeApp}`, null, json, this ); return this.conduit.queryNativeMessage({ nativeApp, holder }); } sendRuntimeMessage({ extensionId, message, callback, ...args }) { let response = this.conduit.queryRuntimeMessage({ extensionId: extensionId || this.context.extension.id, holder: holdMessage( `Messenger/${this.context.extension.id}/sendRuntimeMessage`, null, message ), ...args, }); // If |response| is a rejected promise, the value will be sanitized by // wrapPromise, according to the rules of context.normalizeError. return this.context.wrapPromise(response, callback); } connect({ name, native, ...args }) { let portId = getUniqueId(); let port = new Port(this.context, portId, name, !!native); this.conduit .queryPortConnect({ portId, name, native, ...args }) .catch(error => port.recvPortDisconnect({ error })); return port.api; } recvPortConnect({ extensionId, portId, name, sender }) { let event = sender.id === extensionId ? this.onConnect : this.onConnectEx; if (this.context.active && event.fires.size) { let port = new Port(this.context, portId, name, false, sender); return event.emit(port.api).length; } } recvRuntimeMessage({ extensionId, holder, sender }) { let event = sender.id === extensionId ? this.onMessage : this.onMessageEx; return event.emit(holder, sender); } } // For test use only. var ExtensionManager = { extensions: new Map(), }; // Represents a browser extension in the content process. class BrowserExtensionContent extends EventEmitter { constructor(policy) { super(); this.policy = policy; // Set a weak reference to this instance on the WebExtensionPolicy expando properties // (because it makes it easier to reach the extension instance from the policy object // without leaking it due to a circular dependency keeping it alive). this.policy.weakExtension = Cu.getWeakReference(this); this.instanceId = policy.instanceId; this.optionalPermissions = policy.optionalPermissions; if (WebExtensionPolicy.isExtensionProcess) { // Keep in sync with serializeExtended in Extension.jsm let ed = this.getSharedData("extendedData"); this.backgroundScripts = ed.backgroundScripts; this.backgroundWorkerScript = ed.backgroundWorkerScript; this.childModules = ed.childModules; this.dependencies = ed.dependencies; this.persistentBackground = ed.persistentBackground; this.schemaURLs = ed.schemaURLs; } this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`; Services.cpmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this); this.apiManager = this.getAPIManager(); this._manifest = null; this._localeData = null; this.baseURI = Services.io.newURI(`moz-extension://${this.uuid}/`); this.baseURL = this.baseURI.spec; this.principal = Services.scriptSecurityManager.createContentPrincipal( this.baseURI, {} ); // Only used in addon processes. this.blockedParsingDocuments = new WeakSet(); this.views = new Set(); // Only used for devtools views. this.devtoolsViews = new Set(); ExtensionManager.extensions.set(this.id, this); } get id() { return this.policy.id; } get uuid() { return this.policy.mozExtensionHostname; } get permissions() { return new Set(this.policy.permissions); } get allowedOrigins() { return this.policy.allowedOrigins; } getSharedData(key, value) { return sharedData.get(`extension/${this.id}/${key}`); } get localeData() { if (!this._localeData) { this._localeData = new LocaleData(this.getSharedData("locales")); } return this._localeData; } get manifest() { if (!this._manifest) { this._manifest = this.getSharedData("manifest"); } return this._manifest; } get manifestVersion() { return this.manifest.manifest_version; } get privateBrowsingAllowed() { return this.policy.privateBrowsingAllowed; } canAccessWindow(window) { return this.policy.canAccessWindow(window); } getAPIManager() { /** @type {InstanceType[]} */ let apiManagers = [lazy.ExtensionPageChild.apiManager]; if (this.dependencies) { for (let id of this.dependencies) { let extension = lazy.ExtensionProcessScript.getExtensionChild(id); if (extension) { apiManagers.push(extension.experimentAPIManager); } } } if (this.childModules) { this.experimentAPIManager = new ExtensionCommon.LazyAPIManager( "addon", this.childModules, this.schemaURLs ); apiManagers.push(this.experimentAPIManager); } if (apiManagers.length == 1) { return apiManagers[0]; } return new ExtensionCommon.MultiAPIManager("addon", apiManagers.reverse()); } shutdown() { ExtensionManager.extensions.delete(this.id); lazy.ExtensionContent.shutdownExtension(this); Services.cpmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this); this.emit("shutdown"); } getContext(window) { return lazy.ExtensionContent.getContext(this, window); } emit(event, ...args) { Services.cpmm.sendAsyncMessage(this.MESSAGE_EMIT_EVENT, { event, args }); return super.emit(event, ...args); } // TODO(Bug 1768471): consider folding this back into emit if we will change it to // return a value as EventEmitter and Extension emit methods do. emitLocalWithResult(event, ...args) { return super.emit(event, ...args); } receiveMessage({ name, data }) { if (name === this.MESSAGE_EMIT_EVENT) { super.emit(data.event, ...data.args); } } localizeMessage(...args) { return this.localeData.localizeMessage(...args); } localize(...args) { return this.localeData.localize(...args); } hasPermission(perm) { // If the permission is a "manifest property" permission, we check if the extension // does have the required property in its manifest. let manifest_ = "manifest:"; if (perm.startsWith(manifest_)) { // Handle nested "manifest property" permission (e.g. as in "manifest:property.nested"). let value = this.manifest; for (let prop of perm.substr(manifest_.length).split(".")) { if (!value) { break; } value = value[prop]; } return value != null; } return this.permissions.has(perm); } trackBlockedParsingDocument(doc) { this.blockedParsingDocuments.add(doc); } untrackBlockedParsingDocument(doc) { this.blockedParsingDocuments.delete(doc); } hasContextBlockedParsingDocument(extContext) { return this.blockedParsingDocuments.has(extContext.contentWindow?.document); } } /** * An object that runs an remote implementation of an API. */ class ProxyAPIImplementation extends SchemaAPIInterface { /** * @param {string} namespace The full path to the namespace that contains the * `name` member. This may contain dots, e.g. "storage.local". * @param {string} name The name of the method or property. * @param {ChildAPIManager} childApiManager The owner of this implementation. * @param {boolean} alreadyLogged Whether the child already logged the event. */ constructor(namespace, name, childApiManager, alreadyLogged = false) { super(); this.path = `${namespace}.${name}`; this.childApiManager = childApiManager; this.alreadyLogged = alreadyLogged; } revoke() { let map = this.childApiManager.listeners.get(this.path); for (let listener of map.listeners.keys()) { this.removeListener(listener); } this.path = null; this.childApiManager = null; } callFunctionNoReturn(args) { this.childApiManager.callParentFunctionNoReturn(this.path, args); } callAsyncFunction(args, callback, requireUserInput) { const context = this.childApiManager.context; const isHandlingUserInput = context.contentWindow?.windowUtils?.isHandlingUserInput; if (requireUserInput) { if (!isHandlingUserInput) { let err = new context.cloneScope.Error( `${this.path} may only be called from a user input handler` ); return context.wrapPromise(Promise.reject(err), callback); } } return this.childApiManager.callParentAsyncFunction( this.path, args, callback, { alreadyLogged: this.alreadyLogged, isHandlingUserInput, } ); } addListener(listener, args) { let map = this.childApiManager.listeners.get(this.path); if (map.listeners.has(listener)) { // TODO: Called with different args? return; } let id = getUniqueId(); map.ids.set(id, listener); map.listeners.set(listener, id); this.childApiManager.conduit.sendAddListener({ childId: this.childApiManager.id, listenerId: id, path: this.path, args, alreadyLogged: this.alreadyLogged, }); } removeListener(listener) { let map = this.childApiManager.listeners.get(this.path); if (!map.listeners.has(listener)) { return; } let id = map.listeners.get(listener); map.listeners.delete(listener); map.ids.delete(id); map.removedIds.add(id); this.childApiManager.conduit.sendRemoveListener({ childId: this.childApiManager.id, listenerId: id, path: this.path, alreadyLogged: this.alreadyLogged, }); } hasListener(listener) { let map = this.childApiManager.listeners.get(this.path); return map.listeners.has(listener); } } class ChildLocalAPIImplementation extends LocalAPIImplementation { constructor(pathObj, namespace, name, childApiManager) { super(pathObj, name, childApiManager.context); this.childApiManagerId = childApiManager.id; this.fullname = `${namespace}.${name}`; } /** * Call the given function and also log the call as appropriate * (i.e., with activity logging and/or profiler markers) * * @param {Function} callable The actual implementation to invoke. * @param {Array} args Arguments to the function call. * @returns {any} The return result of callable. */ callAndLog(callable, args) { this.context.logActivity("api_call", this.fullname, { args }); let start = Cu.now(); try { return callable(); } finally { ChromeUtils.addProfilerMarker( "ExtensionChild", { startTime: start }, `${this.context.extension.id}, api_call: ${this.fullname}` ); } } callFunction(args) { return this.callAndLog(() => super.callFunction(args), args); } callFunctionNoReturn(args) { return this.callAndLog(() => super.callFunctionNoReturn(args), args); } callAsyncFunction(args, callback, requireUserInput) { return this.callAndLog( () => super.callAsyncFunction(args, callback, requireUserInput), args ); } } // We create one instance of this class for every extension context that // needs to use remote APIs. It uses the the JSWindowActor and // JSProcessActor Conduits actors (see ConduitsChild.jsm) to communicate // with the ParentAPIManager singleton in ExtensionParent.jsm. // It handles asynchronous function calls as well as event listeners. class ChildAPIManager { constructor(context, messageManager, localAPICan, contextData) { this.context = context; this.messageManager = messageManager; this.url = contextData.url; // The root namespace of all locally implemented APIs. If an extension calls // an API that does not exist in this object, then the implementation is // delegated to the ParentAPIManager. this.localApis = localAPICan.root; this.apiCan = localAPICan; this.schema = this.apiCan.apiManager.schema; this.id = `${context.extension.id}.${context.contextId}`; this.conduit = context.openConduit(this, { childId: this.id, send: [ "CreateProxyContext", "ContextLoaded", "APICall", "AddListener", "RemoveListener", ], recv: ["CallResult", "RunListener", "StreamFilterSuspendCancel"], }); this.conduit.sendCreateProxyContext({ childId: this.id, extensionId: context.extension.id, principal: context.principal, ...contextData, }); this.listeners = new DefaultMap(() => ({ ids: new Map(), listeners: new Map(), removedIds: new LimitedSet(10), })); // Map[callId -> Deferred] this.callPromises = new Map(); this.permissionsChangedCallbacks = new Set(); this.updatePermissions = null; if (this.context.extension.optionalPermissions.length) { this.updatePermissions = () => { for (let callback of this.permissionsChangedCallbacks) { try { callback(); } catch (err) { Cu.reportError(err); } } }; this.context.extension.on("update-permissions", this.updatePermissions); } } inject(obj) { this.schema.inject(obj, this); } recvCallResult(data) { let deferred = this.callPromises.get(data.callId); this.callPromises.delete(data.callId); if ("error" in data) { deferred.reject(data.error); } else { let result = data.result.deserialize(this.context.cloneScope); deferred.resolve(new NoCloneSpreadArgs(result)); } } recvRunListener(data) { let map = this.listeners.get(data.path); let listener = map.ids.get(data.listenerId); if (listener) { if (!this.context.active) { Services.console.logStringMessage( `Ignored listener for inactive context at childId=${data.childId} path=${data.path} listenerId=${data.listenerId}\n` ); return; } let args = data.args.deserialize(this.context.cloneScope); let fire = () => this.context.applySafeWithoutClone(listener, args); return Promise.resolve( data.handlingUserInput ? withHandlingUserInput(this.context.contentWindow, fire) : fire() ).then(result => { if (result !== undefined) { return new StructuredCloneHolder( `ChildAPIManager/${this.context.extension.id}/${data.path}`, null, result, this.context.cloneScope ); } return result; }); } if (!map.removedIds.has(data.listenerId)) { Services.console.logStringMessage( `Unknown listener at childId=${data.childId} path=${data.path} listenerId=${data.listenerId}\n` ); } } async recvStreamFilterSuspendCancel() { const promise = this.context.extension.emitLocalWithResult( "internal:stream-filter-suspend-cancel" ); // if all listeners throws emitLocalWithResult returns undefined. if (!promise) { return false; } return promise.then(results => results.some(hasActiveStreamFilter => hasActiveStreamFilter === true) ); } /** * Call a function in the parent process and ignores its return value. * * @param {string} path The full name of the method, e.g. "tabs.create". * @param {Array} args The parameters for the function. */ callParentFunctionNoReturn(path, args) { this.conduit.sendAPICall({ childId: this.id, path, args }); } /** * Calls a function in the parent process and returns its result * asynchronously. * * @param {string} path The full name of the method, e.g. "tabs.create". * @param {Array} args The parameters for the function. * @param {callback} [callback] The callback to be called when the * function completes. * @param {object} [options] Extra options. * @returns {Promise|undefined} Must be void if `callback` is set, and a * promise otherwise. The promise is resolved when the function completes. */ callParentAsyncFunction(path, args, callback, options = {}) { let callId = getUniqueId(); let deferred = Promise.withResolvers(); this.callPromises.set(callId, deferred); let { // Any child api that calls into a parent function will have already // logged the api_call. Flag it so the parent doesn't log again. alreadyLogged = true, // Propagating the isHAndlingUserInput flag to the API call handler // executed on the parent process side. isHandlingUserInput = false, } = options; // TODO: conduit.queryAPICall() this.conduit.sendAPICall({ childId: this.id, callId, path, args, options: { alreadyLogged, isHandlingUserInput }, }); return this.context.wrapPromise(deferred.promise, callback); } /** * Create a proxy for an event in the parent process. The returned event * object shares its internal state with other instances. For instance, if * `removeListener` is used on a listener that was added on another object * through `addListener`, then the event is unregistered. * * @param {string} path The full name of the event, e.g. "tabs.onCreated". * @returns {object} An object with the addListener, removeListener and * hasListener methods. See SchemaAPIInterface for documentation. */ getParentEvent(path) { let parts = path.split("."); let name = parts.pop(); let namespace = parts.join("."); let impl = new ProxyAPIImplementation(namespace, name, this, true); return { addListener: (listener, ...args) => impl.addListener(listener, args), removeListener: listener => impl.removeListener(listener), hasListener: listener => impl.hasListener(listener), }; } close() { // Reports CONDUIT_CLOSED on the parent side. this.conduit.close(); if (this.updatePermissions) { this.context.extension.off("update-permissions", this.updatePermissions); } } get cloneScope() { return this.context.cloneScope; } get principal() { return this.context.principal; } get manifestVersion() { return this.context.manifestVersion; } shouldInject(namespace, name, allowedContexts) { // Do not generate content script APIs, unless explicitly allowed. if ( this.context.envType === "content_child" && !allowedContexts.includes("content") ) { return false; } // Do not generate devtools APIs, unless explicitly allowed. if ( this.context.envType === "devtools_child" && !allowedContexts.includes("devtools") ) { return false; } // Do not generate devtools APIs, unless explicitly allowed. if ( this.context.envType !== "devtools_child" && allowedContexts.includes("devtools_only") ) { return false; } // Do not generate content_only APIs, unless explicitly allowed. if ( this.context.envType !== "content_child" && allowedContexts.includes("content_only") ) { return false; } return true; } getImplementation(namespace, name) { this.apiCan.findAPIPath(`${namespace}.${name}`); let obj = this.apiCan.findAPIPath(namespace); if (obj && name in obj) { return new ChildLocalAPIImplementation(obj, namespace, name, this); } return this.getFallbackImplementation(namespace, name); } getFallbackImplementation(namespace, name) { // No local API found, defer implementation to the parent. return new ProxyAPIImplementation(namespace, name, this); } hasPermission(permission) { return this.context.extension.hasPermission(permission); } isPermissionRevokable(permission) { return this.context.extension.optionalPermissions.includes(permission); } setPermissionsChangedCallback(callback) { this.permissionsChangedCallbacks.add(callback); } } export var ExtensionChild = { BrowserExtensionContent, ChildAPIManager, ChildLocalAPIImplementation, MessageEvent, Messenger, Port, ProxyAPIImplementation, SimpleEventAPI, };