UNPKG

@iamevan/electron-chrome-web-store

Version:

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

5 lines 68.2 kB
{ "version": 3, "sources": ["../../../src/browser/index.ts", "../../../src/browser/api.ts", "../../../src/browser/utils.ts", "../../../src/common/constants.ts", "../../../src/browser/installer.ts", "../../../src/browser/crx3.ts", "../../../src/browser/id.ts", "../../../src/browser/loader.ts", "../../../src/browser/updater.ts"], "sourcesContent": ["import { app, session as electronSession } from 'electron'\nimport * as path from 'node:path'\nimport { existsSync } from 'node:fs'\nimport { createRequire } from 'node:module'\n\nimport { registerWebStoreApi } from './api'\nimport { loadAllExtensions } from './loader'\nexport { loadAllExtensions } from './loader'\nexport { installExtension, uninstallExtension, downloadExtension } from './installer'\nimport { initUpdater } from './updater'\nexport { updateExtensions } from './updater'\nimport { getDefaultExtensionsPath } from './utils'\nimport {\n BeforeInstall,\n AfterInstall,\n ExtensionId,\n WebStoreState,\n OverrideExtensionInstallStatus,\n AfterUninstall,\n CustomSetExtensionEnabled,\n} from './types'\nimport { ExtensionInstallStatus } from '../common/constants'\nexport { ExtensionInstallStatus }\n\nfunction resolvePreloadPath(modulePath?: string) {\n // Attempt to resolve preload path from module exports\n try {\n return createRequire(__dirname).resolve('electron-chrome-web-store/preload')\n } catch (error) {\n if (process.env.NODE_ENV !== 'production') {\n console.error(error)\n }\n }\n\n const preloadFilename = 'chrome-web-store.preload.js'\n\n // Deprecated: use modulePath if provided\n if (modulePath) {\n process.emitWarning(\n 'electron-chrome-web-store: \"modulePath\" is deprecated and will be removed in future versions.',\n { type: 'DeprecationWarning' },\n )\n return path.join(modulePath, 'dist', preloadFilename)\n }\n\n // Fallback to preload relative to entrypoint directory\n return path.join(__dirname, preloadFilename)\n}\n\ninterface ElectronChromeWebStoreOptions {\n /**\n * Session to enable the Chrome Web Store in.\n * Defaults to session.defaultSession\n */\n session?: Electron.Session\n\n /**\n * Path to the 'electron-chrome-web-store' module.\n *\n * @deprecated See \"Packaging the preload script\" in the readme.\n */\n modulePath?: string\n\n /**\n * Path to extensions directory.\n * Defaults to 'Extensions/' under app's userData path.\n */\n extensionsPath?: string\n\n /**\n * Load extensions installed by Chrome Web Store.\n * Defaults to true.\n */\n loadExtensions?: boolean\n\n /**\n * Whether to allow loading unpacked extensions. Only loads if\n * `loadExtensions` is also enabled.\n * Defaults to false.\n */\n allowUnpackedExtensions?: boolean\n\n /**\n * List of allowed extension IDs to install.\n */\n allowlist?: ExtensionId[]\n\n /**\n * List of denied extension IDs to install.\n */\n denylist?: ExtensionId[]\n\n /**\n * Whether extensions should auto-update.\n */\n autoUpdate?: boolean\n\n /**\n * Minimum supported version of Chrome extensions.\n * Defaults to 3.\n */\n minimumManifestVersion?: number\n\n /**\n * Called prior to installing an extension. If implemented, return a Promise\n * which resolves with `{ action: 'allow' | 'deny' }` depending on the action\n * to be taken.\n */\n beforeInstall?: BeforeInstall\n\n /**\n * Called when setting the enabled status of an extension.\n */\n customSetExtensionEnabled?: CustomSetExtensionEnabled\n\n /**\n * Called when determining the install status of an extension.\n */\n overrideExtensionInstallStatus?: OverrideExtensionInstallStatus\n\n /**\n * Called after an extension is installed.\n */\n afterInstall?: AfterInstall\n\n /**\n * Called after an extension is uninstalled.\n */\n afterUninstall?: AfterUninstall\n}\n\n/**\n * Install Chrome Web Store support.\n *\n * @param options Chrome Web Store configuration options.\n */\nexport async function installChromeWebStore(opts: ElectronChromeWebStoreOptions = {}) {\n const session = opts.session || electronSession.defaultSession\n const extensionsPath = opts.extensionsPath || getDefaultExtensionsPath()\n const loadExtensions = typeof opts.loadExtensions === 'boolean' ? opts.loadExtensions : true\n const allowUnpackedExtensions =\n typeof opts.allowUnpackedExtensions === 'boolean' ? opts.allowUnpackedExtensions : false\n const autoUpdate = typeof opts.autoUpdate === 'boolean' ? opts.autoUpdate : true\n const minimumManifestVersion =\n typeof opts.minimumManifestVersion === 'number' ? opts.minimumManifestVersion : 3\n\n const beforeInstall = typeof opts.beforeInstall === 'function' ? opts.beforeInstall : undefined\n const afterInstall = typeof opts.afterInstall === 'function' ? opts.afterInstall : undefined\n const afterUninstall = typeof opts.afterUninstall === 'function' ? opts.afterUninstall : undefined\n\n const customSetExtensionEnabled =\n typeof opts.customSetExtensionEnabled === 'function'\n ? opts.customSetExtensionEnabled\n : undefined\n\n const overrideExtensionInstallStatus =\n typeof opts.overrideExtensionInstallStatus === 'function'\n ? opts.overrideExtensionInstallStatus\n : undefined\n\n const webStoreState: WebStoreState = {\n session,\n extensionsPath,\n installing: new Set(),\n allowlist: opts.allowlist ? new Set(opts.allowlist) : undefined,\n denylist: opts.denylist ? new Set(opts.denylist) : undefined,\n minimumManifestVersion,\n beforeInstall,\n afterInstall,\n afterUninstall,\n customSetExtensionEnabled,\n overrideExtensionInstallStatus,\n }\n\n // Add preload script to session\n const preloadPath = resolvePreloadPath(opts.modulePath)\n\n if ('registerPreloadScript' in session) {\n session.registerPreloadScript({\n id: 'electron-chrome-web-store',\n type: 'frame',\n filePath: preloadPath,\n })\n } else {\n // @ts-expect-error Deprecated electron@<35\n session.setPreloads([...session.getPreloads(), preloadPath])\n }\n\n if (!existsSync(preloadPath)) {\n console.error(\n new Error(\n `electron-chrome-web-store: Preload file not found at \"${preloadPath}\". ` +\n 'See \"Packaging the preload script\" in the readme.',\n ),\n )\n }\n\n registerWebStoreApi(webStoreState)\n\n await app.whenReady()\n\n if (loadExtensions) {\n await loadAllExtensions(session, extensionsPath, { allowUnpacked: allowUnpackedExtensions })\n }\n\n if (autoUpdate) {\n void initUpdater(webStoreState)\n }\n}\n", "import * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport debug from 'debug'\nimport { app, BrowserWindow, ipcMain, nativeImage, NativeImage, Session } from 'electron'\nimport { fetch } from './utils'\n\nimport {\n ExtensionInstallStatus,\n MV2DeprecationStatus,\n Result,\n WebGlStatus,\n} from '../common/constants'\nimport { installExtension, uninstallExtension } from './installer'\nimport { ExtensionId, WebStoreState } from './types'\n\nconst d = debug('electron-chrome-web-store:api')\n\nconst WEBSTORE_URL = 'https://chromewebstore.google.com'\n\nfunction getExtensionInfo(ext: Electron.Extension) {\n const manifest: chrome.runtime.Manifest = ext.manifest\n return {\n description: manifest.description || '',\n enabled: !manifest.disabled,\n homepageUrl: manifest.homepage_url || '',\n hostPermissions: manifest.host_permissions || [],\n icons: Object.entries(manifest?.icons || {}).map(([size, url]) => ({\n size: parseInt(size),\n url: `chrome://extension-icon/${ext.id}/${size}/0`,\n })),\n id: ext.id,\n installType: 'normal',\n isApp: !!manifest.app,\n mayDisable: true,\n name: manifest.name,\n offlineEnabled: !!manifest.offline_enabled,\n optionsUrl: manifest.options_page\n ? `chrome-extension://${ext.id}/${manifest.options_page}`\n : '',\n permissions: manifest.permissions || [],\n shortName: manifest.short_name || manifest.name,\n type: manifest.app ? 'app' : 'extension',\n updateUrl: manifest.update_url || '',\n version: manifest.version,\n }\n}\n\nfunction getExtensionInstallStatus(\n state: WebStoreState,\n extensionId: ExtensionId,\n manifest?: chrome.runtime.Manifest,\n) {\n const customStatus = state.overrideExtensionInstallStatus?.(state, extensionId, manifest)\n if (customStatus) {\n return customStatus\n }\n\n if (manifest && manifest.manifest_version < state.minimumManifestVersion) {\n return ExtensionInstallStatus.DEPRECATED_MANIFEST_VERSION\n }\n\n if (state.denylist?.has(extensionId)) {\n return ExtensionInstallStatus.BLOCKED_BY_POLICY\n }\n\n if (state.allowlist && !state.allowlist.has(extensionId)) {\n return ExtensionInstallStatus.BLOCKED_BY_POLICY\n }\n\n const extensions = state.session.getAllExtensions()\n const extension = extensions.find((ext) => ext.id === extensionId)\n\n if (!extension) {\n return ExtensionInstallStatus.INSTALLABLE\n }\n\n if (extension.manifest.disabled) {\n return ExtensionInstallStatus.DISABLED\n }\n\n return ExtensionInstallStatus.ENABLED\n}\n\ninterface InstallDetails {\n id: string\n manifest: string\n localizedName: string\n esbAllowlist: boolean\n iconUrl: string\n}\n\nasync function beginInstall(\n { sender, senderFrame }: Electron.IpcMainInvokeEvent,\n state: WebStoreState,\n details: InstallDetails,\n) {\n const extensionId = details.id\n\n try {\n if (state.installing.has(extensionId)) {\n return { result: Result.INSTALL_IN_PROGRESS }\n }\n\n let manifest: chrome.runtime.Manifest\n try {\n manifest = JSON.parse(details.manifest)\n } catch {\n return { result: Result.MANIFEST_ERROR }\n }\n\n const installStatus = getExtensionInstallStatus(state, extensionId, manifest)\n switch (installStatus) {\n case ExtensionInstallStatus.INSTALLABLE:\n break // good to go\n case ExtensionInstallStatus.BLOCKED_BY_POLICY:\n return { result: Result.BLOCKED_BY_POLICY }\n default: {\n d('unable to install extension %s with status \"%s\"', extensionId, installStatus)\n return { result: Result.UNKNOWN_ERROR }\n }\n }\n\n let iconUrl: URL\n try {\n iconUrl = new URL(details.iconUrl)\n } catch {\n return { result: Result.INVALID_ICON_URL }\n }\n\n let icon: NativeImage\n try {\n const response = await fetch(iconUrl.href)\n const imageBuffer = Buffer.from(await response.arrayBuffer())\n icon = nativeImage.createFromBuffer(imageBuffer)\n } catch {\n return { result: Result.ICON_ERROR }\n }\n\n const browserWindow = BrowserWindow.fromWebContents(sender)\n if (!senderFrame || senderFrame.isDestroyed()) {\n return { result: Result.UNKNOWN_ERROR }\n }\n\n if (state.beforeInstall) {\n const result: unknown = await state.beforeInstall({\n id: extensionId,\n localizedName: details.localizedName,\n manifest,\n icon,\n frame: senderFrame,\n browserWindow: browserWindow || undefined,\n })\n\n if (typeof result !== 'object' || typeof (result as any).action !== 'string') {\n return { result: Result.UNKNOWN_ERROR }\n } else if ((result as any).action !== 'allow') {\n return { result: Result.USER_CANCELLED }\n }\n }\n\n state.installing.add(extensionId)\n await installExtension(extensionId, state)\n\n if (state.afterInstall) {\n // Doesn't need to await, just a callback\n state.afterInstall({\n id: extensionId,\n localizedName: details.localizedName,\n manifest,\n icon,\n frame: senderFrame,\n browserWindow: browserWindow || undefined,\n })\n }\n\n return { result: Result.SUCCESS }\n } catch (error) {\n console.error('Extension installation failed:', error)\n return {\n result: Result.INSTALL_ERROR,\n message: error instanceof Error ? error.message : String(error),\n }\n } finally {\n state.installing.delete(extensionId)\n }\n}\n\ntype IPCChannelHandler = (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any\nconst handledIpcChannels = new Map<string, Map<Session, IPCChannelHandler>>()\n\nexport function registerWebStoreApi(webStoreState: WebStoreState) {\n /** Handle IPCs from the Chrome Web Store. */\n const handle = (\n channel: string,\n handle: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any,\n ) => {\n let handlersMap = handledIpcChannels.get(channel)\n\n // Handle each channel only once\n if (!handlersMap) {\n handlersMap = new Map<Session, IPCChannelHandler>()\n handledIpcChannels.set(channel, handlersMap)\n\n ipcMain.handle(channel, async function handleWebStoreIpc(event, ...args) {\n d('received %s', channel)\n\n const senderOrigin = event.senderFrame?.origin\n if (!senderOrigin || !senderOrigin.startsWith(WEBSTORE_URL)) {\n d('ignoring webstore request from %s', senderOrigin)\n return\n }\n\n const session = event.sender.session\n\n const handler = handlersMap?.get(session)\n if (!handler) {\n d('no handler for session %s', session.storagePath)\n return\n }\n\n const result = await handler(event, ...args)\n d('%s result', channel, result)\n return result\n })\n }\n\n // Add handler\n handlersMap.set(webStoreState.session, handle)\n }\n\n handle('chromeWebstore.beginInstall', async (event, details: InstallDetails) => {\n const { senderFrame } = event\n\n d('beginInstall', details)\n\n const result = await beginInstall(event, webStoreState, details)\n\n if (result.result === Result.SUCCESS) {\n queueMicrotask(() => {\n const ext = webStoreState.session.getExtension(details.id)\n if (ext && senderFrame && !senderFrame.isDestroyed()) {\n try {\n senderFrame.send('chrome.management.onInstalled', getExtensionInfo(ext))\n } catch (error) {\n console.error(error)\n }\n }\n })\n }\n\n return result\n })\n\n handle('chromeWebstore.completeInstall', async (event, id) => {\n // TODO: Implement completion of extension installation\n return Result.SUCCESS\n })\n\n handle('chromeWebstore.enableAppLauncher', async (event, enable) => {\n // TODO: Implement app launcher enable/disable\n return true\n })\n\n handle('chromeWebstore.getBrowserLogin', async () => {\n // TODO: Implement getting browser login\n return ''\n })\n handle('chromeWebstore.getExtensionStatus', async (_event, id, manifestJson) => {\n const manifest = JSON.parse(manifestJson)\n return getExtensionInstallStatus(webStoreState, id, manifest)\n })\n\n handle('chromeWebstore.getFullChromeVersion', async () => {\n return {\n version_number: process.versions.chrome,\n app_name: app.getName(),\n }\n })\n\n handle('chromeWebstore.getIsLauncherEnabled', async () => {\n // TODO: Implement checking if launcher is enabled\n return true\n })\n\n handle('chromeWebstore.getMV2DeprecationStatus', async () => {\n return webStoreState.minimumManifestVersion > 2\n ? MV2DeprecationStatus.SOFT_DISABLE\n : MV2DeprecationStatus.INACTIVE\n })\n\n handle('chromeWebstore.getReferrerChain', async () => {\n // TODO: Implement getting referrer chain\n return 'EgIIAA=='\n })\n\n handle('chromeWebstore.getStoreLogin', async () => {\n // TODO: Implement getting store login\n return ''\n })\n\n handle('chromeWebstore.getWebGLStatus', async () => {\n await app.getGPUInfo('basic')\n const features = app.getGPUFeatureStatus()\n return features.webgl.startsWith('enabled')\n ? WebGlStatus.WEBGL_ALLOWED\n : WebGlStatus.WEBGL_BLOCKED\n })\n\n handle('chromeWebstore.install', async (event, id, silentInstall) => {\n // TODO: Implement extension installation\n return Result.SUCCESS\n })\n\n handle('chromeWebstore.isInIncognitoMode', async () => {\n // TODO: Implement incognito mode check\n return false\n })\n\n handle('chromeWebstore.isPendingCustodianApproval', async (event, id) => {\n // TODO: Implement custodian approval check\n return false\n })\n\n handle('chromeWebstore.setStoreLogin', async (event, login) => {\n // TODO: Implement setting store login\n return true\n })\n\n handle('chrome.runtime.getManifest', async () => {\n // TODO: Implement getting extension manifest\n return {}\n })\n\n handle('chrome.management.getAll', async (event) => {\n const extensions = webStoreState.session.getAllExtensions()\n return extensions.map(getExtensionInfo)\n })\n\n handle('chrome.management.setEnabled', async (event, id, enabled) => {\n // TODO: Implement enabling/disabling extension\n if (webStoreState.customSetExtensionEnabled) {\n await webStoreState.customSetExtensionEnabled(webStoreState, id, enabled)\n }\n return true\n })\n\n handle(\n 'chrome.management.uninstall',\n async (event, id, options: { showConfirmDialog: boolean }) => {\n if (options?.showConfirmDialog) {\n // TODO: confirmation dialog\n }\n\n try {\n await uninstallExtension(id, webStoreState)\n queueMicrotask(() => {\n event.sender.send('chrome.management.onUninstalled', id)\n })\n return Result.SUCCESS\n } catch (error) {\n console.error(error)\n return Result.UNKNOWN_ERROR\n }\n },\n )\n}\n", "import * as path from 'node:path'\nimport { app, net } from 'electron'\n\n// Include fallbacks for node environments that aren't Electron\nexport const fetch =\n // Prefer Node's fetch until net.fetch crash is fixed\n // https://github.com/electron/electron/pull/45050\n globalThis.fetch ||\n net?.fetch ||\n (() => {\n throw new Error(\n 'electron-chrome-web-store: Missing fetch API. Please upgrade Electron or Node.',\n )\n })\nexport const getChromeVersion = () => process.versions.chrome || '131.0.6778.109'\n\nexport function compareVersions(version1: string, version2: string) {\n const v1 = version1.split('.').map(Number)\n const v2 = version2.split('.').map(Number)\n\n for (let i = 0; i < 3; i++) {\n if (v1[i] > v2[i]) return 1\n if (v1[i] < v2[i]) return -1\n }\n return 0\n}\n\nexport const getDefaultExtensionsPath = () => path.join(app.getPath('userData'), 'Extensions')\n", "export const ExtensionInstallStatus = {\n BLACKLISTED: 'blacklisted',\n BLOCKED_BY_POLICY: 'blocked_by_policy',\n CAN_REQUEST: 'can_request',\n CORRUPTED: 'corrupted',\n CUSTODIAN_APPROVAL_REQUIRED: 'custodian_approval_required',\n CUSTODIAN_APPROVAL_REQUIRED_FOR_INSTALLATION: 'custodian_approval_required_for_installation',\n DEPRECATED_MANIFEST_VERSION: 'deprecated_manifest_version',\n DISABLED: 'disabled',\n ENABLED: 'enabled',\n FORCE_INSTALLED: 'force_installed',\n INSTALLABLE: 'installable',\n REQUEST_PENDING: 'request_pending',\n TERMINATED: 'terminated',\n}\n\nexport const MV2DeprecationStatus = {\n INACTIVE: 'inactive',\n SOFT_DISABLE: 'soft_disable',\n WARNING: 'warning',\n}\n\nexport const Result = {\n ALREADY_INSTALLED: 'already_installed',\n BLACKLISTED: 'blacklisted',\n BLOCKED_BY_POLICY: 'blocked_by_policy',\n BLOCKED_FOR_CHILD_ACCOUNT: 'blocked_for_child_account',\n FEATURE_DISABLED: 'feature_disabled',\n ICON_ERROR: 'icon_error',\n INSTALL_ERROR: 'install_error',\n INSTALL_IN_PROGRESS: 'install_in_progress',\n INVALID_ICON_URL: 'invalid_icon_url',\n INVALID_ID: 'invalid_id',\n LAUNCH_IN_PROGRESS: 'launch_in_progress',\n MANIFEST_ERROR: 'manifest_error',\n MISSING_DEPENDENCIES: 'missing_dependencies',\n SUCCESS: 'success',\n UNKNOWN_ERROR: 'unknown_error',\n UNSUPPORTED_EXTENSION_TYPE: 'unsupported_extension_type',\n USER_CANCELLED: 'user_cancelled',\n USER_GESTURE_REQUIRED: 'user_gesture_required',\n}\n\nexport const WebGlStatus = {\n WEBGL_ALLOWED: 'webgl_allowed',\n WEBGL_BLOCKED: 'webgl_blocked',\n}\n", "import * as fs from 'node:fs'\nimport * as os from 'node:os'\nimport * as path from 'node:path'\nimport { Readable } from 'node:stream'\nimport { pipeline } from 'node:stream/promises'\nimport { session as electronSession } from 'electron'\n\nimport AdmZip from 'adm-zip'\nimport debug from 'debug'\nimport Pbf from 'pbf'\n\nimport { readCrxFileHeader, readSignedData } from './crx3'\nimport { convertHexadecimalToIDAlphabet, generateId } from './id'\nimport { fetch, getChromeVersion, getDefaultExtensionsPath } from './utils'\nimport { findExtensionInstall } from './loader'\nimport { AfterUninstall, ExtensionId } from './types'\n\nconst d = debug('electron-chrome-web-store:installer')\n\nfunction getExtensionCrxURL(extensionId: ExtensionId) {\n const url = new URL('https://clients2.google.com/service/update2/crx')\n url.searchParams.append('response', 'redirect')\n url.searchParams.append('acceptformat', ['crx2', 'crx3'].join(','))\n\n const x = new URLSearchParams()\n x.append('id', extensionId)\n x.append('uc', '')\n\n url.searchParams.append('x', x.toString())\n url.searchParams.append('prodversion', getChromeVersion())\n\n return url.toString()\n}\n\ninterface CrxInfo {\n extensionId: string\n version: number\n header: Buffer\n contents: Buffer\n publicKey: Buffer\n}\n\n// Parse CRX header and extract contents\nfunction parseCrx(buffer: Buffer): CrxInfo {\n // CRX3 magic number: 'Cr24'\n const magicNumber = buffer.toString('utf8', 0, 4)\n if (magicNumber !== 'Cr24') {\n throw new Error('Invalid CRX format')\n }\n\n // CRX3 format has version = 3 and header size at bytes 8-12\n const version = buffer.readUInt32LE(4)\n const headerSize = buffer.readUInt32LE(8)\n\n // Extract header and contents\n const header = buffer.subarray(12, 12 + headerSize)\n const contents = buffer.subarray(12 + headerSize)\n\n let extensionId: string\n let publicKey: Buffer\n\n // For CRX2 format\n if (version === 2) {\n const pubKeyLength = buffer.readUInt32LE(8)\n const sigLength = buffer.readUInt32LE(12)\n publicKey = buffer.subarray(16, 16 + pubKeyLength)\n extensionId = generateId(publicKey.toString('base64'))\n } else {\n // For CRX3, extract public key from header\n // CRX3 header contains a protocol buffer message\n const crxFileHeader = readCrxFileHeader(new Pbf(header))\n const crxSignedData = readSignedData(new Pbf(crxFileHeader.signed_header_data))\n const declaredCrxId = crxSignedData.crx_id\n ? convertHexadecimalToIDAlphabet(crxSignedData.crx_id.toString('hex'))\n : null\n\n if (!declaredCrxId) {\n throw new Error('Invalid CRX signed data')\n }\n\n // Need to find store key proof which matches the declared ID\n const keyProof = crxFileHeader.sha256_with_rsa.find((proof) => {\n const crxId = proof.public_key ? generateId(proof.public_key.toString('base64')) : null\n return crxId === declaredCrxId\n })\n\n if (!keyProof) {\n throw new Error('Invalid CRX key')\n }\n\n extensionId = declaredCrxId\n publicKey = keyProof.public_key\n }\n\n return {\n extensionId,\n version,\n header,\n contents,\n publicKey,\n }\n}\n\n// Extract CRX contents and update manifest\nasync function unpackCrx(crx: CrxInfo, destPath: string): Promise<chrome.runtime.Manifest> {\n // Create zip file from contents\n const zip = new AdmZip(crx.contents)\n\n // Extract zip to destination\n zip.extractAllTo(destPath, true)\n\n // Read manifest.json\n const manifestPath = path.join(destPath, 'manifest.json')\n const manifestContent = await fs.promises.readFile(manifestPath, 'utf8')\n const manifest = JSON.parse(manifestContent) as chrome.runtime.Manifest\n\n // Add public key to manifest\n manifest.key = crx.publicKey.toString('base64')\n\n // Write updated manifest back\n await fs.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2))\n\n return manifest\n}\n\nasync function readCrx(crxPath: string) {\n const crxBuffer = await fs.promises.readFile(crxPath)\n return parseCrx(crxBuffer)\n}\n\nasync function downloadCrx(url: string, dest: string) {\n const response = await fetch(url)\n if (!response.ok) {\n throw new Error('Failed to download extension')\n }\n\n const fileStream = fs.createWriteStream(dest)\n const downloadStream = Readable.fromWeb(response.body as any)\n await pipeline(downloadStream, fileStream)\n}\n\nexport async function downloadExtensionFromURL(\n url: string,\n extensionsDir: string,\n expectedExtensionId?: string,\n): Promise<string> {\n d('downloading %s', url)\n\n const installUuid = crypto.randomUUID()\n const crxPath = path.join(os.tmpdir(), `electron-cws-download_${installUuid}.crx`)\n try {\n await downloadCrx(url, crxPath)\n\n const crx = await readCrx(crxPath)\n\n if (expectedExtensionId && expectedExtensionId !== crx.extensionId) {\n throw new Error(\n `CRX mismatches expected extension ID: ${expectedExtensionId} !== ${crx.extensionId}`,\n )\n }\n\n const unpackedPath = path.join(extensionsDir, crx.extensionId, installUuid)\n await fs.promises.mkdir(unpackedPath, { recursive: true })\n const manifest = await unpackCrx(crx, unpackedPath)\n\n if (!manifest.version) {\n throw new Error('Installed extension is missing manifest version')\n }\n\n const versionedPath = path.join(extensionsDir, crx.extensionId, `${manifest.version}_0`)\n await fs.promises.rename(unpackedPath, versionedPath)\n\n return versionedPath\n } finally {\n await fs.promises.rm(crxPath, { force: true })\n }\n}\n\n/**\n * Download and unpack extension to the given extensions directory.\n */\nexport async function downloadExtension(\n extensionId: string,\n extensionsDir: string,\n): Promise<string> {\n const url = getExtensionCrxURL(extensionId)\n return await downloadExtensionFromURL(url, extensionsDir, extensionId)\n}\n\ninterface CommonExtensionOptions {\n /** Session to load extensions into. */\n session?: Electron.Session\n\n /**\n * Directory where web store extensions will be installed.\n * Defaults to `Extensions` under the app's `userData` directory.\n */\n extensionsPath?: string\n}\n\ninterface InstallExtensionOptions extends CommonExtensionOptions {\n /** Options for loading the extension. */\n loadExtensionOptions?: Electron.LoadExtensionOptions\n}\n\ninterface UninstallExtensionOptions extends CommonExtensionOptions {\n /** Called after an extension is uninstalled. */\n afterUninstall?: AfterUninstall\n}\n\n/**\n * Install extension from the web store.\n */\nexport async function installExtension(\n extensionId: string,\n opts: InstallExtensionOptions = {},\n): Promise<Electron.Extension> {\n d('installing %s', extensionId)\n\n const session = opts.session || electronSession.defaultSession\n const extensionsPath = opts.extensionsPath || getDefaultExtensionsPath()\n\n // Check if already loaded\n const existingExtension = session.getExtension(extensionId)\n if (existingExtension) {\n d('%s already loaded', extensionId)\n return existingExtension\n }\n\n // Check if already installed\n const existingExtensionInfo = await findExtensionInstall(extensionId, extensionsPath)\n if (existingExtensionInfo && existingExtensionInfo.type === 'store') {\n d('%s already installed', extensionId)\n return await session.loadExtension(existingExtensionInfo.path, opts.loadExtensionOptions)\n }\n\n // Download and load new extension\n const extensionPath = await downloadExtension(extensionId, extensionsPath)\n const extension = await session.loadExtension(extensionPath, opts.loadExtensionOptions)\n d('installed %s', extensionId)\n\n return extension\n}\n\n/**\n * Uninstall extension from the web store.\n */\nexport async function uninstallExtension(\n extensionId: string,\n opts: UninstallExtensionOptions = {},\n) {\n d('uninstalling %s', extensionId)\n\n const session = opts.session || electronSession.defaultSession\n const extensionsPath = opts.extensionsPath || getDefaultExtensionsPath()\n\n const extensions = session.getAllExtensions()\n const existingExt = extensions.find((ext) => ext.id === extensionId)\n if (existingExt) {\n session.removeExtension(extensionId)\n }\n\n if (opts.afterUninstall) {\n await opts.afterUninstall({\n id: extensionId,\n extension: existingExt,\n manifest: existingExt?.manifest,\n })\n }\n\n const extensionDir = path.join(extensionsPath, extensionId)\n try {\n const stat = await fs.promises.stat(extensionDir)\n if (stat.isDirectory()) {\n await fs.promises.rm(extensionDir, { recursive: true, force: true })\n }\n } catch (error: any) {\n if (error?.code !== 'ENOENT') {\n throw error\n }\n }\n}\n", "// code generated by pbf v4.0.1\n// modified for electron-chrome-web-store\n\nimport Pbf from 'pbf'\n\ninterface AsymmetricKeyProof {\n public_key: Buffer\n signature: Buffer\n}\n\ninterface CrxFileHeader {\n sha256_with_rsa: AsymmetricKeyProof[]\n sha256_with_ecdsa: AsymmetricKeyProof[]\n verified_contents?: Buffer\n signed_header_data?: Buffer\n}\n\nexport function readCrxFileHeader(pbf: Pbf, end?: any): CrxFileHeader {\n return pbf.readFields(\n readCrxFileHeaderField,\n {\n sha256_with_rsa: [],\n sha256_with_ecdsa: [],\n verified_contents: undefined,\n signed_header_data: undefined,\n },\n end,\n )\n}\nfunction readCrxFileHeaderField(tag: any, obj: any, pbf: Pbf) {\n if (tag === 2) obj.sha256_with_rsa.push(readAsymmetricKeyProof(pbf, pbf.readVarint() + pbf.pos))\n else if (tag === 3)\n obj.sha256_with_ecdsa.push(readAsymmetricKeyProof(pbf, pbf.readVarint() + pbf.pos))\n else if (tag === 4) obj.verified_contents = pbf.readBytes()\n else if (tag === 10000) obj.signed_header_data = pbf.readBytes()\n}\n\nexport function readAsymmetricKeyProof(pbf: Pbf, end: any) {\n return pbf.readFields(\n readAsymmetricKeyProofField,\n { public_key: undefined, signature: undefined },\n end,\n )\n}\nfunction readAsymmetricKeyProofField(tag: any, obj: any, pbf: Pbf) {\n if (tag === 1) obj.public_key = pbf.readBytes()\n else if (tag === 2) obj.signature = pbf.readBytes()\n}\n\nexport function readSignedData(pbf: Pbf, end?: any): { crx_id?: Buffer } {\n return pbf.readFields(readSignedDataField, { crx_id: undefined }, end)\n}\nfunction readSignedDataField(tag: any, obj: any, pbf: Pbf) {\n if (tag === 1) obj.crx_id = pbf.readBytes()\n}\n", "import { createHash } from 'node:crypto'\n\n/**\n * Converts a normal hexadecimal string into the alphabet used by extensions.\n * We use the characters 'a'-'p' instead of '0'-'f' to avoid ever having a\n * completely numeric host, since some software interprets that as an IP address.\n *\n * @param id - The hexadecimal string to convert. This is modified in place.\n */\nexport function convertHexadecimalToIDAlphabet(id: string) {\n let result = ''\n for (const ch of id) {\n const val = parseInt(ch, 16)\n if (!isNaN(val)) {\n result += String.fromCharCode('a'.charCodeAt(0) + val)\n } else {\n result += 'a'\n }\n }\n return result\n}\n\nfunction generateIdFromHash(hash: Buffer): string {\n const hashedId = hash.subarray(0, 16).toString('hex')\n return convertHexadecimalToIDAlphabet(hashedId)\n}\n\nexport function generateId(input: string): string {\n const hash = createHash('sha256').update(input, 'base64').digest()\n return generateIdFromHash(hash)\n}\n", "import * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport debug from 'debug'\n\nimport { generateId } from './id'\nimport { compareVersions } from './utils'\nimport { ExtensionId } from './types'\n\nconst d = debug('electron-chrome-web-store:loader')\n\ntype ExtensionPathBaseInfo = { manifest: chrome.runtime.Manifest; path: string }\ntype ExtensionPathInfo =\n | ({ type: 'store'; id: string } & ExtensionPathBaseInfo)\n | ({ type: 'unpacked' } & ExtensionPathBaseInfo)\n\nconst manifestExists = async (dirPath: string) => {\n if (!dirPath) return false\n const manifestPath = path.join(dirPath, 'manifest.json')\n try {\n return (await fs.promises.stat(manifestPath)).isFile()\n } catch {\n return false\n }\n}\n\n/**\n * DFS directories for extension manifests.\n */\nasync function extensionSearch(dirPath: string, depth: number = 0): Promise<string[]> {\n if (depth >= 2) return []\n const results = []\n const dirEntries = await fs.promises.readdir(dirPath, { withFileTypes: true })\n for (const entry of dirEntries) {\n if (entry.isDirectory()) {\n if (await manifestExists(path.join(dirPath, entry.name))) {\n results.push(path.join(dirPath, entry.name))\n } else {\n results.push(...(await extensionSearch(path.join(dirPath, entry.name), depth + 1)))\n }\n }\n }\n return results\n}\n\n/**\n * Discover list of extensions in the given path.\n */\nasync function discoverExtensions(extensionsPath: string): Promise<ExtensionPathInfo[]> {\n try {\n const stat = await fs.promises.stat(extensionsPath)\n if (!stat.isDirectory()) {\n d('%s is not a directory', extensionsPath)\n return []\n }\n } catch {\n d('%s does not exist', extensionsPath)\n return []\n }\n\n const extensionDirectories = await extensionSearch(extensionsPath)\n const results: ExtensionPathInfo[] = []\n\n for (const extPath of extensionDirectories.filter(Boolean)) {\n try {\n const manifestPath = path.join(extPath!, 'manifest.json')\n const manifestJson = (await fs.promises.readFile(manifestPath)).toString()\n const manifest: chrome.runtime.Manifest = JSON.parse(manifestJson)\n const result = manifest.key\n ? {\n type: 'store' as const,\n path: extPath!,\n manifest,\n id: generateId(manifest.key),\n }\n : {\n type: 'unpacked' as const,\n path: extPath!,\n manifest,\n }\n results.push(result)\n } catch (e) {\n console.error(e)\n }\n }\n\n return results\n}\n\n/**\n * Filter any outdated extensions in the case of duplicate installations.\n */\nfunction filterOutdatedExtensions(extensions: ExtensionPathInfo[]): ExtensionPathInfo[] {\n const uniqueExtensions: ExtensionPathInfo[] = []\n const storeExtMap = new Map<ExtensionId, ExtensionPathInfo>()\n\n for (const ext of extensions) {\n if (ext.type === 'unpacked') {\n // Unpacked extensions are always unique to their path\n uniqueExtensions.push(ext)\n } else if (!storeExtMap.has(ext.id)) {\n // New store extension\n storeExtMap.set(ext.id, ext)\n } else {\n // Existing store extension, compare with existing version\n const latestExt = storeExtMap.get(ext.id)!\n if (compareVersions(latestExt.manifest.version, ext.manifest.version) < 0) {\n storeExtMap.set(ext.id, ext)\n }\n }\n }\n\n // Append up to date store extensions\n storeExtMap.forEach((ext) => uniqueExtensions.push(ext))\n\n return uniqueExtensions\n}\n\n/**\n * Load all extensions from the given directory.\n */\nexport async function loadAllExtensions(\n session: Electron.Session,\n extensionsPath: string,\n options: {\n allowUnpacked?: boolean\n } = {},\n) {\n let extensions = await discoverExtensions(extensionsPath)\n extensions = filterOutdatedExtensions(extensions)\n d('discovered %d extension(s) in %s', extensions.length, extensionsPath)\n\n for (const ext of extensions) {\n try {\n let extension: Electron.Extension | undefined\n if (ext.type === 'store') {\n const existingExt = session.getExtension(ext.id)\n if (existingExt) {\n d('skipping loading existing extension %s', ext.id)\n continue\n }\n d('loading extension %s', `${ext.id}@${ext.manifest.version}`)\n extension = await session.loadExtension(ext.path)\n } else if (options.allowUnpacked) {\n d('loading unpacked extension %s', ext.path)\n extension = await session.loadExtension(ext.path)\n }\n\n if (\n extension &&\n extension.manifest.manifest_version === 3 &&\n extension.manifest.background?.service_worker\n ) {\n const scope = `chrome-extension://${extension.id}`\n await session.serviceWorkers.startWorkerForScope(scope).catch(() => {\n console.error(`Failed to start worker for extension ${extension.id}`)\n })\n }\n } catch (error) {\n console.error(`Failed to load extension from ${ext.path}`)\n console.error(error)\n }\n }\n}\n\nexport async function findExtensionInstall(extensionId: string, extensionsPath: string) {\n const extensionPath = path.join(extensionsPath, extensionId)\n let extensions = await discoverExtensions(extensionPath)\n extensions = filterOutdatedExtensions(extensions)\n return extensions.length > 0 ? extensions[0] : null\n}\n", "import * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport debug from 'debug'\nimport { app, powerMonitor, session as electronSession } from 'electron'\n\nimport { compareVersions, fetch, getChromeVersion } from './utils'\nimport { downloadExtensionFromURL } from './installer'\nimport { WebStoreState } from './types'\n\nconst d = debug('electron-chrome-web-store:updater')\n\ninterface OmahaResponseBody {\n response: {\n server: string\n protocol: string\n daystart: {\n elapsed_seconds: number\n elapsed_days: number\n }\n app: Array<{\n appid: string\n cohort: string\n status: string\n cohortname: string\n updatecheck: {\n _esbAllowlist: string\n status:\n | 'ok'\n | 'noupdate'\n | 'error-internal'\n | 'error-hash'\n | 'error-osnotsupported'\n | 'error-hwnotsupported'\n | 'error-unsupportedprotocol'\n urls?: {\n url: Array<{\n codebase: string\n }>\n }\n manifest?: {\n version: string\n packages: {\n package: Array<{\n hash_sha256: string\n size: number\n name: string\n fp: string\n required: boolean\n }>\n }\n }\n }\n }>\n }\n}\n\ntype ExtensionUpdate = {\n extension: Electron.Extension\n id: string\n name: string\n version: string\n url: string\n}\n\nconst SYSTEM_IDLE_DURATION = 1 * 60 * 60 * 1000 // 1 hour\nconst UPDATE_CHECK_INTERVAL = 5 * 60 * 60 * 1000 // 5 hours\nconst MIN_UPDATE_INTERVAL = 3 * 60 * 60 * 1000 // 3 hours\n\n/** Time of last update check */\nlet lastUpdateCheck: number | undefined\n\n/**\n * Updates are limited to certain URLs for the initial implementation.\n */\nconst ALLOWED_UPDATE_URLS = new Set(['https://clients2.google.com/service/update2/crx'])\n\nconst getSessionId = (() => {\n let sessionId: string\n return () => sessionId || (sessionId = crypto.randomUUID())\n})()\n\nconst getOmahaPlatform = (): string => {\n switch (process.platform) {\n case 'win32':\n return 'win'\n case 'darwin':\n return 'mac'\n default:\n return process.platform\n }\n}\n\nconst getOmahaArch = (): string => {\n switch (process.arch) {\n case 'ia32':\n return 'x86'\n case 'x64':\n return 'x64'\n default:\n return process.arch\n }\n}\n\nfunction filterWebStoreExtension(extension: Electron.Extension) {\n const manifest = extension.manifest as chrome.runtime.Manifest\n if (!manifest) return false\n // TODO: implement extension.isFromStore() to check creation flags\n return manifest.key && manifest.update_url && ALLOWED_UPDATE_URLS.has(manifest.update_url)\n}\n\nasync function fetchAvailableUpdates(extensions: Electron.Extension[]): Promise<ExtensionUpdate[]> {\n if (extensions.length === 0) return []\n\n const extensionIds = extensions.map((extension) => extension.id)\n const extensionMap: Record<string, Electron.Extension> = extensions.reduce(\n (map, ext) => ({\n ...map,\n [ext.id]: ext,\n }),\n {},\n )\n\n const chromeVersion = getChromeVersion()\n const url = 'https://update.googleapis.com/service/update2/json'\n\n // Chrome's extension updater uses its Omaha Protocol.\n // https://chromium.googlesource.com/chromium/src/+/main/docs/updater/protocol_3_1.md\n const body = {\n request: {\n '@updater': 'electron-chrome-web-store',\n acceptformat: 'crx3',\n app: [\n ...extensions.map((extension) => ({\n appid: extension.id,\n updatecheck: {},\n // API always reports 'noupdate' when version is set :thinking:\n // version: extension.version,\n })),\n ],\n os: {\n platform: getOmahaPlatform(),\n arch: getOmahaArch(),\n },\n prodversion: chromeVersion,\n protocol: '3.1',\n requestid: crypto.randomUUID(),\n sessionid: getSessionId(),\n testsource: process.env.NODE_ENV === 'production' ? '' : 'electron_dev',\n },\n }\n\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'X-Goog-Update-Interactivity': 'bg',\n 'X-Goog-Update-AppId': extensionIds.join(','),\n 'X-Goog-Update-Updater': `chromiumcrx-${chromeVersion}`,\n },\n body: JSON.stringify(body),\n })\n\n if (!response.ok) {\n d('update response not ok')\n return []\n }\n\n // Skip safe JSON prefix\n const text = await response.text()\n const prefix = `)]}'\\n`\n if (!text.startsWith(prefix)) {\n d('unexpected update response: %s', text)\n return []\n }\n\n const json = text.substring(prefix.length)\n const result: OmahaResponseBody = JSON.parse(json)\n\n let updates: ExtensionUpdate[]\n try {\n const apps = result?.response?.app || []\n updates = apps\n // Find extensions with update\n .filter((app) => app.updatecheck.status === 'ok')\n // Collect info\n .map((app) => {\n const extensionId = app.appid\n const extension = extensionMap[extensionId]\n const manifest = app.updatecheck.manifest!\n const pkg = manifest!.packages.package[0]\n return {\n extension,\n id: extensionId,\n version: manifest.version,\n name: pkg.name,\n url: app.updatecheck.urls!.url[0].codebase,\n }\n })\n // Remove extensions without newer version\n .filter((update) => {\n const extension = extensionMap[update.id]\n return compareVersions(extension.version, update.version) < 0\n })\n } catch (error) {\n console.error('Unable to read extension updates response', error)\n return []\n }\n\n return updates\n}\n\nasync function updateExtension(session: Electron.Session, update: ExtensionUpdate) {\n const extensionId = update.id\n const oldExtension = update.extension\n d('updating %s %s -> %s', extensionId, oldExtension.version, update.version)\n\n // Updates must be installed in adjacent directories. Ensure the old install\n // was contained in a versioned directory structure.\n const oldVersionDirectoryName = path.basename(oldExtension.path)\n if (!oldVersionDirectoryName.startsWith(oldExtension.version)) {\n console.error(\n `updateExtension: extension ${extensionId} must conform to versioned directory names`,\n {\n oldPath: oldExtension.path,\n },\n )\n d('skipping %s update due to invalid install path %s', extensionId, oldExtension.path)\n return\n }\n\n // Download update\n const extensionsPath = path.join(oldExtension.path, '..', '..')\n const updatePath = await downloadExtensionFromURL(update.url, extensionsPath, extensionId)\n d('downloaded update %s@%s', extensionId, update.version)\n\n // Reload extension if already loaded\n if (session.getExtension(extensionId)) {\n session.removeExtension(extensionId)\n await session.loadExtension(updatePath)\n d('loaded update %s@%s', extensionId, update.version)\n }\n\n // Remove old version\n await fs.promises.rm(oldExtension.path, { recursive: true, force: true })\n}\n\nasync function checkForUpdates(session: Electron.Session) {\n // Only check for extensions from the store\n const extensions = session.getAllExtensions().filter(filterWebStoreExtension)\n d('checking for updates: %s', extensions.map((ext) => `${ext.id}@${ext.version}`).join(','))\n\n const updates = await fetchAvailableUpdates(extensions)\n if (!updates || updates.length === 0) {\n d('no updates found')\n return []\n }\n\n return updates\n}\n\nasync function installUpdates(session: Electron.Session, updates: ExtensionUpdate[]) {\n d('updating %d extension(s)', updates.length)\n for (const update of updates) {\n try {\n await updateExtension(session, update)\n } catch (error) {\n console.error(`checkForUpdates: Error updating extension ${update.id}`)\n console.error(error)\n }\n }\n}\n\n/**\n * Check session's loaded extensions for updates and install any if available.\n */\nexport async function updateExtensions(\n session: Electron.Session = electronSession.defaultSession,\n): Promise<void> {\n const updates = await checkForUpdates(session)\n if (updates.length > 0) {\n await installUpdates(session, updates)\n }\n}\n\nasync function maybeCheckForUpdates(session: Electron.Session) {\n const idleState = powerMonitor.getSystemIdleState(SYSTEM_IDLE_DURATION)\n if (idleState !== 'active') {\n d('skipping update check while system is in \"%s\" idle state', idleState)\n return\n }\n\n // Determine if enough time has passed to check updates\n if (lastUpdateCheck && Date.now() - lastUpdateCheck < MIN_UPDATE_INTERVAL) {\n return\n }\n lastUpdateCheck = Date.now()\n\n void updateExtensions(session)\n}\n\nexport async function initUpdater(state: WebStoreState) {\n const check = () => maybeCheckForUpdates(state.session)\n\n switch (process.platform) {\n case 'darwin':\n app.on('did-become-active', check)\n break\n case 'win32':\n case 'linux':\n app.on('browser-window-focus', check)\n break\n }\n\n const updateIntervalId = setInterval(check, UPDATE_CHECK_INTERVAL)\n check()\n\n app.on('before-quit', (event) => {\n queueMicrotask(() => {\n if (!event.defaultPrevented) {\n d('stopping update checks')\n clearInterval(updateIntervalId)\n }\n })\n })\n}\n"], "mappings": "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