UNPKG

basic-electron-updater

Version:

A secure, cross-platform auto-update library for Electron Forge apps using GitHub Releases.

287 lines (286 loc) 12.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const electron_1 = require("electron"); const events_1 = require("./events"); const GitHubProvider_1 = require("./github/GitHubProvider"); const config_1 = require("./config"); const Downloader_1 = require("./download/Downloader"); const os = __importStar(require("os")); const path = __importStar(require("path")); const windows_1 = require("./platform/windows"); const macos_1 = require("./platform/macos"); const linux_1 = require("./platform/linux"); const semver = __importStar(require("semver")); /** * Updater provides secure, cross-platform auto-update support for Electron Forge apps using GitHub Releases. * * @example * import Updater from 'my-auto-updater'; * const updater = new Updater({ repo: 'user/repo' }); * updater.on('update-available', info => { ... }); * updater.checkForUpdates(); */ class Updater extends events_1.TypedEventEmitter { config; githubProvider; downloader; lastUpdateInfo = null; lastDownloadedPath = null; currentVersion; debug; /** * Create a new Updater instance. * @param config Updater configuration options */ constructor(config) { super(); this.config = (0, config_1.resolveConfig)(config); this.githubProvider = new GitHubProvider_1.GitHubProvider({ repo: this.config.repo, allowPrerelease: this.config.allowPrerelease, channel: this.config.channel, }); this.downloader = new Downloader_1.Downloader(); this.currentVersion = this.detectAppVersion(); this.debug = !!config.debug; if (this.debug) { this.sendDebugLog("[Updater:debug] Initialized with config:", this.config); this.sendDebugLog("[Updater:debug] Current version:", this.currentVersion); } } sendDebugLog(message, ...args) { if (!this.debug) return; // Print to main process console // eslint-disable-next-line no-console console.debug(message, ...args); // Send to all renderer processes if Electron IPC is available try { if (typeof electron_1.ipcMain !== "undefined" && electron_1.webContents.getAllWebContents) { for (const wc of electron_1.webContents.getAllWebContents()) { wc.send("basic-electron-updater:debug", message, ...args); } } } catch { // Ignore errors when not in Electron context } } detectAppVersion() { try { // Only works in Electron main process // eslint-disable-next-line @typescript-eslint/no-var-requires const electron = require("electron"); const version = electron.app.getVersion(); if (this.debug) this.sendDebugLog("[Updater:debug] Detected Electron app version:", version); return version; } catch { if (this.debug) this.sendDebugLog("[Updater:debug] Could not detect Electron app version, using 0.0.0"); return "0.0.0"; } } /** * Checks GitHub Releases for the latest update. * Emits 'update-available' or 'update-not-available'. * @returns UpdateInfo if available, otherwise null */ async checkForUpdates() { try { this.emit("checking-for-update"); this.config.logger.info("Checking for updates..."); if (this.debug) this.sendDebugLog("[Updater:debug] Calling getLatestRelease..."); const info = await this.githubProvider.getLatestRelease(); if (this.debug) { this.sendDebugLog("[Updater:debug] Latest release info:", info); this.sendDebugLog("[Updater:debug] Current version:", this.currentVersion); } this.lastUpdateInfo = info; if (info && semver.valid(info.version) && semver.valid(this.currentVersion) && semver.gt(info.version, this.currentVersion)) { if (this.debug) this.sendDebugLog("[Updater:debug] Update available:", info.version); this.emit("update-available", info); this.config.logger.info("Update available:", info.version); if (this.config.autoDownload) { this.downloadUpdate(); } return info; } else { if (this.debug) this.sendDebugLog("[Updater:debug] No update available."); this.emit("update-not-available"); this.config.logger.info("No update available."); return null; } } catch (err) { const error = err instanceof Error ? err : new Error(String(err)); if (this.debug) this.sendDebugLog("[Updater:debug] Error in checkForUpdates:", error); this.emit("error", error); this.config.logger.error("Update check failed:", error); return null; } } /** * Downloads the update asset for the current platform. * Emits 'download-progress' and 'downloaded'. * Validates SHA256 and GPG signature if provided. * @returns Path to the downloaded file * @throws Error if download or validation fails */ async downloadUpdate() { if (!this.lastUpdateInfo) { throw new Error("No update info available. Call checkForUpdates() first."); } const platform = os.platform(); const arch = os.arch(); if (this.debug) { this.sendDebugLog("[Updater:debug] Platform:", platform, "Arch:", arch); this.sendDebugLog("[Updater:debug] Assets:", this.lastUpdateInfo.assets); } // Select asset for current platform const asset = this.lastUpdateInfo.assets.find((a) => { const n = a.name.toLowerCase(); if (platform === "win32") { return n.endsWith(".exe") || n.endsWith(".msi") || n.endsWith(".nsis.zip") || n.includes("win") || n.includes("windows") || n.includes("setup"); } if (platform === "darwin") { return n.endsWith(".dmg") || n.endsWith(".zip") || n.endsWith(".pkg") || n.includes("mac") || n.includes("darwin") || n.includes("osx"); } if (platform === "linux") { return n.endsWith(".appimage") || n.endsWith(".tar.gz") || n.endsWith(".deb") || n.endsWith(".rpm") || n.endsWith(".snap") || n.includes("linux"); } return false; }); if (!asset) throw new Error("No suitable asset found for platform: " + platform); const dest = path.join(os.tmpdir(), asset.name); if (this.debug) { this.sendDebugLog("[Updater:debug] Downloading asset:", asset.url); this.sendDebugLog("[Updater:debug] Download destination:", dest); } this.config.logger.info("Downloading update asset:", asset.url); try { const filePath = await this.downloader.downloadAsset(asset.url, dest, (progress) => { if (this.debug) this.sendDebugLog("[Updater:debug] Download progress:", progress); this.emit("download-progress", progress); }, asset.sha256); // GPG signature validation if present if (asset.gpgSignatureUrl) { const sigDest = dest + ".sig"; if (this.debug) this.sendDebugLog("[Updater:debug] Downloading GPG signature:", asset.gpgSignatureUrl); await this.downloader.downloadAsset(asset.gpgSignatureUrl, sigDest, undefined, undefined); try { await this.downloader.validateGpg(filePath, sigDest); if (this.debug) this.sendDebugLog("[Updater:debug] GPG signature validated for", filePath); this.config.logger.info("GPG signature validated for", filePath); } catch (err) { const error = err instanceof Error ? err : new Error(String(err)); if (this.debug) this.sendDebugLog("[Updater:debug] GPG validation failed:", error); this.emit("error", error); this.config.logger.error("GPG validation failed:", error); throw error; } } this.emit("downloaded", filePath); this.config.logger.info("Downloaded update to:", filePath); this.lastDownloadedPath = filePath; return filePath; } catch (err) { const error = err instanceof Error ? err : new Error(String(err)); if (this.debug) this.sendDebugLog("[Updater:debug] Download failed:", error); this.emit("error", error); this.config.logger.error("Download failed:", error); throw error; } } /** * Applies the downloaded update by launching the installer or archive. * Handles platform-specific logic. * @throws Error if no update is downloaded or launching fails */ async applyUpdate() { if (!this.lastDownloadedPath) { throw new Error("No downloaded update to apply. Call downloadUpdate() first."); } const platform = os.platform(); if (this.debug) this.sendDebugLog("[Updater:debug] Applying update for platform:", platform); this.config.logger.info("Applying update for platform:", platform); try { if (platform === "win32") { await (0, windows_1.applyWindowsUpdate)(this.lastDownloadedPath); } else if (platform === "darwin") { await (0, macos_1.applyMacUpdate)(this.lastDownloadedPath); } else if (platform === "linux") { await (0, linux_1.applyLinuxUpdate)(this.lastDownloadedPath); } else { throw new Error("Unsupported platform: " + platform); } if (this.debug) this.sendDebugLog("[Updater:debug] Update applied (installer launched)."); this.config.logger.info("Update applied (installer launched)."); } catch (err) { const error = err instanceof Error ? err : new Error(String(err)); if (this.debug) this.sendDebugLog("[Updater:debug] Failed to apply update:", error); this.emit("error", error); this.config.logger.error("Failed to apply update:", error); throw error; } } } exports.default = Updater;