electron-chrome-web-store
Version:
Install and update Chrome extensions from the Chrome Web Store for Electron
951 lines (939 loc) • 33.1 kB
JavaScript
// 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
};