UNPKG

electron-chrome-web-store

Version:

Install and update Chrome extensions from the Chrome Web Store for Electron

951 lines (939 loc) 33.1 kB
// src/browser/index.ts import { app as app4, session as electronSession3 } from "electron"; import * as path5 from "node:path"; import { existsSync } from "node:fs"; import { createRequire } from "node:module"; // src/browser/api.ts import debug3 from "debug"; import { app as app2, BrowserWindow, ipcMain, nativeImage } from "electron"; // src/browser/utils.ts import * as path from "node:path"; import { app, net } from "electron"; var fetch = ( // Prefer Node's fetch until net.fetch crash is fixed // https://github.com/electron/electron/pull/45050 globalThis.fetch || net?.fetch || (() => { throw new Error( "electron-chrome-web-store: Missing fetch API. Please upgrade Electron or Node." ); }) ); var getChromeVersion = () => process.versions.chrome || "131.0.6778.109"; function compareVersions(version1, version2) { const v1 = version1.split(".").map(Number); const v2 = version2.split(".").map(Number); for (let i = 0; i < 3; i++) { if (v1[i] > v2[i]) return 1; if (v1[i] < v2[i]) return -1; } return 0; } var getDefaultExtensionsPath = () => path.join(app.getPath("userData"), "Extensions"); // src/common/constants.ts var ExtensionInstallStatus = { BLACKLISTED: "blacklisted", BLOCKED_BY_POLICY: "blocked_by_policy", CAN_REQUEST: "can_request", CORRUPTED: "corrupted", CUSTODIAN_APPROVAL_REQUIRED: "custodian_approval_required", CUSTODIAN_APPROVAL_REQUIRED_FOR_INSTALLATION: "custodian_approval_required_for_installation", DEPRECATED_MANIFEST_VERSION: "deprecated_manifest_version", DISABLED: "disabled", ENABLED: "enabled", FORCE_INSTALLED: "force_installed", INSTALLABLE: "installable", REQUEST_PENDING: "request_pending", TERMINATED: "terminated" }; var MV2DeprecationStatus = { INACTIVE: "inactive", SOFT_DISABLE: "soft_disable", WARNING: "warning" }; var Result = { ALREADY_INSTALLED: "already_installed", BLACKLISTED: "blacklisted", BLOCKED_BY_POLICY: "blocked_by_policy", BLOCKED_FOR_CHILD_ACCOUNT: "blocked_for_child_account", FEATURE_DISABLED: "feature_disabled", ICON_ERROR: "icon_error", INSTALL_ERROR: "install_error", INSTALL_IN_PROGRESS: "install_in_progress", INVALID_ICON_URL: "invalid_icon_url", INVALID_ID: "invalid_id", LAUNCH_IN_PROGRESS: "launch_in_progress", MANIFEST_ERROR: "manifest_error", MISSING_DEPENDENCIES: "missing_dependencies", SUCCESS: "success", UNKNOWN_ERROR: "unknown_error", UNSUPPORTED_EXTENSION_TYPE: "unsupported_extension_type", USER_CANCELLED: "user_cancelled", USER_GESTURE_REQUIRED: "user_gesture_required" }; var WebGlStatus = { WEBGL_ALLOWED: "webgl_allowed", WEBGL_BLOCKED: "webgl_blocked" }; // src/browser/installer.ts import * as fs2 from "node:fs"; import * as os from "node:os"; import * as path3 from "node:path"; import { Readable } from "node:stream"; import { pipeline } from "node:stream/promises"; import { session as electronSession } from "electron"; import AdmZip from "adm-zip"; import debug2 from "debug"; import Pbf from "pbf"; // src/browser/crx3.ts function readCrxFileHeader(pbf, end) { return pbf.readFields( readCrxFileHeaderField, { sha256_with_rsa: [], sha256_with_ecdsa: [], verified_contents: void 0, signed_header_data: void 0 }, end ); } function readCrxFileHeaderField(tag, obj, pbf) { if (tag === 2) obj.sha256_with_rsa.push(readAsymmetricKeyProof(pbf, pbf.readVarint() + pbf.pos)); else if (tag === 3) obj.sha256_with_ecdsa.push(readAsymmetricKeyProof(pbf, pbf.readVarint() + pbf.pos)); else if (tag === 4) obj.verified_contents = pbf.readBytes(); else if (tag === 1e4) obj.signed_header_data = pbf.readBytes(); } function readAsymmetricKeyProof(pbf, end) { return pbf.readFields( readAsymmetricKeyProofField, { public_key: void 0, signature: void 0 }, end ); } function readAsymmetricKeyProofField(tag, obj, pbf) { if (tag === 1) obj.public_key = pbf.readBytes(); else if (tag === 2) obj.signature = pbf.readBytes(); } function readSignedData(pbf, end) { return pbf.readFields(readSignedDataField, { crx_id: void 0 }, end); } function readSignedDataField(tag, obj, pbf) { if (tag === 1) obj.crx_id = pbf.readBytes(); } // src/browser/id.ts import { createHash } from "node:crypto"; function convertHexadecimalToIDAlphabet(id) { let result = ""; for (const ch of id) { const val = parseInt(ch, 16); if (!isNaN(val)) { result += String.fromCharCode("a".charCodeAt(0) + val); } else { result += "a"; } } return result; } function generateIdFromHash(hash) { const hashedId = hash.subarray(0, 16).toString("hex"); return convertHexadecimalToIDAlphabet(hashedId); } function generateId(input) { const hash = createHash("sha256").update(input, "base64").digest(); return generateIdFromHash(hash); } // src/browser/loader.ts import * as fs from "node:fs"; import * as path2 from "node:path"; import debug from "debug"; var d = debug("electron-chrome-web-store:loader"); var manifestExists = async (dirPath) => { if (!dirPath) return false; const manifestPath = path2.join(dirPath, "manifest.json"); try { return (await fs.promises.stat(manifestPath)).isFile(); } catch { return false; } }; async function extensionSearch(dirPath, depth = 0) { if (depth >= 2) return []; const results = []; const dirEntries = await fs.promises.readdir(dirPath, { withFileTypes: true }); for (const entry of dirEntries) { if (entry.isDirectory()) { if (await manifestExists(path2.join(dirPath, entry.name))) { results.push(path2.join(dirPath, entry.name)); } else { results.push(...await extensionSearch(path2.join(dirPath, entry.name), depth + 1)); } } } return results; } async function discoverExtensions(extensionsPath) { try { const stat = await fs.promises.stat(extensionsPath); if (!stat.isDirectory()) { d("%s is not a directory", extensionsPath); return []; } } catch { d("%s does not exist", extensionsPath); return []; } const extensionDirectories = await extensionSearch(extensionsPath); const results = []; for (const extPath of extensionDirectories.filter(Boolean)) { try { const manifestPath = path2.join(extPath, "manifest.json"); const manifestJson = (await fs.promises.readFile(manifestPath)).toString(); const manifest = JSON.parse(manifestJson); const result = manifest.key ? { type: "store", path: extPath, manifest, id: generateId(manifest.key) } : { type: "unpacked", path: extPath, manifest }; results.push(result); } catch (e) { console.error(e); } } return results; } function filterOutdatedExtensions(extensions) { const uniqueExtensions = []; const storeExtMap = /* @__PURE__ */ new Map(); for (const ext of extensions) { if (ext.type === "unpacked") { uniqueExtensions.push(ext); } else if (!storeExtMap.has(ext.id)) { storeExtMap.set(ext.id, ext); } else { const latestExt = storeExtMap.get(ext.id); if (compareVersions(latestExt.manifest.version, ext.manifest.version) < 0) { storeExtMap.set(ext.id, ext); } } } storeExtMap.forEach((ext) => uniqueExtensions.push(ext)); return uniqueExtensions; } async function loadAllExtensions(session, extensionsPath, options = {}) { const sessionExtensions = session.extensions || session; let extensions = await discoverExtensions(extensionsPath); extensions = filterOutdatedExtensions(extensions); d("discovered %d extension(s) in %s", extensions.length, extensionsPath); for (const ext of extensions) { try { let extension; if (ext.type === "store") { const existingExt = sessionExtensions.getExtension(ext.id); if (existingExt) { d("skipping loading existing extension %s", ext.id); continue; } d("loading extension %s", `${ext.id}@${ext.manifest.version}`); extension = await sessionExtensions.loadExtension(ext.path); } else if (options.allowUnpacked) { d("loading unpacked extension %s", ext.path); extension = await sessionExtensions.loadExtension(ext.path); } if (extension && extension.manifest.manifest_version === 3 && extension.manifest.background?.service_worker) { const scope = `chrome-extension://${extension.id}`; await session.serviceWorkers.startWorkerForScope(scope).catch(() => { console.error(`Failed to start worker for extension ${extension.id}`); }); } } catch (error) { console.error(`Failed to load extension from ${ext.path}`); console.error(error); } } } async function findExtensionInstall(extensionId, extensionsPath) { const extensionPath = path2.join(extensionsPath, extensionId); let extensions = await discoverExtensions(extensionPath); extensions = filterOutdatedExtensions(extensions); return extensions.length > 0 ? extensions[0] : null; } // src/browser/installer.ts var d2 = debug2("electron-chrome-web-store:installer"); function getExtensionCrxURL(extensionId) { const url = new URL("https://clients2.google.com/service/update2/crx"); url.searchParams.append("response", "redirect"); url.searchParams.append("acceptformat", ["crx2", "crx3"].join(",")); const x = new URLSearchParams(); x.append("id", extensionId); x.append("uc", ""); url.searchParams.append("x", x.toString()); url.searchParams.append("prodversion", getChromeVersion()); return url.toString(); } function parseCrx(buffer) { const magicNumber = buffer.toString("utf8", 0, 4); if (magicNumber !== "Cr24") { throw new Error("Invalid CRX format"); } const version = buffer.readUInt32LE(4); const headerSize = buffer.readUInt32LE(8); const header = buffer.subarray(12, 12 + headerSize); const contents = buffer.subarray(12 + headerSize); let extensionId; let publicKey; if (version === 2) { const pubKeyLength = buffer.readUInt32LE(8); const sigLength = buffer.readUInt32LE(12); publicKey = buffer.subarray(16, 16 + pubKeyLength); extensionId = generateId(publicKey.toString("base64")); } else { const crxFileHeader = readCrxFileHeader(new Pbf(header)); const crxSignedData = readSignedData(new Pbf(crxFileHeader.signed_header_data)); const declaredCrxId = crxSignedData.crx_id ? convertHexadecimalToIDAlphabet(crxSignedData.crx_id.toString("hex")) : null; if (!declaredCrxId) { throw new Error("Invalid CRX signed data"); } const keyProof = crxFileHeader.sha256_with_rsa.find((proof) => { const crxId = proof.public_key ? generateId(proof.public_key.toString("base64")) : null; return crxId === declaredCrxId; }); if (!keyProof) { throw new Error("Invalid CRX key"); } extensionId = declaredCrxId; publicKey = keyProof.public_key; } return { extensionId, version, header, contents, publicKey }; } async function unpackCrx(crx, destPath) { const zip = new AdmZip(crx.contents); zip.extractAllTo(destPath, true); const manifestPath = path3.join(destPath, "manifest.json"); const manifestContent = await fs2.promises.readFile(manifestPath, "utf8"); const manifest = JSON.parse(manifestContent); manifest.key = crx.publicKey.toString("base64"); await fs2.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2)); return manifest; } async function readCrx(crxPath) { const crxBuffer = await fs2.promises.readFile(crxPath); return parseCrx(crxBuffer); } async function downloadCrx(url, dest) { const response = await fetch(url); if (!response.ok) { throw new Error("Failed to download extension"); } const fileStream = fs2.createWriteStream(dest); const downloadStream = Readable.fromWeb(response.body); await pipeline(downloadStream, fileStream); } async function downloadExtensionFromURL(url, extensionsDir, expectedExtensionId) { d2("downloading %s", url); const installUuid = crypto.randomUUID(); const crxPath = path3.join(os.tmpdir(), `electron-cws-download_${installUuid}.crx`); try { await downloadCrx(url, crxPath); const crx = await readCrx(crxPath); if (expectedExtensionId && expectedExtensionId !== crx.extensionId) { throw new Error( `CRX mismatches expected extension ID: ${expectedExtensionId} !== ${crx.extensionId}` ); } const unpackedPath = path3.join(extensionsDir, crx.extensionId, installUuid); await fs2.promises.mkdir(unpackedPath, { recursive: true }); const manifest = await unpackCrx(crx, unpackedPath); if (!manifest.version) { throw new Error("Installed extension is missing manifest version"); } const versionedPath = path3.join(extensionsDir, crx.extensionId, `${manifest.version}_0`); await fs2.promises.rename(unpackedPath, versionedPath); return versionedPath; } finally { await fs2.promises.rm(crxPath, { force: true }); } } async function downloadExtension(extensionId, extensionsDir) { const url = getExtensionCrxURL(extensionId); return await downloadExtensionFromURL(url, extensionsDir, extensionId); } async function installExtension(extensionId, opts = {}) { d2("installing %s", extensionId); const session = opts.session || electronSession.defaultSession; const sessionExtensions = session.extensions || session; const extensionsPath = opts.extensionsPath || getDefaultExtensionsPath(); const existingExtension = sessionExtensions.getExtension(extensionId); if (existingExtension) { d2("%s already loaded", extensionId); return existingExtension; } const existingExtensionInfo = await findExtensionInstall(extensionId, extensionsPath); if (existingExtensionInfo && existingExtensionInfo.type === "store") { d2("%s already installed", extensionId); return await sessionExtensions.loadExtension( existingExtensionInfo.path, opts.loadExtensionOptions ); } const extensionPath = await downloadExtension(extensionId, extensionsPath); const extension = await sessionExtensions.loadExtension(extensionPath, opts.loadExtensionOptions); d2("installed %s", extensionId); return extension; } async function uninstallExtension(extensionId, opts = {}) { d2("uninstalling %s", extensionId); const session = opts.session || electronSession.defaultSession; const sessionExtensions = session.extensions || session; const extensionsPath = opts.extensionsPath || getDefaultExtensionsPath(); const extensions = sessionExtensions.getAllExtensions(); const existingExt = extensions.find((ext) => ext.id === extensionId); if (existingExt) { sessionExtensions.removeExtension(extensionId); } const extensionDir = path3.join(extensionsPath, extensionId); try { const stat = await fs2.promises.stat(extensionDir); if (stat.isDirectory()) { await fs2.promises.rm(extensionDir, { recursive: true, force: true }); } } catch (error) { if (error?.code !== "ENOENT") { throw error; } } } // src/browser/api.ts var d3 = debug3("electron-chrome-web-store:api"); var WEBSTORE_URL = "https://chromewebstore.google.com"; function getExtensionInfo(ext) { const manifest = ext.manifest; return { description: manifest.description || "", enabled: !manifest.disabled, homepageUrl: manifest.homepage_url || "", hostPermissions: manifest.host_permissions || [], icons: Object.entries(manifest?.icons || {}).map(([size, url]) => ({ size: parseInt(size), url: `chrome://extension-icon/${ext.id}/${size}/0` })), id: ext.id, installType: "normal", isApp: !!manifest.app, mayDisable: true, name: manifest.name, offlineEnabled: !!manifest.offline_enabled, optionsUrl: manifest.options_page ? `chrome-extension://${ext.id}/${manifest.options_page}` : "", permissions: manifest.permissions || [], shortName: manifest.short_name || manifest.name, type: manifest.app ? "app" : "extension", updateUrl: manifest.update_url || "", version: manifest.version }; } function getExtensionInstallStatus(state, extensionId, manifest) { if (manifest && manifest.manifest_version < state.minimumManifestVersion) { return ExtensionInstallStatus.DEPRECATED_MANIFEST_VERSION; } if (state.denylist?.has(extensionId)) { return ExtensionInstallStatus.BLOCKED_BY_POLICY; } if (state.allowlist && !state.allowlist.has(extensionId)) { return ExtensionInstallStatus.BLOCKED_BY_POLICY; } const sessionExtensions = state.session.extensions || state.session; const extensions = sessionExtensions.getAllExtensions(); const extension = extensions.find((ext) => ext.id === extensionId); if (!extension) { return ExtensionInstallStatus.INSTALLABLE; } if (extension.manifest.disabled) { return ExtensionInstallStatus.DISABLED; } return ExtensionInstallStatus.ENABLED; } async function beginInstall({ sender, senderFrame }, state, details) { const extensionId = details.id; try { if (state.installing.has(extensionId)) { return { result: Result.INSTALL_IN_PROGRESS }; } let manifest; try { manifest = JSON.parse(details.manifest); } catch { return { result: Result.MANIFEST_ERROR }; } const installStatus = getExtensionInstallStatus(state, extensionId, manifest); switch (installStatus) { case ExtensionInstallStatus.INSTALLABLE: break; // good to go case ExtensionInstallStatus.BLOCKED_BY_POLICY: return { result: Result.BLOCKED_BY_POLICY }; default: { d3('unable to install extension %s with status "%s"', extensionId, installStatus); return { result: Result.UNKNOWN_ERROR }; } } let iconUrl; try { iconUrl = new URL(details.iconUrl); } catch { return { result: Result.INVALID_ICON_URL }; } let icon; try { const response = await fetch(iconUrl.href); const imageBuffer = Buffer.from(await response.arrayBuffer()); icon = nativeImage.createFromBuffer(imageBuffer); } catch { return { result: Result.ICON_ERROR }; } const browserWindow = BrowserWindow.fromWebContents(sender); if (!senderFrame || senderFrame.isDestroyed()) { return { result: Result.UNKNOWN_ERROR }; } if (state.beforeInstall) { const result = await state.beforeInstall({ id: extensionId, localizedName: details.localizedName, manifest, icon, frame: senderFrame, browserWindow: browserWindow || void 0 }); if (typeof result !== "object" || typeof result.action !== "string") { return { result: Result.UNKNOWN_ERROR }; } else if (result.action !== "allow") { return { result: Result.USER_CANCELLED }; } } state.installing.add(extensionId); await installExtension(extensionId, state); return { result: Result.SUCCESS }; } catch (error) { console.error("Extension installation failed:", error); return { result: Result.INSTALL_ERROR, message: error instanceof Error ? error.message : String(error) }; } finally { state.installing.delete(extensionId); } } var handledIpcChannels = /* @__PURE__ */ new Map(); function registerWebStoreApi(webStoreState) { const handle = (channel, handle2) => { let handlersMap = handledIpcChannels.get(channel); if (!handlersMap) { handlersMap = /* @__PURE__ */ new Map(); handledIpcChannels.set(channel, handlersMap); ipcMain.handle(channel, async function handleWebStoreIpc(event, ...args) { d3("received %s", channel); const senderOrigin = event.senderFrame?.origin; if (!senderOrigin || !senderOrigin.startsWith(WEBSTORE_URL)) { d3("ignoring webstore request from %s", senderOrigin); return; } const session = event.sender.session; const handler = handlersMap?.get(session); if (!handler) { d3("no handler for session %s", session.storagePath); return; } const result = await handler(event, ...args); d3("%s result", channel, result); return result; }); } handlersMap.set(webStoreState.session, handle2); }; handle("chromeWebstore.beginInstall", async (event, details) => { const { senderFrame } = event; d3("beginInstall", details); const result = await beginInstall(event, webStoreState, details); if (result.result === Result.SUCCESS) { queueMicrotask(() => { const sessionExtensions = webStoreState.session.extensions || webStoreState.session; const ext = sessionExtensions.getExtension(details.id); if (ext && senderFrame && !senderFrame.isDestroyed()) { try { senderFrame.send("chrome.management.onInstalled", getExtensionInfo(ext)); } catch (error) { console.error(error); } } }); } return result; }); handle("chromeWebstore.completeInstall", async (event, id) => { return Result.SUCCESS; }); handle("chromeWebstore.enableAppLauncher", async (event, enable) => { return true; }); handle("chromeWebstore.getBrowserLogin", async () => { return ""; }); handle("chromeWebstore.getExtensionStatus", async (_event, id, manifestJson) => { const manifest = JSON.parse(manifestJson); return getExtensionInstallStatus(webStoreState, id, manifest); }); handle("chromeWebstore.getFullChromeVersion", async () => { return { version_number: process.versions.chrome, app_name: app2.getName() }; }); handle("chromeWebstore.getIsLauncherEnabled", async () => { return true; }); handle("chromeWebstore.getMV2DeprecationStatus", async () => { return webStoreState.minimumManifestVersion > 2 ? MV2DeprecationStatus.SOFT_DISABLE : MV2DeprecationStatus.INACTIVE; }); handle("chromeWebstore.getReferrerChain", async () => { return "EgIIAA=="; }); handle("chromeWebstore.getStoreLogin", async () => { return ""; }); handle("chromeWebstore.getWebGLStatus", async () => { await app2.getGPUInfo("basic"); const features = app2.getGPUFeatureStatus(); return features.webgl.startsWith("enabled") ? WebGlStatus.WEBGL_ALLOWED : WebGlStatus.WEBGL_BLOCKED; }); handle("chromeWebstore.install", async (event, id, silentInstall) => { return Result.SUCCESS; }); handle("chromeWebstore.isInIncognitoMode", async () => { return false; }); handle("chromeWebstore.isPendingCustodianApproval", async (event, id) => { return false; }); handle("chromeWebstore.setStoreLogin", async (event, login) => { return true; }); handle("chrome.runtime.getManifest", async () => { return {}; }); handle("chrome.management.getAll", async (event) => { const sessionExtensions = webStoreState.session.extensions || webStoreState.session; const extensions = sessionExtensions.getAllExtensions(); return extensions.map(getExtensionInfo); }); handle("chrome.management.setEnabled", async (event, id, enabled) => { return true; }); handle( "chrome.management.uninstall", async (event, id, options) => { if (options?.showConfirmDialog) { } try { await uninstallExtension(id, webStoreState); queueMicrotask(() => { event.sender.send("chrome.management.onUninstalled", id); }); return Result.SUCCESS; } catch (error) { console.error(error); return Result.UNKNOWN_ERROR; } } ); } // src/browser/updater.ts import * as fs3 from "node:fs"; import * as path4 from "node:path"; import debug4 from "debug"; import { app as app3, powerMonitor, session as electronSession2 } from "electron"; var d4 = debug4("electron-chrome-web-store:updater"); var SYSTEM_IDLE_DURATION = 1 * 60 * 60 * 1e3; var UPDATE_CHECK_INTERVAL = 5 * 60 * 60 * 1e3; var MIN_UPDATE_INTERVAL = 3 * 60 * 60 * 1e3; var lastUpdateCheck; var ALLOWED_UPDATE_URLS = /* @__PURE__ */ new Set(["https://clients2.google.com/service/update2/crx"]); var getSessionId = /* @__PURE__ */ (() => { let sessionId; return () => sessionId || (sessionId = crypto.randomUUID()); })(); var getOmahaPlatform = () => { switch (process.platform) { case "win32": return "win"; case "darwin": return "mac"; default: return process.platform; } }; var getOmahaArch = () => { switch (process.arch) { case "ia32": return "x86"; case "x64": return "x64"; default: return process.arch; } }; function filterWebStoreExtension(extension) { const manifest = extension.manifest; if (!manifest) return false; return manifest.key && manifest.update_url && ALLOWED_UPDATE_URLS.has(manifest.update_url); } async function fetchAvailableUpdates(extensions) { if (extensions.length === 0) return []; const extensionIds = extensions.map((extension) => extension.id); const extensionMap = extensions.reduce( (map, ext) => ({ ...map, [ext.id]: ext }), {} ); const chromeVersion = getChromeVersion(); const url = "https://update.googleapis.com/service/update2/json"; const body = { request: { "@updater": "electron-chrome-web-store", acceptformat: "crx3", app: [ ...extensions.map((extension) => ({ appid: extension.id, updatecheck: {} // API always reports 'noupdate' when version is set :thinking: // version: extension.version, })) ], os: { platform: getOmahaPlatform(), arch: getOmahaArch() }, prodversion: chromeVersion, protocol: "3.1", requestid: crypto.randomUUID(), sessionid: getSessionId(), testsource: process.env.NODE_ENV === "production" ? "" : "electron_dev" } }; const response = await fetch(url, { method: "POST", headers: { "content-type": "application/json", "X-Goog-Update-Interactivity": "bg", "X-Goog-Update-AppId": extensionIds.join(","), "X-Goog-Update-Updater": `chromiumcrx-${chromeVersion}` }, body: JSON.stringify(body) }); if (!response.ok) { d4("update response not ok"); return []; } const text = await response.text(); const prefix = `)]}' `; if (!text.startsWith(prefix)) { d4("unexpected update response: %s", text); return []; } const json = text.substring(prefix.length); const result = JSON.parse(json); let updates; try { const apps = result?.response?.app || []; updates = apps.filter((app5) => app5.updatecheck.status === "ok").map((app5) => { const extensionId = app5.appid; const extension = extensionMap[extensionId]; const manifest = app5.updatecheck.manifest; const pkg = manifest.packages.package[0]; return { extension, id: extensionId, version: manifest.version, name: pkg.name, url: app5.updatecheck.urls.url[0].codebase }; }).filter((update) => { const extension = extensionMap[update.id]; return compareVersions(extension.version, update.version) < 0; }); } catch (error) { console.error("Unable to read extension updates response", error); return []; } return updates; } async function updateExtension(session, update) { const sessionExtensions = session.extensions || session; const extensionId = update.id; const oldExtension = update.extension; d4("updating %s %s -> %s", extensionId, oldExtension.version, update.version); const oldVersionDirectoryName = path4.basename(oldExtension.path); if (!oldVersionDirectoryName.startsWith(oldExtension.version)) { console.error( `updateExtension: extension ${extensionId} must conform to versioned directory names`, { oldPath: oldExtension.path } ); d4("skipping %s update due to invalid install path %s", extensionId, oldExtension.path); return; } const extensionsPath = path4.join(oldExtension.path, "..", ".."); const updatePath = await downloadExtensionFromURL(update.url, extensionsPath, extensionId); d4("downloaded update %s@%s", extensionId, update.version); if (sessionExtensions.getExtension(extensionId)) { sessionExtensions.removeExtension(extensionId); await sessionExtensions.loadExtension(updatePath); d4("loaded update %s@%s", extensionId, update.version); } await fs3.promises.rm(oldExtension.path, { recursive: true, force: true }); } async function checkForUpdates(session) { const sessionExtensions = session.extensions || session; const extensions = sessionExtensions.getAllExtensions().filter(filterWebStoreExtension); d4("checking for updates: %s", extensions.map((ext) => `${ext.id}@${ext.version}`).join(",")); const updates = await fetchAvailableUpdates(extensions); if (!updates || updates.length === 0) { d4("no updates found"); return []; } return updates; } async function installUpdates(session, updates) { d4("updating %d extension(s)", updates.length); for (const update of updates) { try { await updateExtension(session, update); } catch (error) { console.error(`checkForUpdates: Error updating extension ${update.id}`); console.error(error); } } } async function updateExtensions(session = electronSession2.defaultSession) { const updates = await checkForUpdates(session); if (updates.length > 0) { await installUpdates(session, updates); } } async function maybeCheckForUpdates(session) { const idleState = powerMonitor.getSystemIdleState(SYSTEM_IDLE_DURATION); if (idleState !== "active") { d4('skipping update check while system is in "%s" idle state', idleState); return; } if (lastUpdateCheck && Date.now() - lastUpdateCheck < MIN_UPDATE_INTERVAL) { return; } lastUpdateCheck = Date.now(); void updateExtensions(session); } async function initUpdater(state) { const check = () => maybeCheckForUpdates(state.session); switch (process.platform) { case "darwin": app3.on("did-become-active", check); break; case "win32": case "linux": app3.on("browser-window-focus", check); break; } const updateIntervalId = setInterval(check, UPDATE_CHECK_INTERVAL); check(); app3.on("before-quit", (event) => { queueMicrotask(() => { if (!event.defaultPrevented) { d4("stopping update checks"); clearInterval(updateIntervalId); } }); }); } // src/browser/index.ts function resolvePreloadPath(modulePath) { try { return createRequire(import.meta.dirname).resolve("electron-chrome-web-store/preload"); } catch (error) { if (process.env.NODE_ENV !== "production") { console.error(error); } } const preloadFilename = "chrome-web-store.preload.js"; if (modulePath) { process.emitWarning( 'electron-chrome-web-store: "modulePath" is deprecated and will be removed in future versions.', { type: "DeprecationWarning" } ); return path5.join(modulePath, "dist", preloadFilename); } return path5.join(import.meta.dirname, preloadFilename); } async function installChromeWebStore(opts = {}) { const session = opts.session || electronSession3.defaultSession; const extensionsPath = opts.extensionsPath || getDefaultExtensionsPath(); const loadExtensions = typeof opts.loadExtensions === "boolean" ? opts.loadExtensions : true; const allowUnpackedExtensions = typeof opts.allowUnpackedExtensions === "boolean" ? opts.allowUnpackedExtensions : false; const autoUpdate = typeof opts.autoUpdate === "boolean" ? opts.autoUpdate : true; const minimumManifestVersion = typeof opts.minimumManifestVersion === "number" ? opts.minimumManifestVersion : 3; const beforeInstall = typeof opts.beforeInstall === "function" ? opts.beforeInstall : void 0; const webStoreState = { session, extensionsPath, installing: /* @__PURE__ */ new Set(), allowlist: opts.allowlist ? new Set(opts.allowlist) : void 0, denylist: opts.denylist ? new Set(opts.denylist) : void 0, minimumManifestVersion, beforeInstall }; const preloadPath = resolvePreloadPath(opts.modulePath); if ("registerPreloadScript" in session) { session.registerPreloadScript({ id: "electron-chrome-web-store", type: "frame", filePath: preloadPath }); } else { session.setPreloads([...session.getPreloads(), preloadPath]); } if (!existsSync(preloadPath)) { console.error( new Error( `electron-chrome-web-store: Preload file not found at "${preloadPath}". See "Packaging the preload script" in the readme.` ) ); } registerWebStoreApi(webStoreState); await app4.whenReady(); if (loadExtensions) { await loadAllExtensions(session, extensionsPath, { allowUnpacked: allowUnpackedExtensions }); } if (autoUpdate) { void initUpdater(webStoreState); } } export { downloadExtension, installChromeWebStore, installExtension, loadAllExtensions, uninstallExtension, updateExtensions };