{"version":3,"file":"SubjectMetadataController.cjs","sourceRoot":"","sources":["../src/SubjectMetadataController.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n  ControllerGetStateAction,\n  ControllerStateChangeEvent,\n} from '@metamask/base-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport type { Messenger } from '@metamask/messenger';\nimport type { Json } from '@metamask/utils';\n\nimport type {\n  GenericPermissionController,\n  PermissionSubjectMetadata,\n} from './PermissionController';\nimport type { PermissionControllerHasPermissionsAction } from './PermissionController-method-action-types';\nimport type { SubjectMetadataControllerMethodActions } from './SubjectMetadataController-method-action-types';\n\nconst controllerName = 'SubjectMetadataController';\n\nconst MESSENGER_EXPOSED_METHODS = [\n  'clearState',\n  'addSubjectMetadata',\n  'getSubjectMetadata',\n  'trimMetadataState',\n] as const;\n\ntype SubjectOrigin = string;\n\n/**\n * The different kinds of subjects that MetaMask may interact with, including\n * third parties and itself (e.g., when the background communicated with the UI).\n */\nexport enum SubjectType {\n  Extension = 'extension',\n  Internal = 'internal',\n  Unknown = 'unknown',\n  Website = 'website',\n  Snap = 'snap',\n}\n\nexport type SubjectMetadata = PermissionSubjectMetadata & {\n  [key: string]: Json;\n  name: string | null;\n  subjectType: SubjectType | null;\n  extensionId: string | null;\n  iconUrl: string | null;\n};\n\ntype SubjectMetadataToAdd = PermissionSubjectMetadata & {\n  name?: string | null;\n  subjectType?: SubjectType | null;\n  extensionId?: string | null;\n  iconUrl?: string | null;\n} & Record<string, Json>;\n\nexport type SubjectMetadataControllerState = {\n  subjectMetadata: Record<SubjectOrigin, SubjectMetadata>;\n};\n\nconst stateMetadata = {\n  subjectMetadata: {\n    includeInStateLogs: true,\n    persist: true,\n    includeInDebugSnapshot: false,\n    usedInUi: true,\n  },\n};\n\nconst defaultState: SubjectMetadataControllerState = {\n  subjectMetadata: {},\n};\n\nexport type SubjectMetadataControllerGetStateAction = ControllerGetStateAction<\n  typeof controllerName,\n  SubjectMetadataControllerState\n>;\n\n/**\n * @deprecated Use `SubjectMetadataControllerGetStateAction` instead.\n */\nexport type GetSubjectMetadataState = SubjectMetadataControllerGetStateAction;\n\n/**\n * @deprecated Use `SubjectMetadataControllerGetSubjectMetadataAction` instead.\n */\nexport type GetSubjectMetadata = {\n  type: `${typeof controllerName}:getSubjectMetadata`;\n  handler: (origin: SubjectOrigin) => SubjectMetadata | undefined;\n};\n\n/**\n * @deprecated Use `SubjectMetadataControllerAddSubjectMetadataAction` instead.\n */\nexport type AddSubjectMetadata = {\n  type: `${typeof controllerName}:addSubjectMetadata`;\n  handler: (metadata: SubjectMetadataToAdd) => void;\n};\n\nexport type SubjectMetadataControllerActions =\n  | SubjectMetadataControllerGetStateAction\n  | SubjectMetadataControllerMethodActions;\n\nexport type SubjectMetadataStateChange = ControllerStateChangeEvent<\n  typeof controllerName,\n  SubjectMetadataControllerState\n>;\n\nexport type SubjectMetadataControllerEvents = SubjectMetadataStateChange;\n\ntype AllowedActions = PermissionControllerHasPermissionsAction;\n\nexport type SubjectMetadataControllerMessenger = Messenger<\n  typeof controllerName,\n  SubjectMetadataControllerActions | AllowedActions,\n  SubjectMetadataControllerEvents\n>;\n\ntype SubjectMetadataControllerOptions = {\n  messenger: SubjectMetadataControllerMessenger;\n  subjectCacheLimit: number;\n  state?: Partial<SubjectMetadataControllerState>;\n};\n\n/**\n * A controller for storing metadata associated with permission subjects. More\n * or less, a cache.\n */\nexport class SubjectMetadataController extends BaseController<\n  typeof controllerName,\n  SubjectMetadataControllerState,\n  SubjectMetadataControllerMessenger\n> {\n  readonly #subjectCacheLimit: number;\n\n  readonly #subjectsWithoutPermissionsEncounteredSinceStartup: Set<string>;\n\n  readonly #subjectHasPermissions: GenericPermissionController['hasPermissions'];\n\n  constructor({\n    messenger,\n    subjectCacheLimit,\n    state = {},\n  }: SubjectMetadataControllerOptions) {\n    if (!Number.isInteger(subjectCacheLimit) || subjectCacheLimit < 1) {\n      throw new Error(\n        `subjectCacheLimit must be a positive integer. Received: \"${subjectCacheLimit}\"`,\n      );\n    }\n\n    const hasPermissions = (origin: string): boolean => {\n      return messenger.call('PermissionController:hasPermissions', origin);\n    };\n\n    super({\n      name: controllerName,\n      metadata: stateMetadata,\n      messenger,\n      state: {\n        ...SubjectMetadataController.#getTrimmedState(state, hasPermissions),\n      },\n    });\n\n    this.#subjectHasPermissions = hasPermissions;\n    this.#subjectCacheLimit = subjectCacheLimit;\n    this.#subjectsWithoutPermissionsEncounteredSinceStartup = new Set();\n\n    this.messenger.registerMethodActionHandlers(\n      this,\n      MESSENGER_EXPOSED_METHODS,\n    );\n  }\n\n  /**\n   * Clears the state of this controller. Also resets the cache of subjects\n   * encountered since startup, so as to not prematurely reach the cache limit.\n   */\n  clearState(): void {\n    this.#subjectsWithoutPermissionsEncounteredSinceStartup.clear();\n    this.update((_draftState) => {\n      return { ...defaultState };\n    });\n  }\n\n  /**\n   * Stores domain metadata for the given origin (subject). Deletes metadata for\n   * subjects without permissions in a FIFO manner once more than\n   * {@link SubjectMetadataController.subjectCacheLimit} distinct origins have\n   * been added since boot.\n   *\n   * In order to prevent a degraded user experience,\n   * metadata is never deleted for subjects with permissions, since metadata\n   * cannot yet be requested on demand.\n   *\n   * @param metadata - The subject metadata to store.\n   */\n  addSubjectMetadata(metadata: SubjectMetadataToAdd): void {\n    const { origin } = metadata;\n    const newMetadata: SubjectMetadata = {\n      ...metadata,\n      extensionId: metadata.extensionId ?? null,\n      iconUrl: metadata.iconUrl ?? null,\n      name: metadata.name ?? null,\n      subjectType: metadata.subjectType ?? null,\n    };\n\n    let originToForget: string | null = null;\n    // We only delete the oldest encountered subject from the cache, again to\n    // ensure that the user's experience isn't degraded by missing icons etc.\n    if (\n      this.#subjectsWithoutPermissionsEncounteredSinceStartup.size >=\n      this.#subjectCacheLimit\n    ) {\n      const cachedOrigin =\n        this.#subjectsWithoutPermissionsEncounteredSinceStartup\n          .values()\n          .next().value;\n\n      this.#subjectsWithoutPermissionsEncounteredSinceStartup.delete(\n        cachedOrigin,\n      );\n\n      if (!this.#subjectHasPermissions(cachedOrigin)) {\n        originToForget = cachedOrigin;\n      }\n    }\n\n    this.#subjectsWithoutPermissionsEncounteredSinceStartup.add(origin);\n\n    this.update((draftState) => {\n      // @ts-expect-error TS2589: Type instantiation is excessively deep and possibly infinite\n      draftState.subjectMetadata[origin] = newMetadata;\n      if (typeof originToForget === 'string') {\n        delete draftState.subjectMetadata[originToForget];\n      }\n    });\n  }\n\n  /**\n   * Gets the subject metadata for the given origin, if any.\n   *\n   * @param origin - The origin for which to get the subject metadata.\n   * @returns The subject metadata, if any, or `undefined` otherwise.\n   */\n  getSubjectMetadata(origin: SubjectOrigin): SubjectMetadata | undefined {\n    return this.state.subjectMetadata[origin];\n  }\n\n  /**\n   * Deletes all subjects without permissions from the controller's state.\n   */\n  trimMetadataState(): void {\n    this.update(() => {\n      return SubjectMetadataController.#getTrimmedState(\n        this.state,\n        this.#subjectHasPermissions,\n      );\n    });\n  }\n\n  /**\n   * Returns a new state object that only includes subjects with permissions.\n   * This method is static because we want to call it in the constructor, before\n   * the controller's state is initialized.\n   *\n   * @param state - The state object to trim.\n   * @param hasPermissions - A function that returns a boolean indicating\n   * whether a particular subject (identified by its origin) has any\n   * permissions.\n   * @returns The new state object. If the specified `state` object has no\n   * subject metadata, the returned object will be equivalent to the default\n   * state of this controller.\n   */\n  static #getTrimmedState(\n    state: Partial<SubjectMetadataControllerState>,\n    hasPermissions: GenericPermissionController['hasPermissions'],\n  ): SubjectMetadataControllerState {\n    const { subjectMetadata = {} } = state;\n\n    return {\n      subjectMetadata: Object.keys(subjectMetadata).reduce<\n        Record<SubjectOrigin, SubjectMetadata>\n      >((newSubjectMetadata, origin) => {\n        if (hasPermissions(origin)) {\n          newSubjectMetadata[origin] = subjectMetadata[origin];\n        }\n        return newSubjectMetadata;\n      }, {}),\n    };\n  }\n}\n"]}