// Copyright 2011 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable @devtools/no-imperative-dom-api */

import type {Chrome} from '../../../extension-api/ExtensionAPI.js';
import * as Common from '../../core/common/common.js';
import * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as _ProtocolClient from '../../core/protocol_client/protocol_client.js';  // eslint-disable-line @typescript-eslint/no-unused-vars
import * as SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';
import * as Bindings from '../../models/bindings/bindings.js';
import * as Extensions from '../../models/extensions/extensions.js';
import * as HAR from '../../models/har/har.js';
import * as Logs from '../../models/logs/logs.js';
import * as TextUtils from '../../models/text_utils/text_utils.js';
import * as Workspace from '../../models/workspace/workspace.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as ThemeSupport from '../../ui/legacy/theme_support/theme_support.js';

import {ExtensionButton, ExtensionPanel, ExtensionSidebarPane} from './ExtensionPanel.js';

const extensionOrigins = new WeakMap<MessagePort, Platform.DevToolsPath.UrlString>();
const kPermittedSchemes = ['http:', 'https:', 'file:', 'data:', 'chrome-extension:', 'about:'];

declare global {
  interface Window {
    DevToolsAPI?: {getInspectedTabId?(): string|undefined, getOriginsForbiddenForExtensions?(): string[]};
  }
}

let extensionServerInstance: ExtensionServer|null;

export class HostsPolicy {
  static create(policy?: Host.InspectorFrontendHostAPI.ExtensionHostsPolicy): HostsPolicy|null {
    const runtimeAllowedHosts = [];
    const runtimeBlockedHosts = [];
    if (policy) {
      for (const pattern of policy.runtimeAllowedHosts) {
        const parsedPattern = Extensions.HostUrlPattern.HostUrlPattern.parse(pattern);
        if (!parsedPattern) {
          return null;
        }
        runtimeAllowedHosts.push(parsedPattern);
      }
      for (const pattern of policy.runtimeBlockedHosts) {
        const parsedPattern = Extensions.HostUrlPattern.HostUrlPattern.parse(pattern);
        if (!parsedPattern) {
          return null;
        }
        runtimeBlockedHosts.push(parsedPattern);
      }
    }
    return new HostsPolicy(runtimeAllowedHosts, runtimeBlockedHosts);
  }
  private constructor(
      readonly runtimeAllowedHosts: Extensions.HostUrlPattern.HostUrlPattern[],
      readonly runtimeBlockedHosts: Extensions.HostUrlPattern.HostUrlPattern[]) {
  }

  isAllowedOnURL(inspectedURL?: Platform.DevToolsPath.UrlString): boolean {
    if (!inspectedURL) {
      // If there aren't any blocked hosts retain the old behavior and don't worry about the inspectedURL
      return this.runtimeBlockedHosts.length === 0;
    }
    if (this.runtimeBlockedHosts.some(pattern => pattern.matchesUrl(inspectedURL)) &&
        !this.runtimeAllowedHosts.some(pattern => pattern.matchesUrl(inspectedURL))) {
      return false;
    }
    return true;
  }
}

class RegisteredExtension {
  openResourceScheme: null|string = null;
  constructor(readonly name: string, readonly hostsPolicy: HostsPolicy, readonly allowFileAccess: boolean) {
  }

  isAllowedOnTarget(inspectedURL?: Platform.DevToolsPath.UrlString): boolean {
    if (!inspectedURL) {
      inspectedURL = SDK.TargetManager.TargetManager.instance().primaryPageTarget()?.inspectedURL();
    }

    if (!inspectedURL) {
      return false;
    }

    if (this.openResourceScheme && inspectedURL.startsWith(this.openResourceScheme)) {
      return true;
    }

    if (!ExtensionServer.canInspectURL(inspectedURL)) {
      return false;
    }

    if (!this.hostsPolicy.isAllowedOnURL(inspectedURL)) {
      return false;
    }

    if (!this.allowFileAccess) {
      let parsedURL;
      try {
        parsedURL = new URL(inspectedURL);
      } catch {
        return false;
      }
      return parsedURL.protocol !== 'file:';
    }

    return true;
  }
}

export class RevealableNetworkRequestFilter {
  constructor(readonly filter: string|undefined) {
  }
}

export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
  private readonly clientObjects: Map<string, unknown>;
  private readonly handlers:
      Map<string,
          (message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort) => unknown>;
  private readonly subscribers: Map<string, Set<MessagePort>>;
  private readonly subscriptionStartHandlers: Map<string, () => unknown>;
  private readonly subscriptionStopHandlers: Map<string, () => unknown>;
  private readonly extraHeaders: Map<string, Map<string, unknown>>;
  private requests: Map<number, TextUtils.ContentProvider.ContentProvider>;
  private readonly requestIds: Map<TextUtils.ContentProvider.ContentProvider, number>;
  private lastRequestId: number;
  private registeredExtensions: Map<string, RegisteredExtension>;
  private status: ExtensionStatus;
  readonly #sidebarPanes: ExtensionSidebarPane[];
  private extensionsEnabled: boolean;
  private inspectedTabId?: string;
  private readonly extensionAPITestHook?: (server: unknown, api: unknown) => unknown;
  private themeChangeHandlers = new Map<string, MessagePort>();
  readonly #pendingExtensions: Host.InspectorFrontendHostAPI.ExtensionDescriptor[] = [];

  private constructor() {
    super();
    this.clientObjects = new Map();
    this.handlers = new Map();
    this.subscribers = new Map();
    this.subscriptionStartHandlers = new Map();
    this.subscriptionStopHandlers = new Map();
    this.extraHeaders = new Map();
    this.requests = new Map();
    this.requestIds = new Map();
    this.lastRequestId = 0;
    this.registeredExtensions = new Map();
    this.status = new ExtensionStatus();
    this.#sidebarPanes = [];
    // TODO(caseq): properly unload extensions when we disable them.
    this.extensionsEnabled = true;

    this.registerHandler(
        Extensions.ExtensionAPI.PrivateAPI.Commands.AddRequestHeaders, this.onAddRequestHeaders.bind(this));
    this.registerHandler(Extensions.ExtensionAPI.PrivateAPI.Commands.CreatePanel, this.onCreatePanel.bind(this));
    this.registerHandler(
        Extensions.ExtensionAPI.PrivateAPI.Commands.CreateSidebarPane, this.onCreateSidebarPane.bind(this));
    this.registerHandler(
        Extensions.ExtensionAPI.PrivateAPI.Commands.CreateToolbarButton, this.onCreateToolbarButton.bind(this));
    this.registerHandler(
        Extensions.ExtensionAPI.PrivateAPI.Commands.EvaluateOnInspectedPage, this.onEvaluateOnInspectedPage.bind(this));
    this.registerHandler(
        Extensions.ExtensionAPI.PrivateAPI.Commands.ForwardKeyboardEvent, this.onForwardKeyboardEvent.bind(this));
    this.registerHandler(Extensions.ExtensionAPI.PrivateAPI.Commands.GetHAR, this.onGetHAR.bind(this));
    this.registerHandler(
        Extensions.ExtensionAPI.PrivateAPI.Commands.GetPageResources, this.onGetPageResources.bind(this));
    this.registerHandler(
        Extensions.ExtensionAPI.PrivateAPI.Commands.GetRequestContent, this.onGetRequestContent.bind(this));
    this.registerHandler(
        Extensions.ExtensionAPI.PrivateAPI.Commands.GetResourceContent, this.onGetResourceContent.bind(this));
    this.registerHandler(Extensions.ExtensionAPI.PrivateAPI.Commands.Reload, this.onReload.bind(this));
    this.registerHandler(
        Extensions.ExtensionAPI.PrivateAPI.Commands.SetOpenResourceHandler, this.onSetOpenResourceHandler.bind(this));
    this.registerHandler(
        Extensions.ExtensionAPI.PrivateAPI.Commands.SetThemeChangeHandler, this.onSetThemeChangeHandler.bind(this));
    this.registerHandler(
        Extensions.ExtensionAPI.PrivateAPI.Commands.SetResourceContent, this.onSetResourceContent.bind(this));
    this.registerHandler(
        Extensions.ExtensionAPI.PrivateAPI.Commands.AttachSourceMapToResource,
        this.onAttachSourceMapToResource.bind(this));
    this.registerHandler(
        Extensions.ExtensionAPI.PrivateAPI.Commands.SetSidebarHeight, this.onSetSidebarHeight.bind(this));
    this.registerHandler(
        Extensions.ExtensionAPI.PrivateAPI.Commands.SetSidebarContent, this.onSetSidebarContent.bind(this));
    this.registerHandler(Extensions.ExtensionAPI.PrivateAPI.Commands.SetSidebarPage, this.onSetSidebarPage.bind(this));
    this.registerHandler(Extensions.ExtensionAPI.PrivateAPI.Commands.ShowPanel, this.onShowPanel.bind(this));
    this.registerHandler(Extensions.ExtensionAPI.PrivateAPI.Commands.Subscribe, this.onSubscribe.bind(this));
    this.registerHandler(Extensions.ExtensionAPI.PrivateAPI.Commands.OpenResource, this.onOpenResource.bind(this));
    this.registerHandler(Extensions.ExtensionAPI.PrivateAPI.Commands.Unsubscribe, this.onUnsubscribe.bind(this));
    this.registerHandler(Extensions.ExtensionAPI.PrivateAPI.Commands.UpdateButton, this.onUpdateButton.bind(this));
    this.registerHandler(
        Extensions.ExtensionAPI.PrivateAPI.Commands.RegisterLanguageExtensionPlugin,
        this.registerLanguageExtensionEndpoint.bind(this));
    this.registerHandler(
        Extensions.ExtensionAPI.PrivateAPI.Commands.GetWasmLinearMemory, this.onGetWasmLinearMemory.bind(this));
    this.registerHandler(Extensions.ExtensionAPI.PrivateAPI.Commands.GetWasmGlobal, this.onGetWasmGlobal.bind(this));
    this.registerHandler(Extensions.ExtensionAPI.PrivateAPI.Commands.GetWasmLocal, this.onGetWasmLocal.bind(this));
    this.registerHandler(Extensions.ExtensionAPI.PrivateAPI.Commands.GetWasmOp, this.onGetWasmOp.bind(this));
    this.registerHandler(
        Extensions.ExtensionAPI.PrivateAPI.Commands.RegisterRecorderExtensionPlugin,
        this.registerRecorderExtensionEndpoint.bind(this));
    this.registerHandler(
        Extensions.ExtensionAPI.PrivateAPI.Commands.ReportResourceLoad, this.onReportResourceLoad.bind(this));
    this.registerHandler(
        Extensions.ExtensionAPI.PrivateAPI.Commands.SetFunctionRangesForScript,
        this.onSetFunctionRangesForScript.bind(this));
    this.registerHandler(
        Extensions.ExtensionAPI.PrivateAPI.Commands.CreateRecorderView, this.onCreateRecorderView.bind(this));
    this.registerHandler(
        Extensions.ExtensionAPI.PrivateAPI.Commands.ShowRecorderView, this.onShowRecorderView.bind(this));
    this.registerHandler(
        Extensions.ExtensionAPI.PrivateAPI.Commands.ShowNetworkPanel, this.onShowNetworkPanel.bind(this));
    window.addEventListener('message', this.onWindowMessage, false);  // Only for main window.

    const existingTabId = window.DevToolsAPI?.getInspectedTabId?.();

    if (existingTabId) {
      this.setInspectedTabId({data: existingTabId});
    }
    Host.InspectorFrontendHost.InspectorFrontendHostInstance.events.addEventListener(
        Host.InspectorFrontendHostAPI.Events.SetInspectedTabId, this.setInspectedTabId, this);

    this.initExtensions();

    ThemeSupport.ThemeSupport.instance().addEventListener(ThemeSupport.ThemeChangeEvent.eventName, this.#onThemeChange);
  }

  get isEnabledForTest(): boolean {
    return this.extensionsEnabled;
  }

  dispose(): void {
    ThemeSupport.ThemeSupport.instance().removeEventListener(
        ThemeSupport.ThemeChangeEvent.eventName, this.#onThemeChange);

    // Set up by this.initExtensions in the constructor.
    SDK.TargetManager.TargetManager.instance().removeEventListener(
        SDK.TargetManager.Events.INSPECTED_URL_CHANGED, this.inspectedURLChanged, this);

    Host.InspectorFrontendHost.InspectorFrontendHostInstance.events.removeEventListener(
        Host.InspectorFrontendHostAPI.Events.SetInspectedTabId, this.setInspectedTabId, this);

    window.removeEventListener('message', this.onWindowMessage, false);
  }

  #onThemeChange = (): void => {
    const themeName = ThemeSupport.ThemeSupport.instance().themeName();
    for (const port of this.themeChangeHandlers.values()) {
      port.postMessage({command: Extensions.ExtensionAPI.PrivateAPI.Events.ThemeChange, themeName});
    }
  };

  static instance(opts: {
    forceNew: boolean|null,
  } = {forceNew: null}): ExtensionServer {
    const {forceNew} = opts;
    if (!extensionServerInstance || forceNew) {
      extensionServerInstance?.dispose();
      extensionServerInstance = new ExtensionServer();
    }

    return extensionServerInstance;
  }

  initializeExtensions(): void {
    // Defer initialization until DevTools is fully loaded.
    if (this.inspectedTabId !== null) {
      Host.InspectorFrontendHost.InspectorFrontendHostInstance.setAddExtensionCallback(this.addExtension.bind(this));
    }
  }

  hasExtensions(): boolean {
    return Boolean(this.registeredExtensions.size);
  }

  notifySearchAction(panelId: string, action: string, searchString?: string): void {
    this.postNotification(Extensions.ExtensionAPI.PrivateAPI.Events.PanelSearch + panelId, [action, searchString]);
  }

  notifyViewShown(identifier: string, frameIndex?: number): void {
    this.postNotification(Extensions.ExtensionAPI.PrivateAPI.Events.ViewShown + identifier, [frameIndex]);
  }

  notifyViewHidden(identifier: string): void {
    this.postNotification(Extensions.ExtensionAPI.PrivateAPI.Events.ViewHidden + identifier, []);
  }

  notifyButtonClicked(identifier: string): void {
    this.postNotification(Extensions.ExtensionAPI.PrivateAPI.Events.ButtonClicked + identifier, []);
  }

  profilingStarted(): void {
    this.postNotification(Extensions.ExtensionAPI.PrivateAPI.Events.ProfilingStarted, []);
  }

  profilingStopped(): void {
    this.postNotification(Extensions.ExtensionAPI.PrivateAPI.Events.ProfilingStopped, []);
  }

  private registerLanguageExtensionEndpoint(
      message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, _shared_port: MessagePort): Record {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.RegisterLanguageExtensionPlugin) {
      return this.status.E_BADARG(
          'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.RegisterLanguageExtensionPlugin}`);
    }
    const {pluginManager} = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance();
    const {pluginName, port, supportedScriptTypes: {language, symbol_types}} = message;
    const symbol_types_array =
        (Array.isArray(symbol_types) && symbol_types.every(e => typeof e === 'string') ? symbol_types : []);
    const extensionOrigin = this.getExtensionOrigin(_shared_port);
    const registration = this.registeredExtensions.get(extensionOrigin);
    if (!registration) {
      throw new Error('Received a message from an unregistered extension');
    }
    const endpoint = new Extensions.LanguageExtensionEndpoint.LanguageExtensionEndpoint(
        registration.allowFileAccess, extensionOrigin, pluginName, {language, symbol_types: symbol_types_array}, port);
    pluginManager.addPlugin(endpoint);
    return this.status.OK();
  }

  private async loadWasmValue<T>(
      expectValue: boolean, convert: (result: Protocol.Runtime.RemoteObject) => Record | T, expression: string,
      stopId: unknown): Promise<Record|T> {
    const {pluginManager} = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance();
    const callFrame = pluginManager.callFrameForStopId(stopId as Bindings.DebuggerLanguagePlugins.StopId);
    if (!callFrame) {
      return this.status.E_BADARG('stopId', 'Unknown stop id');
    }
    const result = await callFrame.debuggerModel.agent.invoke_evaluateOnCallFrame({
      callFrameId: callFrame.id,
      expression,
      silent: true,
      returnByValue: !expectValue,
      generatePreview: expectValue,
      throwOnSideEffect: true,
    });

    if (!result.exceptionDetails && !result.getError()) {
      return convert(result.result);
    }

    return this.status.E_FAILED('Failed');
  }

  private async onGetWasmLinearMemory(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage):
      Promise<Record|number[]> {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.GetWasmLinearMemory) {
      return this.status.E_BADARG(
          'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.GetWasmLinearMemory}`);
    }
    return await this.loadWasmValue<number[]>(
        false, result => result.value,
        `[].slice.call(new Uint8Array(memories[0].buffer, ${Number(message.offset)}, ${Number(message.length)}))`,
        message.stopId);
  }

  private convertWasmValue(valueClass: 'local'|'global'|'operand', index: number):
      (obj: Protocol.Runtime.RemoteObject) => Chrome.DevTools.WasmValue | undefined | Record {
    return obj => {
      if (obj.type === 'undefined') {
        return;
      }
      if (obj.type !== 'object' || obj.subtype !== 'wasmvalue') {
        return this.status.E_FAILED('Bad object type');
      }
      const type = obj?.description;
      const value: string = obj.preview?.properties?.find(o => o.name === 'value')?.value ?? '';
      switch (type) {
        case 'i32':
        case 'f32':
        case 'f64':
          return {type, value: Number(value)};
        case 'i64':
          return {type, value: BigInt(value.replace(/n$/, ''))};
        case 'v128':
          return {type, value};
        default:
          return {type: 'reftype', valueClass, index};
      }
    };
  }

  private async onGetWasmGlobal(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage):
      Promise<Record|Chrome.DevTools.WasmValue> {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.GetWasmGlobal) {
      return this.status.E_BADARG('command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.GetWasmGlobal}`);
    }
    const global = Number(message.global);
    const result = await this.loadWasmValue<Chrome.DevTools.WasmValue|undefined>(
        true, this.convertWasmValue('global', global), `globals[${global}]`, message.stopId);
    return result ?? this.status.E_BADARG('global', `No global with index ${global}`);
  }

  private async onGetWasmLocal(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage):
      Promise<Record|Chrome.DevTools.WasmValue> {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.GetWasmLocal) {
      return this.status.E_BADARG('command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.GetWasmLocal}`);
    }
    const local = Number(message.local);
    const result = await this.loadWasmValue<Chrome.DevTools.WasmValue|undefined>(
        true, this.convertWasmValue('local', local), `locals[${local}]`, message.stopId);
    return result ?? this.status.E_BADARG('local', `No local with index ${local}`);
  }

  private async onGetWasmOp(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage):
      Promise<Record|Chrome.DevTools.WasmValue> {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.GetWasmOp) {
      return this.status.E_BADARG('command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.GetWasmOp}`);
    }
    const op = Number(message.op);
    const result = await this.loadWasmValue<Chrome.DevTools.WasmValue|undefined>(
        true, this.convertWasmValue('operand', op), `stack[${op}]`, message.stopId);
    return result ?? this.status.E_BADARG('op', `No operand with index ${op}`);
  }

  private registerRecorderExtensionEndpoint(
      message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, _shared_port: MessagePort): Record {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.RegisterRecorderExtensionPlugin) {
      return this.status.E_BADARG(
          'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.RegisterRecorderExtensionPlugin}`);
    }
    const {pluginName, mediaType, port, capabilities} = message;
    Extensions.RecorderPluginManager.RecorderPluginManager.instance().addPlugin(
        new Extensions.RecorderExtensionEndpoint.RecorderExtensionEndpoint(pluginName, port, capabilities, mediaType));
    return this.status.OK();
  }

  private onReportResourceLoad(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage): Record {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.ReportResourceLoad) {
      return this.status.E_BADARG(
          'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.ReportResourceLoad}`);
    }
    const {resourceUrl, extensionId, status} = message;
    const url = resourceUrl as Platform.DevToolsPath.UrlString;
    const initiator: SDK.PageResourceLoader.ExtensionInitiator =
        {target: null, frameId: null, initiatorUrl: extensionId as Platform.DevToolsPath.UrlString, extensionId};

    const pageResource: SDK.PageResourceLoader.PageResource = {
      url,
      initiator,
      errorMessage: status.errorMessage,
      success: status.success ?? null,
      size: status.size ?? null,
      duration: null,
    };
    SDK.PageResourceLoader.PageResourceLoader.instance().resourceLoadedThroughExtension(pageResource);
    return this.status.OK();
  }

  private onSetFunctionRangesForScript(
      message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.SetFunctionRangesForScript) {
      return this.status.E_BADARG(
          'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.SetFunctionRangesForScript}`);
    }
    const {scriptUrl, ranges} = message;
    if (!scriptUrl || !ranges?.length) {
      return this.status.E_BADARG('command', 'expected valid scriptUrl and non-empty NamedFunctionRanges');
    }
    const resource = this.lookupAllowedUISourceCode(scriptUrl as Platform.DevToolsPath.UrlString, port);
    if ('error' in resource) {
      return resource.error;
    }
    const {uiSourceCode} = resource;
    if (!uiSourceCode.contentType().isScript() || !uiSourceCode.contentType().isFromSourceMap()) {
      return this.status.E_BADARG('command', `expected a source map script resource for url: ${scriptUrl}`);
    }
    try {
      Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().setFunctionRanges(uiSourceCode, ranges);
    } catch (e) {
      return this.status.E_FAILED(e);
    }
    return this.status.OK();
  }

  private onShowRecorderView(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage): Record
      |undefined {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.ShowRecorderView) {
      return this.status.E_BADARG(
          'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.ShowRecorderView}`);
    }
    Extensions.RecorderPluginManager.RecorderPluginManager.instance().showView(message.id);
    return undefined;
  }

  private onShowNetworkPanel(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage): Record
      |undefined {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.ShowNetworkPanel) {
      return this.status.E_BADARG(
          'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.ShowNetworkPanel}`);
    }
    void Common.Revealer.reveal(new RevealableNetworkRequestFilter(message.filter));
    return this.status.OK();
  }

  private onCreateRecorderView(
      message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.CreateRecorderView) {
      return this.status.E_BADARG(
          'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.CreateRecorderView}`);
    }
    const id = message.id;
    // The ids are generated on the client API side and must be unique, so the check below
    // shouldn't be hit unless someone is bypassing the API.
    if (this.clientObjects.has(id)) {
      return this.status.E_EXISTS(id);
    }

    const pagePath = ExtensionServer.expandResourcePath(this.getExtensionOrigin(port), message.pagePath);
    if (pagePath === undefined) {
      return this.status.E_BADARG('pagePath', 'Resources paths cannot point to non-extension resources');
    }
    const onShown = (): void => this.notifyViewShown(id);
    const onHidden = (): void => this.notifyViewHidden(id);
    Extensions.RecorderPluginManager.RecorderPluginManager.instance().registerView({
      id,
      pagePath,
      title: message.title,
      onShown,
      onHidden,
    });
    return this.status.OK();
  }

  private inspectedURLChanged(event: Common.EventTarget.EventTargetEvent<SDK.Target.Target>): void {
    if (!ExtensionServer.canInspectURL(event.data.inspectedURL())) {
      this.disableExtensions();
      return;
    }
    if (event.data !== SDK.TargetManager.TargetManager.instance().primaryPageTarget()) {
      return;
    }
    this.requests = new Map();
    this.enableExtensions();
    const url = event.data.inspectedURL();
    this.postNotification(Extensions.ExtensionAPI.PrivateAPI.Events.InspectedURLChanged, [url]);
    const extensions = this.#pendingExtensions.splice(0);
    extensions.forEach(e => this.addExtension(e));
  }

  hasSubscribers(type: string): boolean {
    return this.subscribers.has(type);
  }

  private postNotification(type: string, args: unknown[], filter?: (extension: RegisteredExtension) => boolean): void {
    if (!this.extensionsEnabled) {
      return;
    }
    const subscribers = this.subscribers.get(type);
    if (!subscribers) {
      return;
    }
    const message = {command: 'notify-' + type, arguments: args};
    for (const subscriber of subscribers) {
      if (!this.extensionEnabled(subscriber)) {
        continue;
      }
      if (filter) {
        const origin = extensionOrigins.get(subscriber);
        const extension = origin && this.registeredExtensions.get(origin);
        if (!extension || !filter(extension)) {
          continue;
        }
      }
      subscriber.postMessage(message);
    }
  }

  private onSubscribe(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort):
      Record|undefined {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.Subscribe) {
      return this.status.E_BADARG('command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.Subscribe}`);
    }
    const subscribers = this.subscribers.get(message.type);
    if (subscribers) {
      subscribers.add(port);
    } else {
      this.subscribers.set(message.type, new Set([port]));
      const handler = this.subscriptionStartHandlers.get(message.type);
      if (handler) {
        handler();
      }
    }
    return undefined;
  }

  private onUnsubscribe(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort):
      Record|undefined {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.Unsubscribe) {
      return this.status.E_BADARG('command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.Unsubscribe}`);
    }
    const subscribers = this.subscribers.get(message.type);
    if (!subscribers) {
      return;
    }
    subscribers.delete(port);
    if (!subscribers.size) {
      this.subscribers.delete(message.type);
      const handler = this.subscriptionStopHandlers.get(message.type);
      if (handler) {
        handler();
      }
    }
    return undefined;
  }

  private onAddRequestHeaders(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage): Record
      |undefined {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.AddRequestHeaders) {
      return this.status.E_BADARG(
          'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.AddRequestHeaders}`);
    }
    const id = message.extensionId;
    if (typeof id !== 'string') {
      return this.status.E_BADARGTYPE('extensionId', typeof id, 'string');
    }
    let extensionHeaders = this.extraHeaders.get(id);
    if (!extensionHeaders) {
      extensionHeaders = new Map();
      this.extraHeaders.set(id, extensionHeaders);
    }
    for (const name in message.headers) {
      extensionHeaders.set(name, message.headers[name]);
    }
    const allHeaders = ({} as Protocol.Network.Headers);
    for (const headers of this.extraHeaders.values()) {
      for (const [name, value] of headers) {
        if (name !== '__proto__' && typeof value === 'string') {
          allHeaders[name] = value;
        }
      }
    }

    SDK.NetworkManager.MultitargetNetworkManager.instance().setExtraHTTPHeaders(allHeaders);
    return undefined;
  }

  private getExtensionOrigin(port: MessagePort): Platform.DevToolsPath.UrlString {
    const origin = extensionOrigins.get(port);
    if (!origin) {
      throw new Error('Received a message from an unregistered extension');
    }
    return origin;
  }

  private onCreatePanel(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort):
      Record {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.CreatePanel) {
      return this.status.E_BADARG('command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.CreatePanel}`);
    }
    const id = message.id;
    // The ids are generated on the client API side and must be unique, so the check below
    // shouldn't be hit unless someone is bypassing the API.
    if (this.clientObjects.has(id) || UI.InspectorView.InspectorView.instance().hasPanel(id)) {
      return this.status.E_EXISTS(id);
    }

    const page = ExtensionServer.expandResourcePath(this.getExtensionOrigin(port), message.page);
    if (page === undefined) {
      return this.status.E_BADARG('page', 'Resources paths cannot point to non-extension resources');
    }
    let persistentId = this.getExtensionOrigin(port) + message.title;
    persistentId = persistentId.replace(/\s|:\d+/g, '');
    const panelView = new ExtensionServerPanelView(
        persistentId, i18n.i18n.lockedString(message.title), new ExtensionPanel(this, persistentId, id, page));
    this.clientObjects.set(id, panelView);
    UI.InspectorView.InspectorView.instance().addPanel(panelView);
    return this.status.OK();
  }

  private onShowPanel(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage): Record|undefined {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.ShowPanel) {
      return this.status.E_BADARG('command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.ShowPanel}`);
    }

    let panelViewId = message.id;
    const panelView = this.clientObjects.get(message.id);
    if (panelView && panelView instanceof ExtensionServerPanelView) {
      panelViewId = panelView.viewId();
    }
    void UI.InspectorView.InspectorView.instance().showPanel(panelViewId);
    return undefined;
  }

  private onCreateToolbarButton(
      message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.CreateToolbarButton) {
      return this.status.E_BADARG(
          'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.CreateToolbarButton}`);
    }
    const panelView = this.clientObjects.get(message.panel);
    if (!panelView || !(panelView instanceof ExtensionServerPanelView)) {
      return this.status.E_NOTFOUND(message.panel);
    }
    const resourcePath = ExtensionServer.expandResourcePath(this.getExtensionOrigin(port), message.icon);
    if (resourcePath === undefined) {
      return this.status.E_BADARG('icon', 'Resources paths cannot point to non-extension resources');
    }
    const button = new ExtensionButton(this, message.id, resourcePath, message.tooltip, message.disabled);
    this.clientObjects.set(message.id, button);

    void panelView.widget().then(appendButton);

    function appendButton(panel: UI.Widget.Widget): void {
      (panel as ExtensionPanel).addToolbarItem(button.toolbarButton());
    }

    return this.status.OK();
  }

  private onUpdateButton(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort):
      Record {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.UpdateButton) {
      return this.status.E_BADARG('command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.UpdateButton}`);
    }
    const button = this.clientObjects.get(message.id);
    if (!button || !(button instanceof ExtensionButton)) {
      return this.status.E_NOTFOUND(message.id);
    }
    const resourcePath =
        message.icon && ExtensionServer.expandResourcePath(this.getExtensionOrigin(port), message.icon);
    if (message.icon && resourcePath === undefined) {
      return this.status.E_BADARG('icon', 'Resources paths cannot point to non-extension resources');
    }
    button.update(resourcePath, message.tooltip, message.disabled);
    return this.status.OK();
  }

  private onCreateSidebarPane(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage): Record {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.CreateSidebarPane) {
      return this.status.E_BADARG(
          'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.CreateSidebarPane}`);
    }
    const id = message.id;
    const sidebar = new ExtensionSidebarPane(this, message.panel, i18n.i18n.lockedString(message.title), id);
    this.#sidebarPanes.push(sidebar);
    this.clientObjects.set(id, sidebar);
    this.dispatchEventToListeners(Events.SidebarPaneAdded, sidebar);

    return this.status.OK();
  }

  sidebarPanes(): ExtensionSidebarPane[] {
    return this.#sidebarPanes;
  }

  private onSetSidebarHeight(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage): Record {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.SetSidebarHeight) {
      return this.status.E_BADARG(
          'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.SetSidebarHeight}`);
    }
    const sidebar = this.clientObjects.get(message.id);
    if (!sidebar || !(sidebar instanceof ExtensionSidebarPane)) {
      return this.status.E_NOTFOUND(message.id);
    }
    sidebar.setHeight(message.height);
    return this.status.OK();
  }

  private onSetSidebarContent(
      message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record|undefined {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.SetSidebarContent) {
      return this.status.E_BADARG(
          'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.SetSidebarContent}`);
    }
    const {requestId, id, rootTitle, expression, evaluateOptions, evaluateOnPage} = message;
    const sidebar = this.clientObjects.get(id);
    if (!sidebar || !(sidebar instanceof ExtensionSidebarPane)) {
      return this.status.E_NOTFOUND(message.id);
    }

    function callback(this: ExtensionServer, error: unknown): void {
      const result = error ? this.status.E_FAILED(error) : this.status.OK();
      this.dispatchCallback(requestId, port, result);
    }
    if (evaluateOnPage) {
      sidebar.setExpression(expression, rootTitle, evaluateOptions, this.getExtensionOrigin(port), callback.bind(this));
      return undefined;
    }
    sidebar.setObject(message.expression, message.rootTitle, callback.bind(this));
    return undefined;
  }

  private onSetSidebarPage(
      message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record|undefined {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.SetSidebarPage) {
      return this.status.E_BADARG('command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.SetSidebarPage}`);
    }
    const sidebar = this.clientObjects.get(message.id);
    if (!sidebar || !(sidebar instanceof ExtensionSidebarPane)) {
      return this.status.E_NOTFOUND(message.id);
    }
    const resourcePath = ExtensionServer.expandResourcePath(this.getExtensionOrigin(port), message.page);
    if (resourcePath === undefined) {
      return this.status.E_BADARG('page', 'Resources paths cannot point to non-extension resources');
    }
    sidebar.setPage(resourcePath);
    return undefined;
  }

  private onOpenResource(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage): Record {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.OpenResource) {
      return this.status.E_BADARG('command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.OpenResource}`);
    }
    const uiSourceCode = Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodeForURL(message.url);
    if (uiSourceCode) {
      void Common.Revealer.reveal(uiSourceCode.uiLocation(message.lineNumber, message.columnNumber));
      return this.status.OK();
    }

    const resource = Bindings.ResourceUtils.resourceForURL(message.url);
    if (resource) {
      void Common.Revealer.reveal(resource);
      return this.status.OK();
    }

    const request = Logs.NetworkLog.NetworkLog.instance().requestForURL(message.url);
    if (request) {
      void Common.Revealer.reveal(request);
      return this.status.OK();
    }

    return this.status.E_NOTFOUND(message.url);
  }

  private onSetOpenResourceHandler(
      message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record|undefined {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.SetOpenResourceHandler) {
      return this.status.E_BADARG(
          'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.SetOpenResourceHandler}`);
    }
    const extension = this.registeredExtensions.get(this.getExtensionOrigin(port));
    if (!extension) {
      throw new Error('Received a message from an unregistered extension');
    }
    if (message.urlScheme) {
      extension.openResourceScheme = message.urlScheme;
    }
    const extensionOrigin = this.getExtensionOrigin(port);
    const {name} = extension;
    const registration = {
      title: name,
      origin: extensionOrigin,
      scheme: message.urlScheme,
      handler: this.handleOpenURL.bind(this, port),
      shouldHandleOpenResource: (url: Platform.DevToolsPath.UrlString, schemes: Set<string>) =>
          Components.Linkifier.Linkifier.shouldHandleOpenResource(extension.openResourceScheme, url, schemes),
    };
    if (message.handlerPresent) {
      Components.Linkifier.Linkifier.registerLinkHandler(registration);
    } else {
      Components.Linkifier.Linkifier.unregisterLinkHandler(registration);
    }
    return undefined;
  }

  private onSetThemeChangeHandler(
      message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record|undefined {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.SetThemeChangeHandler) {
      return this.status.E_BADARG(
          'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.SetThemeChangeHandler}`);
    }
    const extensionOrigin = this.getExtensionOrigin(port);
    const extension = this.registeredExtensions.get(extensionOrigin);
    if (!extension) {
      throw new Error('Received a message from an unregistered extension');
    }

    if (message.handlerPresent) {
      this.themeChangeHandlers.set(extensionOrigin, port);
    } else {
      this.themeChangeHandlers.delete(extensionOrigin);
    }
    return undefined;
  }

  private handleOpenURL(
      port: MessagePort, contentProviderOrUrl: TextUtils.ContentProvider.ContentProvider|string, lineNumber?: number,
      columnNumber?: number): void {
    let resource: {url: string, type: string};
    let isAllowed: boolean;
    if (typeof contentProviderOrUrl !== 'string') {
      resource = this.makeResource(contentProviderOrUrl);
      isAllowed = this.extensionAllowedOnContentProvider(contentProviderOrUrl, port);
    } else {
      const url = contentProviderOrUrl as Platform.DevToolsPath.UrlString;
      resource = {url, type: Common.ResourceType.resourceTypes.Other.name()};
      isAllowed = this.extensionAllowedOnURL(url, port);
    }

    if (isAllowed) {
      port.postMessage({
        command: 'open-resource',
        resource,
        lineNumber: lineNumber ? lineNumber + 1 : undefined,
        columnNumber: columnNumber ? columnNumber + 1 : undefined,
      });
    }
  }

  private extensionAllowedOnURL(url: Platform.DevToolsPath.UrlString, port: MessagePort): boolean {
    const origin = extensionOrigins.get(port);
    const extension = origin && this.registeredExtensions.get(origin);
    return Boolean(extension?.isAllowedOnTarget(url));
  }

  /**
   * Slightly more permissive as {@link extensionAllowedOnURL}: This method also permits
   * UISourceCodes that originate from a {@link SDK.Script.Script} with a sourceURL magic comment as
   * long as the corresponding target is permitted.
   */
  private extensionAllowedOnContentProvider(
      contentProvider: TextUtils.ContentProvider.ContentProvider, port: MessagePort): boolean {
    if (!(contentProvider instanceof Workspace.UISourceCode.UISourceCode)) {
      return this.extensionAllowedOnURL(contentProvider.contentURL(), port);
    }

    if (contentProvider.contentType() !== Common.ResourceType.resourceTypes.Script) {
      // We only check sourceURL magic comments for scripts (excluding ones coming from source maps).
      return this.extensionAllowedOnURL(contentProvider.contentURL(), port);
    }

    const scripts =
        Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().scriptsForUISourceCode(contentProvider);
    if (scripts.length === 0) {
      return this.extensionAllowedOnURL(contentProvider.contentURL(), port);
    }

    return scripts.every(script => {
      if (script.hasSourceURL) {
        return this.extensionAllowedOnTarget(script.target(), port);
      }
      return this.extensionAllowedOnURL(script.contentURL(), port);
    });
  }

  /**
   * This method prefers returning 'Permission denied' errors if restricted resources are not found,
   * rather then NOTFOUND. This prevents extensions from being able to fish for restricted resources.
   */
  private lookupAllowedUISourceCode(url: Platform.DevToolsPath.UrlString, port: MessagePort):
      {uiSourceCode: Workspace.UISourceCode.UISourceCode}|{
    error: Record,
  }
  {
    const uiSourceCode = Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodeForURL(url);
    if (!uiSourceCode && !this.extensionAllowedOnURL(url, port)) {
      return {error: this.status.E_FAILED('Permission denied')};
    }
    if (!uiSourceCode) {
      return {error: this.status.E_NOTFOUND(url)};
    }
    if (!this.extensionAllowedOnContentProvider(uiSourceCode, port)) {
      return {error: this.status.E_FAILED('Permission denied')};
    }
    return {uiSourceCode};
  }

  private extensionAllowedOnTarget(target: SDK.Target.Target, port: MessagePort): boolean {
    return this.extensionAllowedOnURL(target.inspectedURL(), port);
  }

  private onReload(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort):
      Record {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.Reload) {
      return this.status.E_BADARG('command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.Reload}`);
    }
    const options = (message.options || {});

    SDK.NetworkManager.MultitargetNetworkManager.instance().setUserAgentOverride(
        typeof options.userAgent === 'string' ? options.userAgent : '', null);
    let injectedScript;
    if (options.injectedScript) {
      injectedScript = '(function(){' + options.injectedScript + '})()';
    }
    const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
    if (!target) {
      return this.status.OK();
    }
    const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
    if (!this.extensionAllowedOnTarget(target, port)) {
      return this.status.E_FAILED('Permission denied');
    }
    resourceTreeModel?.reloadPage(Boolean(options.ignoreCache), injectedScript);
    return this.status.OK();
  }

  private onEvaluateOnInspectedPage(
      message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record|undefined {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.EvaluateOnInspectedPage) {
      return this.status.E_BADARG(
          'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.EvaluateOnInspectedPage}`);
    }

    const {requestId, expression, evaluateOptions} = message;
    function callback(
        this: ExtensionServer, error: string|null, object: SDK.RemoteObject.RemoteObject|null,
        wasThrown: boolean): void {
      let result;
      if (error || !object) {
        result = this.status.E_PROTOCOLERROR(error?.toString());
      } else if (wasThrown) {
        result = {isException: true, value: object.description};
      } else {
        result = {value: object.value};
      }

      this.dispatchCallback(requestId, port, result);
    }
    return this.evaluate(expression, true, true, evaluateOptions, this.getExtensionOrigin(port), callback.bind(this));
  }

  private async onGetHAR(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort):
      Promise<Record|HAR.Log.LogDTO> {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.GetHAR) {
      return this.status.E_BADARG('command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.GetHAR}`);
    }
    const requests =
        Logs.NetworkLog.NetworkLog.instance().requests().filter(r => this.extensionAllowedOnURL(r.url(), port));
    const harLog = await HAR.Log.Log.build(requests, {sanitize: false});
    for (let i = 0; i < harLog.entries.length; ++i) {
      // @ts-expect-error
      harLog.entries[i]._requestId = this.requestId(requests[i]);
    }
    return harLog;
  }

  private makeResource(contentProvider: TextUtils.ContentProvider.ContentProvider):
      {url: string, type: string, buildId?: string} {
    let buildId: string|undefined = undefined;
    if (contentProvider instanceof Workspace.UISourceCode.UISourceCode) {
      // We use the first buildId we find searching in all Script objects that correspond to this UISourceCode.
      buildId = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance()
                    .scriptsForUISourceCode(contentProvider)
                    .find(script => Boolean(script.buildId))
                    ?.buildId ??
          undefined;
    }
    return {url: contentProvider.contentURL(), type: contentProvider.contentType().name(), buildId};
  }

  private onGetPageResources(_message: unknown, port: MessagePort): Array<{url: string, type: string}> {
    const resources = new Map<unknown, {
      url: string,
      type: string,
    }>();
    function pushResourceData(
        this: ExtensionServer, contentProvider: TextUtils.ContentProvider.ContentProvider): boolean {
      if (!resources.has(contentProvider.contentURL()) &&
          this.extensionAllowedOnContentProvider(contentProvider, port)) {
        resources.set(contentProvider.contentURL(), this.makeResource(contentProvider));
      }
      return false;
    }
    let uiSourceCodes = Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodesForProjectType(
        Workspace.Workspace.projectTypes.Network);
    uiSourceCodes = uiSourceCodes.concat(Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodesForProjectType(
        Workspace.Workspace.projectTypes.ContentScripts));
    uiSourceCodes.forEach(pushResourceData.bind(this));
    for (const resourceTreeModel of SDK.TargetManager.TargetManager.instance().models(
             SDK.ResourceTreeModel.ResourceTreeModel)) {
      if (this.extensionAllowedOnTarget(resourceTreeModel.target(), port)) {
        resourceTreeModel.forAllResources(pushResourceData.bind(this));
      }
    }

    return [...resources.values()];
  }

  private async getResourceContent(
      contentProvider: TextUtils.ContentProvider.ContentProvider,
      message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Promise<void> {
    if (!this.extensionAllowedOnContentProvider(contentProvider, port)) {
      this.dispatchCallback(message.requestId, port, this.status.E_FAILED('Permission denied'));
      return undefined;
    }
    const contentData = await contentProvider.requestContentData();
    if (TextUtils.ContentData.ContentData.isError(contentData)) {
      this.dispatchCallback(message.requestId, port, {encoding: '', content: null});
      return;
    }
    const encoding = !contentData.isTextContent ? 'base64' : '';
    const content = contentData.isTextContent ? contentData.text : contentData.base64;
    this.dispatchCallback(message.requestId, port, {encoding, content});
  }

  private onGetRequestContent(
      message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record|undefined {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.GetRequestContent) {
      return this.status.E_BADARG(
          'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.GetRequestContent}`);
    }
    const request = this.requestById(message.id);
    if (!request) {
      return this.status.E_NOTFOUND(message.id);
    }
    void this.getResourceContent(request, message, port);
    return undefined;
  }

  private onGetResourceContent(
      message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record|undefined {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.GetResourceContent) {
      return this.status.E_BADARG(
          'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.GetResourceContent}`);
    }
    const url = message.url as Platform.DevToolsPath.UrlString;
    const contentProvider = Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodeForURL(url) ||
        Bindings.ResourceUtils.resourceForURL(url);
    if (!contentProvider) {
      return this.status.E_NOTFOUND(url);
    }
    void this.getResourceContent(contentProvider, message, port);
    return undefined;
  }

  private onAttachSourceMapToResource(
      message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record|undefined {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.AttachSourceMapToResource) {
      return this.status.E_BADARG(
          'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.GetResourceContent}`);
    }

    if (!message.sourceMapURL) {
      return this.status.E_FAILED('Expected a source map URL but got null');
    }

    const resource = this.lookupAllowedUISourceCode(message.contentUrl as Platform.DevToolsPath.UrlString, port);
    if ('error' in resource) {
      return resource.error;
    }

    const debuggerBindingsInstance = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance();
    const scriptFiles = debuggerBindingsInstance.scriptsForUISourceCode(resource.uiSourceCode);
    if (scriptFiles.length > 0) {
      for (const script of scriptFiles) {
        const resourceFile = debuggerBindingsInstance.scriptFile(resource.uiSourceCode, script.debuggerModel);
        resourceFile?.addSourceMapURL(message.sourceMapURL as Platform.DevToolsPath.UrlString);
      }
    }

    return this.status.OK();
  }

  private onSetResourceContent(
      message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record|undefined {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.SetResourceContent) {
      return this.status.E_BADARG(
          'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.SetResourceContent}`);
    }

    const {url, requestId, content, commit} = message;
    function callbackWrapper(this: ExtensionServer, error: string|null): void {
      const response = error ? this.status.E_FAILED(error) : this.status.OK();
      this.dispatchCallback(requestId, port, response);
    }

    const resource = this.lookupAllowedUISourceCode(url as Platform.DevToolsPath.UrlString, port);
    if ('error' in resource) {
      return resource.error;
    }
    const {uiSourceCode} = resource;
    if (!uiSourceCode.contentType().isDocumentOrScriptOrStyleSheet()) {
      const resource = SDK.ResourceTreeModel.ResourceTreeModel.resourceForURL(url as Platform.DevToolsPath.UrlString);
      if (!resource) {
        return this.status.E_NOTFOUND(url);
      }
      return this.status.E_NOTSUPPORTED('Resource is not editable');
    }
    uiSourceCode.setWorkingCopy(content);
    if (commit) {
      uiSourceCode.commitWorkingCopy();
    }
    callbackWrapper.call(this, null);
    return undefined;
  }

  private requestId(request: TextUtils.ContentProvider.ContentProvider): number {
    const requestId = this.requestIds.get(request);
    if (requestId === undefined) {
      const newId = ++this.lastRequestId;
      this.requestIds.set(request, newId);
      this.requests.set(newId, request);
      return newId;
    }
    return requestId;
  }

  private requestById(id: number): TextUtils.ContentProvider.ContentProvider|undefined {
    return this.requests.get(id);
  }

  private onForwardKeyboardEvent(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage): Record
      |undefined {
    if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.ForwardKeyboardEvent) {
      return this.status.E_BADARG(
          'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.ForwardKeyboardEvent}`);
    }
    message.entries.forEach(handleEventEntry);

    function handleEventEntry(entry: KeyboardEventInit&{eventType: string}): void {
      // Fool around closure compiler -- it has its own notion of both KeyboardEvent constructor
      // and initKeyboardEvent methods and overriding these in externs.js does not have effect.
      const event = new window.KeyboardEvent(entry.eventType, {
        key: entry.key,
        code: entry.code,
        keyCode: entry.keyCode,
        location: entry.location,
        ctrlKey: entry.ctrlKey,
        altKey: entry.altKey,
        shiftKey: entry.shiftKey,
        metaKey: entry.metaKey,
      });

      // @ts-expect-error
      event.__keyCode = keyCodeForEntry(entry);
      document.dispatchEvent(event);
    }

    function keyCodeForEntry(entry: KeyboardEventInit): unknown {
      let keyCode = entry.keyCode;
      if (!keyCode) {
        // This is required only for synthetic events (e.g. dispatched in tests).
        if (entry.key === Platform.KeyboardUtilities.ESCAPE_KEY) {
          keyCode = 27;
        }
      }
      return keyCode || 0;
    }
    return undefined;
  }

  private dispatchCallback(requestId: unknown, port: MessagePort, result: unknown): void {
    if (requestId) {
      port.postMessage({command: 'callback', requestId, result});
    }
  }

  private initExtensions(): void {
    this.registerAutosubscriptionHandler(
        Extensions.ExtensionAPI.PrivateAPI.Events.ResourceAdded, Workspace.Workspace.WorkspaceImpl.instance(),
        Workspace.Workspace.Events.UISourceCodeAdded, this.notifyResourceAdded);
    this.registerAutosubscriptionTargetManagerHandler(
        Extensions.ExtensionAPI.PrivateAPI.Events.NetworkRequestFinished, SDK.NetworkManager.NetworkManager,
        SDK.NetworkManager.Events.RequestFinished, this.notifyRequestFinished);

    function onElementsSubscriptionStarted(this: ExtensionServer): void {
      UI.Context.Context.instance().addFlavorChangeListener(
          SDK.DOMModel.DOMNode, this.notifyElementsSelectionChanged, this);
    }

    function onElementsSubscriptionStopped(this: ExtensionServer): void {
      UI.Context.Context.instance().removeFlavorChangeListener(
          SDK.DOMModel.DOMNode, this.notifyElementsSelectionChanged, this);
    }

    this.registerSubscriptionHandler(
        Extensions.ExtensionAPI.PrivateAPI.Events.PanelObjectSelected + 'elements',
        onElementsSubscriptionStarted.bind(this), onElementsSubscriptionStopped.bind(this));
    this.registerResourceContentCommittedHandler(this.notifyUISourceCodeContentCommitted);

    SDK.TargetManager.TargetManager.instance().addEventListener(
        SDK.TargetManager.Events.INSPECTED_URL_CHANGED, this.inspectedURLChanged, this);
  }

  private notifyResourceAdded(event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.UISourceCode>): void {
    const uiSourceCode = event.data;
    this.postNotification(
        Extensions.ExtensionAPI.PrivateAPI.Events.ResourceAdded, [this.makeResource(uiSourceCode)],
        extension => extension.isAllowedOnTarget(uiSourceCode.url()));
  }

  private notifyUISourceCodeContentCommitted(
      event: Common.EventTarget.EventTargetEvent<Workspace.Workspace.WorkingCopyCommittedEvent>): void {
    const {uiSourceCode, content} = event.data;
    this.postNotification(
        Extensions.ExtensionAPI.PrivateAPI.Events.ResourceContentCommitted, [this.makeResource(uiSourceCode), content],
        extension => extension.isAllowedOnTarget(uiSourceCode.url()));
  }

  private async notifyRequestFinished(event: Common.EventTarget.EventTargetEvent<SDK.NetworkRequest.NetworkRequest>):
      Promise<void> {
    const request = event.data;
    const entry = await HAR.Log.Entry.build(request, {sanitize: false});
    this.postNotification(
        Extensions.ExtensionAPI.PrivateAPI.Events.NetworkRequestFinished, [this.requestId(request), entry],
        extension => extension.isAllowedOnTarget(entry.request.url));
  }

  private notifyElementsSelectionChanged(): void {
    this.postNotification(Extensions.ExtensionAPI.PrivateAPI.Events.PanelObjectSelected + 'elements', []);
  }

  sourceSelectionChanged(url: Platform.DevToolsPath.UrlString, range: TextUtils.TextRange.TextRange): void {
    this.postNotification(
        Extensions.ExtensionAPI.PrivateAPI.Events.PanelObjectSelected + 'sources', [{
          startLine: range.startLine,
          startColumn: range.startColumn,
          endLine: range.endLine,
          endColumn: range.endColumn,
          url,
        }],
        extension => extension.isAllowedOnTarget(url));
  }

  private setInspectedTabId(event: Common.EventTarget.EventTargetEvent<string>): void {
    const oldId = this.inspectedTabId;
    this.inspectedTabId = event.data;
    if (oldId === null) {
      // Run deferred init
      this.initializeExtensions();
    }
  }

  addExtensionFrame({startPage, name}: Host.InspectorFrontendHostAPI.ExtensionDescriptor): void {
    const iframe = document.createElement('iframe');
    iframe.src = startPage;
    iframe.dataset.devtoolsExtension = name;
    iframe.style.display = 'none';
    document.body.appendChild(iframe);  // Only for main window.
  }

  addExtension(extensionInfo: Host.InspectorFrontendHostAPI.ExtensionDescriptor): boolean|undefined {
    const startPage = extensionInfo.startPage;

    const inspectedURL = SDK.TargetManager.TargetManager.instance().primaryPageTarget()?.inspectedURL() ?? '';
    if (inspectedURL === '') {
      this.#pendingExtensions.push(extensionInfo);
      return;
    }
    if (!ExtensionServer.canInspectURL(inspectedURL)) {
      this.disableExtensions();
    }
    if (!this.extensionsEnabled) {
      this.#pendingExtensions.push(extensionInfo);
      return;
    }
    const hostsPolicy = HostsPolicy.create(extensionInfo.hostsPolicy);
    if (!hostsPolicy) {
      return;
    }
    try {
      const startPageURL = new URL((startPage));
      const extensionOrigin = startPageURL.origin;
      const name = extensionInfo.name || `Extension ${extensionOrigin}`;
      const extensionRegistration = new RegisteredExtension(name, hostsPolicy, Boolean(extensionInfo.allowFileAccess));
      if (!extensionRegistration.isAllowedOnTarget(inspectedURL)) {
        this.#pendingExtensions.push(extensionInfo);
        return;
      }
      if (!this.registeredExtensions.get(extensionOrigin)) {
        // See ExtensionAPI.js for details.
        const injectedAPI = self.buildExtensionAPIInjectedScript(
            extensionInfo, this.inspectedTabId as string, ThemeSupport.ThemeSupport.instance().themeName(),
            UI.ShortcutRegistry.ShortcutRegistry.instance().globalShortcutKeys(),
            ExtensionServer.instance().extensionAPITestHook);
        Host.InspectorFrontendHost.InspectorFrontendHostInstance.setInjectedScriptForOrigin(
            extensionOrigin, injectedAPI);
        this.registeredExtensions.set(extensionOrigin, extensionRegistration);
      }
      this.addExtensionFrame(extensionInfo);
    } catch (e) {
      console.error('Failed to initialize extension ' + startPage + ':' + e);
      return false;
    }
    return true;
  }

  private registerExtension(origin: Platform.DevToolsPath.UrlString, port: MessagePort): void {
    if (!this.registeredExtensions.has(origin)) {
      if (origin !== window.location.origin) {  // Just ignore inspector frames.
        console.error('Ignoring unauthorized client request from ' + origin);
      }
      return;
    }
    extensionOrigins.set(port, origin);
    port.addEventListener('message', this.onmessage.bind(this), false);
    port.start();
  }

  private onWindowMessage = (event: MessageEvent): void => {
    if (event.data === 'registerExtension') {
      this.registerExtension(event.origin as Platform.DevToolsPath.UrlString, event.ports[0]);
    }
  };

  private extensionEnabled(port: MessagePort): boolean {
    if (!this.extensionsEnabled) {
      return false;
    }
    const origin = extensionOrigins.get(port);
    if (!origin) {
      return false;
    }
    const extension = this.registeredExtensions.get(origin);
    if (!extension) {
      return false;
    }
    return extension.isAllowedOnTarget();
  }

  private async onmessage(event: MessageEvent): Promise<void> {
    const message = event.data;
    let result;

    const port = event.currentTarget as MessagePort;
    const handler = this.handlers.get(message.command);

    if (!handler) {
      result = this.status.E_NOTSUPPORTED(message.command);
    } else if (!this.extensionEnabled(port)) {
      result = this.status.E_FAILED('Permission denied');
    } else {
      try {
        result = await handler(message, event.target as MessagePort);
      } catch (e) {
        result = this.status.E_FAILED(e.message);
      }
    }

    if (result && message.requestId) {
      this.dispatchCallback(message.requestId, event.target as MessagePort, result);
    }
  }

  private registerHandler(
      command: string,
      callback:
          (message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort) => unknown):
      void {
    console.assert(Boolean(command));
    this.handlers.set(command, callback);
  }

  private registerSubscriptionHandler(
      eventTopic: string, onSubscribeFirst: () => unknown, onUnsubscribeLast: () => unknown): void {
    this.subscriptionStartHandlers.set(eventTopic, onSubscribeFirst);
    this.subscriptionStopHandlers.set(eventTopic, onUnsubscribeLast);
  }

  private registerAutosubscriptionHandler<Events, T extends keyof Events>(
      eventTopic: string, eventTarget: Common.EventTarget.EventTarget<Events>, frontendEventType: T,
      handler: Common.EventTarget.EventListener<Events, T>): void {
    this.registerSubscriptionHandler(
        eventTopic, () => eventTarget.addEventListener(frontendEventType, handler, this),
        () => eventTarget.removeEventListener(frontendEventType, handler, this));
  }

  private registerAutosubscriptionTargetManagerHandler<Events, T extends keyof Events>(
      eventTopic: string, modelClass: new(arg1: SDK.Target.Target) => SDK.SDKModel.SDKModel<Events>,
      frontendEventType: T, handler: Common.EventTarget.EventListener<Events, T>): void {
    this.registerSubscriptionHandler(
        eventTopic,
        () => SDK.TargetManager.TargetManager.instance().addModelListener(modelClass, frontendEventType, handler, this),
        () => SDK.TargetManager.TargetManager.instance().removeModelListener(
            modelClass, frontendEventType, handler, this));
  }

  private registerResourceContentCommittedHandler(
      handler: (arg0: Common.EventTarget.EventTargetEvent<Workspace.Workspace.WorkingCopyCommittedEvent>) => unknown):
      void {
    function addFirstEventListener(this: ExtensionServer): void {
      Workspace.Workspace.WorkspaceImpl.instance().addEventListener(
          Workspace.Workspace.Events.WorkingCopyCommittedByUser, handler, this);
      Workspace.Workspace.WorkspaceImpl.instance().setHasResourceContentTrackingExtensions(true);
    }

    function removeLastEventListener(this: ExtensionServer): void {
      Workspace.Workspace.WorkspaceImpl.instance().setHasResourceContentTrackingExtensions(false);
      Workspace.Workspace.WorkspaceImpl.instance().removeEventListener(
          Workspace.Workspace.Events.WorkingCopyCommittedByUser, handler, this);
    }

    this.registerSubscriptionHandler(
        Extensions.ExtensionAPI.PrivateAPI.Events.ResourceContentCommitted, addFirstEventListener.bind(this),
        removeLastEventListener.bind(this));
  }

  static expandResourcePath(extensionOrigin: Platform.DevToolsPath.UrlString, resourcePath: string):
      Platform.DevToolsPath.UrlString|undefined {
    const strippedOrigin = new URL(extensionOrigin).origin;
    const resourceURL = new URL(Common.ParsedURL.normalizePath(resourcePath), strippedOrigin);
    if (resourceURL.origin !== strippedOrigin) {
      return undefined;
    }
    return resourceURL.href as Platform.DevToolsPath.UrlString;
  }

  evaluate(
      expression: string, exposeCommandLineAPI: boolean, returnByValue: boolean,
      options: Extensions.ExtensionAPI.PrivateAPI.EvaluateOptions|undefined, securityOrigin: string,
      callback: (arg0: string|null, arg1: SDK.RemoteObject.RemoteObject|null, arg2: boolean) => unknown): Record
      |undefined {
    let context;

    function resolveURLToFrame(url: Platform.DevToolsPath.UrlString): SDK.ResourceTreeModel.ResourceTreeFrame|null {
      let found = null;
      function hasMatchingURL(frame: SDK.ResourceTreeModel.ResourceTreeFrame): SDK.ResourceTreeModel.ResourceTreeFrame|
          null {
        found = (frame.url === url) ? frame : null;
        return found;
      }
      SDK.ResourceTreeModel.ResourceTreeModel.frames().some(hasMatchingURL);
      return found;
    }

    options = options || {};
    let frame;
    if (options.frameURL) {
      frame = resolveURLToFrame(options.frameURL as Platform.DevToolsPath.UrlString);
    } else {
      const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
      const resourceTreeModel = target?.model(SDK.ResourceTreeModel.ResourceTreeModel);
      frame = resourceTreeModel?.mainFrame;
    }
    if (!frame) {
      if (options.frameURL) {
        console.warn('evaluate: there is no frame with URL ' + options.frameURL);
      } else {
        console.warn('evaluate: the main frame is not yet available');
      }
      return this.status.E_NOTFOUND(options.frameURL || '<top>');
    }
    // We shouldn't get here if the outermost frame can't be inspected by an extension, but
    // let's double check for subframes.
    const extension = this.registeredExtensions.get(securityOrigin);
    if (!extension?.isAllowedOnTarget(frame.url)) {
      return this.status.E_FAILED('Permission denied');
    }

    let contextSecurityOrigin;
    if (options.useContentScriptContext) {
      contextSecurityOrigin = securityOrigin;
    } else if (options.scriptExecutionContext) {
      contextSecurityOrigin = options.scriptExecutionContext;
    }

    const runtimeModel = frame.resourceTreeModel().target().model(SDK.RuntimeModel.RuntimeModel);
    const executionContexts = runtimeModel ? runtimeModel.executionContexts() : [];
    if (contextSecurityOrigin) {
      for (let i = 0; i < executionContexts.length; ++i) {
        const executionContext = executionContexts[i];
        if (executionContext.frameId === frame.id && executionContext.origin === contextSecurityOrigin &&
            !executionContext.isDefault) {
          context = executionContext;
        }
      }
      if (!context) {
        console.warn('The JavaScript context ' + contextSecurityOrigin + ' was not found in the frame ' + frame.url);
        return this.status.E_NOTFOUND(contextSecurityOrigin);
      }
    } else {
      for (let i = 0; i < executionContexts.length; ++i) {
        const executionContext = executionContexts[i];
        if (executionContext.frameId === frame.id && executionContext.isDefault) {
          context = executionContext;
        }
      }
      if (!context) {
        return this.status.E_FAILED(frame.url + ' has no execution context');
      }
    }
    if (!extension?.isAllowedOnTarget(context.origin)) {
      return this.status.E_FAILED('Permission denied');
    }

    try {
      const parsedUrl = new URL(frame.url);
      let targetType = Host.UserMetrics.ExtensionEvalTarget.WEB_PAGE;
      if (parsedUrl.protocol === 'chrome-extension:') {
        if (parsedUrl.origin === securityOrigin) {
          targetType = Host.UserMetrics.ExtensionEvalTarget.SAME_EXTENSION;
        } else {
          targetType = Host.UserMetrics.ExtensionEvalTarget.OTHER_EXTENSION;
        }
      }
      Host.userMetrics.extensionEvalTarget(targetType);
    } catch {
      // Ignore invalid URLs.
    }

    void context
        .evaluate(
            {
              expression,
              objectGroup: 'extension',
              includeCommandLineAPI: exposeCommandLineAPI,
              silent: true,
              returnByValue,
              generatePreview: false,
            },
            /* userGesture */ false, /* awaitPromise */ false)
        .then(onEvaluate);

    function onEvaluate(result: SDK.RuntimeModel.EvaluationResult): void {
      if ('error' in result) {
        callback(result.error, null, false);
        return;
      }
      callback(null, result.object || null, Boolean(result.exceptionDetails));
    }
    return undefined;
  }

  static canInspectURL(url: Platform.DevToolsPath.UrlString): boolean {
    let parsedURL;
    // This is only to work around invalid URLs we're occasionally getting from some tests.
    // TODO(caseq): make sure tests supply valid URLs or we specifically handle invalid ones.
    try {
      parsedURL = new URL(url);
    } catch {
      return false;
    }

    if (!kPermittedSchemes.includes(parsedURL.protocol)) {
      return false;
    }

    if ((window.DevToolsAPI?.getOriginsForbiddenForExtensions?.() || []).includes(parsedURL.origin)) {
      return false;
    }

    if (this.#isUrlFromChromeWebStore(parsedURL)) {
      return false;
    }

    return true;
  }

  /**
   * Tests whether a given URL is from the Chrome web store to prevent the extension server from
   * being injected. This is treated as separate from the `getOriginsForbiddenForExtensions` API because
   * DevTools might not be being run from a native origin and we still want to lock down this specific
   * origin from DevTools extensions.
   *
   * @param parsedURL The URL to check
   * @returns `true` if the URL corresponds to the Chrome web store; otherwise `false`
   */
  static #isUrlFromChromeWebStore(parsedURL: URL): boolean {
    if (parsedURL.protocol.startsWith('http') && parsedURL.hostname.match(/^chrome\.google\.com\.?$/) &&
        parsedURL.pathname.startsWith('/webstore')) {
      return true;
    }
    if (parsedURL.protocol.startsWith('http') && parsedURL.hostname.match(/^chromewebstore\.google\.com\.?$/)) {
      return true;
    }

    return false;
  }

  private disableExtensions(): void {
    this.extensionsEnabled = false;
  }

  private enableExtensions(): void {
    this.extensionsEnabled = true;
  }
}

export const enum Events {
  SidebarPaneAdded = 'SidebarPaneAdded',
}

export interface EventTypes {
  [Events.SidebarPaneAdded]: ExtensionSidebarPane;
}

class ExtensionServerPanelView extends UI.View.SimpleView {
  private readonly name: string;
  private readonly panel: UI.Panel.Panel;

  constructor(name: string, title: Platform.UIString.LocalizedString, panel: UI.Panel.Panel) {
    // The `viewId` here is to satisfy the `SimpleView` constructor needs, but isn't actually
    // used anywhere, since we override the `viewId()` method below.  Ideally we'd pass the
    // `name` as `viewId` to the constructor, but that doesn't work, since the `name` is not
    // necessarily in Kebab case.
    //
    // For non-ASCII titles (e.g., Chinese, Japanese, Arabic), the kebab-cased result may not
    // pass isExtendedKebabCase validation, so we fall back to 'extension-panel'.
    const kebabTitle = Platform.StringUtilities.toKebabCase(title);
    const viewId =
        Platform.StringUtilities.isExtendedKebabCase(kebabTitle) ? kebabTitle : 'extension-panel' as Lowercase<string>;
    super({title, viewId});
    this.name = name;
    this.panel = panel;
  }

  override viewId(): string {
    return this.name;
  }

  override widget(): Promise<UI.Widget.Widget> {
    return Promise.resolve(this.panel) as Promise<UI.Widget.Widget>;
  }
}

export class ExtensionStatus {
  OK: (...args: unknown[]) => Record;
  E_EXISTS: (...args: unknown[]) => Record;
  E_BADARG: (...args: unknown[]) => Record;
  E_BADARGTYPE: (...args: unknown[]) => Record;
  E_NOTFOUND: (...args: unknown[]) => Record;
  E_NOTSUPPORTED: (...args: unknown[]) => Record;
  E_PROTOCOLERROR: (...args: unknown[]) => Record;
  E_FAILED: (...args: unknown[]) => Record;

  constructor() {
    function makeStatus(code: string, description: string, ...details: unknown[]): Record {
      const status: Record = {code, description, details};
      if (code !== 'OK') {
        status.isError = true;
        console.error('Extension server error: ' + Platform.StringUtilities.sprintf(description, ...details));
      }
      return status;
    }

    this.OK = makeStatus.bind(null, 'OK', 'OK');
    this.E_EXISTS = makeStatus.bind(null, 'E_EXISTS', 'Object already exists: %s');
    this.E_BADARG = makeStatus.bind(null, 'E_BADARG', 'Invalid argument %s: %s');
    this.E_BADARGTYPE = makeStatus.bind(null, 'E_BADARGTYPE', 'Invalid type for argument %s: got %s, expected %s');
    this.E_NOTFOUND = makeStatus.bind(null, 'E_NOTFOUND', 'Object not found: %s');
    this.E_NOTSUPPORTED = makeStatus.bind(null, 'E_NOTSUPPORTED', 'Object does not support requested operation: %s');
    this.E_PROTOCOLERROR = makeStatus.bind(null, 'E_PROTOCOLERROR', 'Inspector protocol error: %s');
    this.E_FAILED = makeStatus.bind(null, 'E_FAILED', 'Operation failed: %s');
  }
}
export interface Record {
  code: string;
  description: string;
  details: unknown[];
  isError?: boolean;
}
