diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index ff36eae02e..f56395cd99 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -108,6 +108,7 @@ }, "web_accessible_resources": [ "content/fido2/page-script.js", + "content/lp-suppress-import-download.js", "notification/bar.html", "images/icon38.png", "images/icon38_locked.png", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 912945af9b..7bbf95234b 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -31,6 +31,12 @@ "matches": ["http://*/*", "https://*/*", "file:///*"], "run_at": "document_start" }, + { + "all_frames": false, + "js": ["content/lp-fileless-importer.js"], + "matches": ["https://lastpass.com/export.php"], + "run_at": "document_start" + }, { "all_frames": true, "css": ["content/autofill.css"], diff --git a/apps/browser/src/platform/browser/browser-api.spec.ts b/apps/browser/src/platform/browser/browser-api.spec.ts index 5bf2507194..3761f42108 100644 --- a/apps/browser/src/platform/browser/browser-api.spec.ts +++ b/apps/browser/src/platform/browser/browser-api.spec.ts @@ -320,6 +320,60 @@ describe("BrowserApi", () => { }, files: [injectDetails.file], injectImmediately: true, + world: "ISOLATED", + }); + expect(result).toEqual(executeScriptResult); + }); + + it("injects the script into a specified frameId when the extension is built for manifest v3", async () => { + const tabId = 1; + const frameId = 2; + const injectDetails = mock({ + file: "file.js", + allFrames: true, + runAt: "document_start", + frameId, + }); + jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(3); + (chrome.scripting.executeScript as jest.Mock).mockResolvedValue(executeScriptResult); + + await BrowserApi.executeScriptInTab(tabId, injectDetails); + + expect(chrome.scripting.executeScript).toHaveBeenCalledWith({ + target: { + tabId: tabId, + allFrames: injectDetails.allFrames, + frameIds: [frameId], + }, + files: [injectDetails.file], + injectImmediately: true, + world: "ISOLATED", + }); + }); + + it("injects the script into the MAIN world context when injecting a script for manifest v3", async () => { + const tabId = 1; + const injectDetails = mock({ + file: null, + allFrames: true, + runAt: "document_start", + frameId: null, + }); + const scriptingApiDetails = { world: "MAIN" as chrome.scripting.ExecutionWorld }; + jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(3); + (chrome.scripting.executeScript as jest.Mock).mockResolvedValue(executeScriptResult); + + const result = await BrowserApi.executeScriptInTab(tabId, injectDetails, scriptingApiDetails); + + expect(chrome.scripting.executeScript).toHaveBeenCalledWith({ + target: { + tabId: tabId, + allFrames: injectDetails.allFrames, + frameIds: null, + }, + files: null, + injectImmediately: true, + world: "MAIN", }); expect(result).toEqual(executeScriptResult); }); diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index 3321296e9e..b09f3e0266 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -475,12 +475,19 @@ export class BrowserApi { /** * Extension API helper method used to execute a script in a tab. + * * @see https://developer.chrome.com/docs/extensions/reference/tabs/#method-executeScript - * @param {number} tabId - * @param {chrome.tabs.InjectDetails} details - * @returns {Promise} + * @param tabId - The id of the tab to execute the script in. + * @param details {@link "InjectDetails" https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/extensionTypes/InjectDetails} + * @param scriptingApiDetails {@link "ExecutionWorld" https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld} */ - static executeScriptInTab(tabId: number, details: chrome.tabs.InjectDetails) { + static executeScriptInTab( + tabId: number, + details: chrome.tabs.InjectDetails, + scriptingApiDetails?: { + world: chrome.scripting.ExecutionWorld; + }, + ): Promise { if (BrowserApi.manifestVersion === 3) { return chrome.scripting.executeScript({ target: { @@ -490,6 +497,7 @@ export class BrowserApi { }, files: details.file ? [details.file] : null, injectImmediately: details.runAt === "document_start", + world: scriptingApiDetails?.world || "ISOLATED", }); } diff --git a/apps/browser/src/tools/background/abstractions/fileless-importer.background.ts b/apps/browser/src/tools/background/abstractions/fileless-importer.background.ts index 2ade5bf767..e4b8413718 100644 --- a/apps/browser/src/tools/background/abstractions/fileless-importer.background.ts +++ b/apps/browser/src/tools/background/abstractions/fileless-importer.background.ts @@ -1,5 +1,10 @@ import { FilelessImportTypeKeys } from "../../enums/fileless-import.enums"; +type SuppressDownloadScriptInjectionConfig = { + file: string; + scriptingApiDetails?: { world: chrome.scripting.ExecutionWorld }; +}; + type FilelessImportPortMessage = { command?: string; importType?: FilelessImportTypeKeys; @@ -27,6 +32,7 @@ interface FilelessImporterBackground { } export { + SuppressDownloadScriptInjectionConfig, FilelessImportPortMessage, ImportNotificationMessageHandlers, LpImporterMessageHandlers, diff --git a/apps/browser/src/tools/background/fileless-importer.background.spec.ts b/apps/browser/src/tools/background/fileless-importer.background.spec.ts index 9fc87d65a4..d3436099ef 100644 --- a/apps/browser/src/tools/background/fileless-importer.background.spec.ts +++ b/apps/browser/src/tools/background/fileless-importer.background.spec.ts @@ -1,4 +1,5 @@ import { mock } from "jest-mock-extended"; +import { firstValueFrom } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -14,10 +15,20 @@ import { sendPortMessage, triggerRuntimeOnConnectEvent, } from "../../autofill/spec/testing-utils"; +import { BrowserApi } from "../../platform/browser/browser-api"; import { FilelessImportPort, FilelessImportType } from "../enums/fileless-import.enums"; import FilelessImporterBackground from "./fileless-importer.background"; +jest.mock("rxjs", () => { + const rxjs = jest.requireActual("rxjs"); + const { firstValueFrom } = rxjs; + return { + ...rxjs, + firstValueFrom: jest.fn(firstValueFrom), + }; +}); + describe("FilelessImporterBackground ", () => { let filelessImporterBackground: FilelessImporterBackground; const configService = mock(); @@ -51,14 +62,17 @@ describe("FilelessImporterBackground ", () => { describe("handle ports onConnect", () => { let lpImporterPort: chrome.runtime.Port; + let manifestVersionSpy: jest.SpyInstance; + let executeScriptInTabSpy: jest.SpyInstance; beforeEach(() => { lpImporterPort = createPortSpyMock(FilelessImportPort.LpImporter); + manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get"); + executeScriptInTabSpy = jest.spyOn(BrowserApi, "executeScriptInTab").mockResolvedValue(null); jest.spyOn(authService, "getAuthStatus").mockResolvedValue(AuthenticationStatus.Unlocked); jest.spyOn(configService, "getFeatureFlag").mockResolvedValue(true); - jest - .spyOn(filelessImporterBackground as any, "removeIndividualVault") - .mockResolvedValue(false); + jest.spyOn(filelessImporterBackground as any, "removeIndividualVault"); + (firstValueFrom as jest.Mock).mockResolvedValue(false); }); it("ignores the port connection if the port name is not present in the set of filelessImportNames", async () => { @@ -83,9 +97,7 @@ describe("FilelessImporterBackground ", () => { }); it("posts a message to the port indicating that the fileless import feature is disabled if the user's policy removes individual vaults", async () => { - jest - .spyOn(filelessImporterBackground as any, "removeIndividualVault") - .mockResolvedValue(true); + (firstValueFrom as jest.Mock).mockResolvedValue(true); triggerRuntimeOnConnectEvent(lpImporterPort); await flushPromises(); @@ -117,6 +129,35 @@ describe("FilelessImporterBackground ", () => { filelessImportEnabled: true, }); }); + + it("triggers an injection of the `lp-suppress-import-download.js` script in manifest v3", async () => { + manifestVersionSpy.mockReturnValue(3); + + triggerRuntimeOnConnectEvent(lpImporterPort); + await flushPromises(); + + expect(executeScriptInTabSpy).toHaveBeenCalledWith( + lpImporterPort.sender.tab.id, + { file: "content/lp-suppress-import-download.js", runAt: "document_start" }, + { world: "MAIN" }, + ); + }); + + it("triggers an injection of the `lp-suppress-import-download-script-append-mv2.js` script in manifest v2", async () => { + manifestVersionSpy.mockReturnValue(2); + + triggerRuntimeOnConnectEvent(lpImporterPort); + await flushPromises(); + + expect(executeScriptInTabSpy).toHaveBeenCalledWith( + lpImporterPort.sender.tab.id, + { + file: "content/lp-suppress-import-download-script-append-mv2.js", + runAt: "document_start", + }, + undefined, + ); + }); }); describe("port messages", () => { @@ -126,9 +167,7 @@ describe("FilelessImporterBackground ", () => { beforeEach(async () => { jest.spyOn(authService, "getAuthStatus").mockResolvedValue(AuthenticationStatus.Unlocked); jest.spyOn(configService, "getFeatureFlag").mockResolvedValue(true); - jest - .spyOn(filelessImporterBackground as any, "removeIndividualVault") - .mockResolvedValue(false); + (firstValueFrom as jest.Mock).mockResolvedValue(false); triggerRuntimeOnConnectEvent(createPortSpyMock(FilelessImportPort.NotificationBar)); triggerRuntimeOnConnectEvent(createPortSpyMock(FilelessImportPort.LpImporter)); await flushPromises(); diff --git a/apps/browser/src/tools/background/fileless-importer.background.ts b/apps/browser/src/tools/background/fileless-importer.background.ts index f7f32c67d5..3ddc7bd1b7 100644 --- a/apps/browser/src/tools/background/fileless-importer.background.ts +++ b/apps/browser/src/tools/background/fileless-importer.background.ts @@ -11,6 +11,7 @@ import { ImportServiceAbstraction } from "@bitwarden/importer/core"; import NotificationBackground from "../../autofill/background/notification.background"; import { BrowserApi } from "../../platform/browser/browser-api"; +import { FilelessImporterInjectedScriptsConfig } from "../config/fileless-importer-injected-scripts"; import { FilelessImportPort, FilelessImportType, @@ -22,6 +23,7 @@ import { LpImporterMessageHandlers, FilelessImporterBackground as FilelessImporterBackgroundInterface, FilelessImportPortMessage, + SuppressDownloadScriptInjectionConfig, } from "./abstractions/fileless-importer.background"; class FilelessImporterBackground implements FilelessImporterBackgroundInterface { @@ -108,6 +110,23 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface await this.notificationBackground.requestFilelessImport(tab, importType); } + /** + * Injects the script used to suppress the download of the LP importer export file. + * + * @param sender - The sender of the message. + * @param injectionConfig - The configuration for the injection. + */ + private async injectScriptConfig( + sender: chrome.runtime.MessageSender, + injectionConfig: SuppressDownloadScriptInjectionConfig, + ) { + await BrowserApi.executeScriptInTab( + sender.tab.id, + { file: injectionConfig.file, runAt: "document_start" }, + injectionConfig.scriptingApiDetails, + ); + } + /** * Triggers the download of the CSV file from the LP importer. This is triggered * when the user opts to not save the export to Bitwarden within the notification bar. @@ -200,6 +219,12 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface switch (port.name) { case FilelessImportPort.LpImporter: this.lpImporterPort = port; + await this.injectScriptConfig( + port.sender, + BrowserApi.manifestVersion === 3 + ? FilelessImporterInjectedScriptsConfig.LpSuppressImportDownload.mv3 + : FilelessImporterInjectedScriptsConfig.LpSuppressImportDownload.mv2, + ); break; case FilelessImportPort.NotificationBar: this.importNotificationsPort = port; diff --git a/apps/browser/src/tools/config/fileless-importer-injected-scripts.ts b/apps/browser/src/tools/config/fileless-importer-injected-scripts.ts new file mode 100644 index 0000000000..dbc05fe18c --- /dev/null +++ b/apps/browser/src/tools/config/fileless-importer-injected-scripts.ts @@ -0,0 +1,22 @@ +import { SuppressDownloadScriptInjectionConfig } from "../background/abstractions/fileless-importer.background"; + +type FilelessImporterInjectedScriptsConfigurations = { + LpSuppressImportDownload: { + mv2: SuppressDownloadScriptInjectionConfig; + mv3: SuppressDownloadScriptInjectionConfig; + }; +}; + +const FilelessImporterInjectedScriptsConfig: FilelessImporterInjectedScriptsConfigurations = { + LpSuppressImportDownload: { + mv2: { + file: "content/lp-suppress-import-download-script-append-mv2.js", + }, + mv3: { + file: "content/lp-suppress-import-download.js", + scriptingApiDetails: { world: "MAIN" }, + }, + }, +} as const; + +export { FilelessImporterInjectedScriptsConfig }; diff --git a/apps/browser/src/tools/content/lp-fileless-importer.spec.ts b/apps/browser/src/tools/content/lp-fileless-importer.spec.ts index 9646eaa893..432754ab91 100644 --- a/apps/browser/src/tools/content/lp-fileless-importer.spec.ts +++ b/apps/browser/src/tools/content/lp-fileless-importer.spec.ts @@ -43,20 +43,6 @@ describe("LpFilelessImporter", () => { expect(portSpy.disconnect).toHaveBeenCalled(); }); - it("injects a script element that suppresses the download of the LastPass export", () => { - const script = document.createElement("script"); - jest.spyOn(document, "createElement").mockReturnValue(script); - jest.spyOn(document.documentElement, "appendChild"); - - lpFilelessImporter.handleFeatureFlagVerification({ filelessImportEnabled: true }); - - expect(document.createElement).toHaveBeenCalledWith("script"); - expect(document.documentElement.appendChild).toHaveBeenCalled(); - expect(script.textContent).toContain( - "const defaultAppendChild = Element.prototype.appendChild;", - ); - }); - it("sets up an event listener for DOMContentLoaded that triggers the importer when the document ready state is `loading`", () => { Object.defineProperty(document, "readyState", { value: "loading", diff --git a/apps/browser/src/tools/content/lp-fileless-importer.ts b/apps/browser/src/tools/content/lp-fileless-importer.ts index 107159e5a5..6f091ecf5a 100644 --- a/apps/browser/src/tools/content/lp-fileless-importer.ts +++ b/apps/browser/src/tools/content/lp-fileless-importer.ts @@ -36,7 +36,6 @@ class LpFilelessImporter implements LpFilelessImporterInterface { return; } - this.suppressDownload(); if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", this.loadImporter); return; @@ -52,46 +51,6 @@ class LpFilelessImporter implements LpFilelessImporterInterface { this.postWindowMessage({ command: "triggerCsvDownload" }); } - /** - * Suppresses the download of the CSV file by overriding the `download` attribute of the - * anchor element that is created by the LP importer. This is done by injecting a script - * into the page that overrides the `appendChild` method of the `Element` prototype. - */ - private suppressDownload() { - const script = document.createElement("script"); - script.textContent = ` - let csvDownload = ''; - let csvHref = ''; - const defaultAppendChild = Element.prototype.appendChild; - Element.prototype.appendChild = function (newChild) { - if (newChild.nodeName.toLowerCase() === 'a' && newChild.download) { - csvDownload = newChild.download; - csvHref = newChild.href; - newChild.setAttribute('href', 'javascript:void(0)'); - newChild.setAttribute('download', ''); - Element.prototype.appendChild = defaultAppendChild; - } - - return defaultAppendChild.call(this, newChild); - }; - - window.addEventListener('message', (event) => { - const command = event.data?.command; - if (event.source !== window || command !== 'triggerCsvDownload') { - return; - } - - const anchor = document.createElement('a'); - anchor.setAttribute('href', csvHref); - anchor.setAttribute('download', csvDownload); - document.body.appendChild(anchor); - anchor.click(); - document.body.removeChild(anchor); - }); - `; - document.documentElement.appendChild(script); - } - /** * Initializes the importing mechanism used to import the CSV file into Bitwarden. * This is done by observing the DOM for the addition of the LP importer element. diff --git a/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.spec.ts b/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.spec.ts new file mode 100644 index 0000000000..95b49ea00e --- /dev/null +++ b/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.spec.ts @@ -0,0 +1,21 @@ +describe("LP Suppress Import Download for Manifest v2", () => { + it("appends the `lp-suppress-import-download.js` script to the document element", () => { + let createdScriptElement: HTMLScriptElement; + jest.spyOn(window.document, "createElement"); + jest.spyOn(window.document.documentElement, "appendChild").mockImplementation((node) => { + createdScriptElement = node as HTMLScriptElement; + return node; + }); + + require("./lp-suppress-import-download-script-append.mv2"); + + expect(window.document.createElement).toHaveBeenCalledWith("script"); + expect(chrome.runtime.getURL).toHaveBeenCalledWith("content/lp-suppress-import-download.js"); + expect(window.document.documentElement.appendChild).toHaveBeenCalledWith( + expect.any(HTMLScriptElement), + ); + expect(createdScriptElement.src).toBe( + "chrome-extension://id/content/lp-suppress-import-download.js", + ); + }); +}); diff --git a/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.ts b/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.ts new file mode 100644 index 0000000000..cd641590ad --- /dev/null +++ b/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.ts @@ -0,0 +1,9 @@ +/** + * This script handles injection of the LP suppress import download script into the document. + * This is required for manifest v2, but will be removed when we migrate fully to manifest v3. + */ +(function (globalContext) { + const script = globalContext.document.createElement("script"); + script.src = chrome.runtime.getURL("content/lp-suppress-import-download.js"); + globalContext.document.documentElement.appendChild(script); +})(window); diff --git a/apps/browser/src/tools/content/lp-suppress-import-download.spec.ts b/apps/browser/src/tools/content/lp-suppress-import-download.spec.ts new file mode 100644 index 0000000000..bfff378750 --- /dev/null +++ b/apps/browser/src/tools/content/lp-suppress-import-download.spec.ts @@ -0,0 +1,81 @@ +import { flushPromises, postWindowMessage } from "../../autofill/spec/testing-utils"; + +describe("LP Suppress Import Download", () => { + const downloadAttribute = "file.csv"; + const hrefAttribute = "https://example.com/file.csv"; + const overridenHrefAttribute = "javascript:void(0)"; + let anchor: HTMLAnchorElement; + + beforeEach(() => { + jest.spyOn(Element.prototype, "appendChild"); + jest.spyOn(window, "addEventListener"); + + require("./lp-suppress-import-download"); + + anchor = document.createElement("a"); + anchor.download = downloadAttribute; + anchor.href = hrefAttribute; + anchor.click = jest.fn(); + }); + + afterEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + }); + + it("disables the automatic download anchor", () => { + document.body.appendChild(anchor); + + expect(anchor.href).toBe(overridenHrefAttribute); + expect(anchor.download).toBe(""); + }); + + it("triggers the CSVDownload when receiving a `triggerCsvDownload` window message", async () => { + window.document.createElement = jest.fn(() => anchor); + jest.spyOn(window, "removeEventListener"); + + document.body.appendChild(anchor); + + // Precondition - Ensure the anchor in the document has overridden href and download attributes + expect(anchor.href).toBe(overridenHrefAttribute); + expect(anchor.download).toBe(""); + + postWindowMessage({ command: "triggerCsvDownload" }); + await flushPromises(); + + expect(anchor.click).toHaveBeenCalled(); + expect(anchor.href).toEqual(hrefAttribute); + expect(anchor.download).toEqual(downloadAttribute); + expect(window.removeEventListener).toHaveBeenCalledWith("message", expect.any(Function)); + }); + + it("skips subsequent calls to trigger a CSVDownload", async () => { + window.document.createElement = jest.fn(() => anchor); + + document.body.appendChild(anchor); + + postWindowMessage({ command: "triggerCsvDownload" }); + await flushPromises(); + + postWindowMessage({ command: "triggerCsvDownload" }); + await flushPromises(); + + expect(anchor.click).toHaveBeenCalledTimes(1); + }); + + it("skips triggering the CSV download for window messages that do not have the correct command", () => { + document.body.appendChild(anchor); + + postWindowMessage({ command: "notTriggerCsvDownload" }); + + expect(anchor.click).not.toHaveBeenCalled(); + }); + + it("skips triggering the CSV download for window messages that do not have a data value", () => { + document.body.appendChild(anchor); + + postWindowMessage(null); + + expect(anchor.click).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/browser/src/tools/content/lp-suppress-import-download.ts b/apps/browser/src/tools/content/lp-suppress-import-download.ts new file mode 100644 index 0000000000..486d391279 --- /dev/null +++ b/apps/browser/src/tools/content/lp-suppress-import-download.ts @@ -0,0 +1,50 @@ +/** + * Handles intercepting the injection of the CSV download link, and ensures the + * download of the script is suppressed until the user opts to download the file. + * The download is triggered by a window message sent from the LpFilelessImporter + * content script. + */ +(function (globalContext) { + let csvDownload = ""; + let csvHref = ""; + let isCsvDownloadTriggered = false; + const defaultAppendChild = Element.prototype.appendChild; + Element.prototype.appendChild = function (newChild: Node) { + if (isAnchorElement(newChild) && newChild.download) { + csvDownload = newChild.download; + csvHref = newChild.href; + newChild.setAttribute("href", "javascript:void(0)"); + newChild.setAttribute("download", ""); + Element.prototype.appendChild = defaultAppendChild; + } + + return defaultAppendChild.call(this, newChild); + }; + + function isAnchorElement(node: Node): node is HTMLAnchorElement { + return node.nodeName.toLowerCase() === "a"; + } + + const handleWindowMessage = (event: MessageEvent) => { + const command = event.data?.command; + if ( + event.source !== globalContext || + command !== "triggerCsvDownload" || + isCsvDownloadTriggered + ) { + return; + } + + isCsvDownloadTriggered = true; + globalContext.removeEventListener("message", handleWindowMessage); + + const anchor = globalContext.document.createElement("a"); + anchor.setAttribute("href", csvHref); + anchor.setAttribute("download", csvDownload); + globalContext.document.body.appendChild(anchor); + anchor.click(); + globalContext.document.body.removeChild(anchor); + }; + + globalContext.addEventListener("message", handleWindowMessage); +})(window); diff --git a/apps/browser/webpack.config.js b/apps/browser/webpack.config.js index 8a074c259a..19d65dbf05 100644 --- a/apps/browser/webpack.config.js +++ b/apps/browser/webpack.config.js @@ -179,6 +179,7 @@ const mainConfig = { "overlay/list": "./src/autofill/overlay/pages/list/bootstrap-autofill-overlay-list.ts", "encrypt-worker": "../../libs/common/src/platform/services/cryptography/encrypt.worker.ts", "content/lp-fileless-importer": "./src/tools/content/lp-fileless-importer.ts", + "content/lp-suppress-import-download": "./src/tools/content/lp-suppress-import-download.ts", }, optimization: { minimize: ENV !== "development", @@ -276,6 +277,8 @@ if (manifestVersion == 2) { // Manifest V2 background pages can be run through the regular build pipeline. // Since it's a standard webpage. mainConfig.entry.background = "./src/platform/background.ts"; + mainConfig.entry["content/lp-suppress-import-download-script-append-mv2"] = + "./src/tools/content/lp-suppress-import-download-script-append.mv2.ts"; configs.push(mainConfig); } else {