electron-incremental-update 1.2.0 → 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,9 @@ The new `${electron.app.name}.asar`, which can download from remote or load from
10
10
 
11
11
  All **native modules** should be packaged into `app.asar` to reduce `${electron.app.name}.asar` file size, [see usage](#use-native-modules). Therefore, auto upgrade of portable app is possible.
12
12
 
13
- no `vite-plugin-electron-renderer` config
13
+ Support bytecode protection, [see details](#bytecode-protection)
14
+
15
+ No `vite-plugin-electron-renderer` config
14
16
 
15
17
  - inspired by [Obsidian](https://obsidian.md/)'s upgrade strategy
16
18
 
@@ -159,16 +161,27 @@ module.exports = {
159
161
 
160
162
  ### Use in main process
161
163
 
162
- To use electron's `net` module for updating, the `checkUpdate` and `download` functions must be called after the app is ready by default.
163
-
164
- However, you have the option to customize the download function when creating the updater.
164
+ To use electron's `net` module for updating, the `checkUpdate` and `download` functions must be called after the app is ready by default. You have the option to customize the download function when creating the updater.
165
165
 
166
166
  **NOTE: There should only one function and should be default export in the entry file**
167
167
 
168
+ in `electron/entry.ts`
169
+
170
+ ```ts
171
+ initApp({
172
+ updater: {
173
+ overrideFunctions: {
174
+ downloadJSON: (url: string, headers: Record<string, any>) => {}
175
+ // ...
176
+ }
177
+ },
178
+ })
179
+ ```
180
+
168
181
  in `electron/main/index.ts`
169
182
 
170
183
  ```ts
171
- import { startupWithUpdater } from 'electron-incremental-update'
184
+ import { startupWithUpdater, UpdaterError } from 'electron-incremental-update'
172
185
  import { getPathFromAppNameAsar, getVersions } from 'electron-incremental-update/utils'
173
186
  import { app } from 'electron'
174
187
 
@@ -185,10 +198,12 @@ export default startupWithUpdater((updater) => {
185
198
  console.log(percent)
186
199
  }
187
200
  updater.logger = console
201
+ updater.receiveBeta = true
202
+
188
203
  updater.checkUpdate().then(async (result) => {
189
204
  if (result === undefined) {
190
205
  console.log('Update Unavailable')
191
- } else if (result instanceof Error) {
206
+ } else if (result instanceof UpdaterError) {
192
207
  console.error(result)
193
208
  } else {
194
209
  console.log('new version: ', result.version)
@@ -213,7 +228,16 @@ export default startupWithUpdater((updater) => {
213
228
 
214
229
  All the **native modules** should be set as `dependency` in `package.json`. `electron-rebuild` only check dependencies inside `dependency` field.
215
230
 
216
- If you are using `electron-builder` to build distributions, all the native modules with its **large relavent `node_modiles`** will be packaged into `app.asar` by default. You can setup `nativeModuleEntryMap` option to prebundle all the native modules and skip bundled by `electron-builder`
231
+ If you are using `electron-builder` to build distributions, all the native modules with its **large relavent `node_modiles`** will be packaged into `app.asar` by default.
232
+
233
+ Luckily, `Esbuild` can bundle all the dependencies. Just follow the steps:
234
+
235
+ 1. setup `nativeModuleEntryMap` option
236
+ 2. Manually copy the native binaries in `postBuild` callback
237
+ 3. Exclude all the dependencies in `electron-builder`'s config
238
+ 4. call the native functions with `loadNativeModuleFromEntry` in your code
239
+
240
+ #### Example
217
241
 
218
242
  in `vite.config.ts`
219
243
 
@@ -224,6 +248,7 @@ const plugin = electronWithUpdater({
224
248
  entry: {
225
249
  nativeModuleEntryMap: {
226
250
  db: './electron/native/db.ts',
251
+ img: './electron/native/img.ts',
227
252
  },
228
253
  postBuild: async ({ copyToEntryOutputDir }) => {
229
254
  // for better-sqlite3
@@ -248,9 +273,8 @@ in `electron/native/db.ts`
248
273
 
249
274
  ```ts
250
275
  import Database from 'better-sqlite3'
251
- import { getPaths } from 'electron-incremental-update/utils'
252
276
 
253
- const db = new Database(':memory:', { nativeBinding: getPaths().getPathFromEntryAsar('better_sqlite3.node') })
277
+ const db = new Database(':memory:', { nativeBinding: './better_sqlite3.node' })
254
278
 
255
279
  export function test() {
256
280
  db.exec(
@@ -295,7 +319,11 @@ module.exports = {
295
319
 
296
320
  ### Bytecode protection
297
321
 
298
- credit to [electron-vite](https://github.com/alex8088/electron-vite/blob/master/src/plugins/bytecode.ts)
322
+ From v1.2, the vite plugin is able to generate bytecode to protect your application.
323
+
324
+ It will automatically protect your `SIGNATURE_CERT` by default.
325
+
326
+ credit to [electron-vite](https://github.com/alex8088/electron-vite/blob/master/src/plugins/bytecode.ts), and improve the string protection (see [original issue](https://github.com/alex8088/electron-vite/issues/552))
299
327
 
300
328
  ```ts
301
329
  electronWithUpdater({
@@ -311,6 +339,99 @@ electronWithUpdater({
311
339
 
312
340
  ### Types
313
341
 
342
+ #### Updater
343
+
344
+ ```ts
345
+ export interface UpdaterOption {
346
+ /**
347
+ * public key of signature, which will be auto generated by plugin,
348
+ * generate by `selfsigned` if not set
349
+ */
350
+ SIGNATURE_CERT?: string
351
+ /**
352
+ * repository url, e.g. `https://github.com/electron/electron`
353
+ *
354
+ * you can use the `repository` in `package.json`
355
+ *
356
+ * if `updateJsonURL` or `releaseAsarURL` are absent,
357
+ * `repository` will be used to determine the url
358
+ */
359
+ repository?: string
360
+ /**
361
+ * URL of version info json
362
+ * @default `${repository.replace('github.com', 'raw.githubusercontent.com')}/master/version.json`
363
+ * @throws if `updateJsonURL` and `repository` are all not set
364
+ */
365
+ updateJsonURL?: string
366
+ /**
367
+ * URL of release asar.gz
368
+ * @default `${repository}/releases/download/v${version}/${app.name}-${version}.asar.gz`
369
+ * @throws if `releaseAsarURL` and `repository` are all not set
370
+ */
371
+ releaseAsarURL?: string
372
+ /**
373
+ * whether to receive beta update
374
+ */
375
+ receiveBeta?: boolean
376
+ overrideFunctions?: UpdaterOverrideFunctions
377
+ downloadConfig?: UpdaterDownloadConfig
378
+ }
379
+ export type Logger = {
380
+ info: (msg: string) => void
381
+ debug: (msg: string) => void
382
+ warn: (msg: string) => void
383
+ error: (msg: string, e?: Error) => void
384
+ }
385
+
386
+ export type UpdaterOverrideFunctions = {
387
+ /**
388
+ * custom version compare function
389
+ * @param version1 old version string
390
+ * @param version2 new version string
391
+ * @returns if version1 < version2
392
+ */
393
+ isLowerVersion?: (version1: string, version2: string) => boolean | Promise<boolean>
394
+ /**
395
+ * custom verify signature function
396
+ * @param buffer file buffer
397
+ * @param signature signature
398
+ * @param cert certificate
399
+ * @returns if signature is valid, returns the version or `true` , otherwise returns `false`
400
+ */
401
+ verifySignaure?: (buffer: Buffer, signature: string, cert: string) => string | false | Promise<string | false>
402
+ /**
403
+ * custom download JSON function
404
+ * @param url download url
405
+ * @param header download header
406
+ * @returns `UpdateJSON`
407
+ */
408
+ downloadJSON?: (url: string, headers: Record<string, any>) => Promise<UpdateJSON>
409
+ /**
410
+ * custom download buffer function
411
+ * @param url download url
412
+ * @param headers download header
413
+ * @param total precaculated file total size
414
+ * @param onDownloading on downloading callback
415
+ * @returns `Buffer`
416
+ */
417
+ downloadBuffer?: (url: string, headers: Record<string, any>, total: number, onDownloading?: (progress: DownloadingInfo) => void) => Promise<Buffer>
418
+ }
419
+
420
+ export type UpdaterDownloadConfig = {
421
+ /**
422
+ * download user agent
423
+ * @default 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36'
424
+ */
425
+ userAgent?: string
426
+ /**
427
+ * extra download header, `accept` and `user-agent` is set by default
428
+ */
429
+ extraHeader?: Record<string, string>
430
+ }
431
+ ```
432
+
433
+ #### Plugin
434
+
314
435
  ```ts
315
436
  type ElectronWithUpdaterOptions = {
316
437
  /**
@@ -0,0 +1,247 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/utils/electron.ts
9
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
10
+ import { dirname, join } from "node:path";
11
+ import { app } from "electron";
12
+ var isDev = __EIU_IS_DEV__;
13
+ var isWin = process.platform === "win32";
14
+ var isMac = process.platform === "darwin";
15
+ var isLinux = process.platform === "linux";
16
+ function getPathFromAppNameAsar(...path) {
17
+ return isDev ? "DEV.asar" : join(dirname(app.getAppPath()), `${app.name}.asar`, ...path);
18
+ }
19
+ function getAppVersion() {
20
+ return isDev ? getEntryVersion() : readFileSync(getPathFromAppNameAsar("version"), "utf-8");
21
+ }
22
+ function getEntryVersion() {
23
+ return app.getVersion();
24
+ }
25
+ function requireNative(moduleName) {
26
+ return __require(join(app.getAppPath(), __EIU_ENTRY_DIST_PATH__, moduleName));
27
+ }
28
+ function restartApp() {
29
+ app.relaunch();
30
+ app.quit();
31
+ }
32
+ function setAppUserModelId(id) {
33
+ isWin && app.setAppUserModelId(id ?? `org.${app.name}`);
34
+ }
35
+ function disableHWAccForWin7() {
36
+ if (__require("node:os").release().startsWith("6.1")) {
37
+ app.disableHardwareAcceleration();
38
+ }
39
+ }
40
+ function singleInstance(window) {
41
+ const result = app.requestSingleInstanceLock();
42
+ result ? app.on("second-instance", () => {
43
+ if (window) {
44
+ window.show();
45
+ if (window.isMinimized()) {
46
+ window.restore();
47
+ }
48
+ window.focus();
49
+ }
50
+ }) : app.quit();
51
+ return result;
52
+ }
53
+ function setPortableAppDataPath(dirName = "data") {
54
+ const portablePath = join(dirname(app.getPath("exe")), dirName);
55
+ if (!existsSync(portablePath)) {
56
+ mkdirSync(portablePath);
57
+ }
58
+ app.setPath("appData", portablePath);
59
+ }
60
+ function waitAppReady(timeout = 1e3) {
61
+ return app.isReady() ? Promise.resolve() : new Promise((resolve, reject) => {
62
+ const _ = setTimeout(() => {
63
+ reject(new Error("app is not ready"));
64
+ }, timeout);
65
+ app.whenReady().then(() => {
66
+ clearTimeout(_);
67
+ resolve();
68
+ });
69
+ });
70
+ }
71
+ function loadPage(win, htmlFilePath = "index.html") {
72
+ isDev ? win.loadURL(process.env.VITE_DEV_SERVER_URL + htmlFilePath) : win.loadFile(getPathFromAppNameAsar("renderer", htmlFilePath));
73
+ }
74
+ function getPathFromPreload(...paths) {
75
+ return isDev ? join(app.getAppPath(), __EIU_ELECTRON_DIST_PATH__, "preload", ...paths) : getPathFromAppNameAsar("preload", ...paths);
76
+ }
77
+ function getPathFromPublic(...paths) {
78
+ return isDev ? join(app.getAppPath(), "public", ...paths) : getPathFromAppNameAsar("renderer", ...paths);
79
+ }
80
+ function getPathFromEntryAsar(...paths) {
81
+ return join(app.getAppPath(), __EIU_ENTRY_DIST_PATH__, ...paths);
82
+ }
83
+
84
+ // src/utils/zip.ts
85
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
86
+ import { brotliCompress } from "node:zlib";
87
+ async function zipFile(filePath, targetFilePath = `${filePath}.gz`) {
88
+ if (!existsSync2(filePath)) {
89
+ throw new Error(`path to be zipped not exist: ${filePath}`);
90
+ }
91
+ const buffer = readFileSync2(filePath);
92
+ return new Promise((resolve, reject) => {
93
+ brotliCompress(buffer, (err, buffer2) => {
94
+ if (err) {
95
+ reject(err);
96
+ }
97
+ writeFileSync(targetFilePath, buffer2);
98
+ resolve(null);
99
+ });
100
+ });
101
+ }
102
+
103
+ // src/utils/unzip.ts
104
+ import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
105
+ import { brotliDecompress } from "node:zlib";
106
+ async function unzipFile(gzipPath, targetFilePath = gzipPath.slice(0, -3)) {
107
+ if (!existsSync3(gzipPath)) {
108
+ throw new Error(`path to zipped file not exist: ${gzipPath}`);
109
+ }
110
+ const compressedBuffer = readFileSync3(gzipPath);
111
+ return new Promise((resolve, reject) => {
112
+ brotliDecompress(compressedBuffer, (err, buffer) => {
113
+ rmSync(gzipPath);
114
+ if (err) {
115
+ reject(err);
116
+ }
117
+ writeFileSync2(targetFilePath, buffer);
118
+ resolve(null);
119
+ });
120
+ });
121
+ }
122
+
123
+ // src/utils/version.ts
124
+ function handleUnexpectedErrors(callback) {
125
+ process.on("uncaughtException", callback);
126
+ process.on("unhandledRejection", callback);
127
+ }
128
+ function parseVersion(version) {
129
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([a-z0-9.-]+))?/i.exec(version);
130
+ if (!match) {
131
+ throw new TypeError(`invalid version: ${version}`);
132
+ }
133
+ const [major, minor, patch] = match.slice(1, 4).map(Number);
134
+ const ret = {
135
+ major,
136
+ minor,
137
+ patch,
138
+ stage: "",
139
+ stageVersion: -1
140
+ };
141
+ if (match[4]) {
142
+ let [stage, _v] = match[4].split(".");
143
+ ret.stage = stage;
144
+ ret.stageVersion = Number(_v) || -1;
145
+ }
146
+ if (Number.isNaN(major) || Number.isNaN(minor) || Number.isNaN(patch) || Number.isNaN(ret.stageVersion)) {
147
+ throw new TypeError(`invalid version: ${version}`);
148
+ }
149
+ return ret;
150
+ }
151
+ function isLowerVersionDefault(oldVer, newVer) {
152
+ const oldV = parseVersion(oldVer);
153
+ const newV = parseVersion(newVer);
154
+ function compareStrings(str1, str2) {
155
+ if (str1 === "") {
156
+ return str2 !== "";
157
+ } else if (str2 === "") {
158
+ return true;
159
+ }
160
+ return str1 < str2;
161
+ }
162
+ for (let key of Object.keys(oldV)) {
163
+ if (key === "stage" && compareStrings(oldV[key], newV[key])) {
164
+ return true;
165
+ } else if (oldV[key] !== newV[key]) {
166
+ return oldV[key] < newV[key];
167
+ }
168
+ }
169
+ return false;
170
+ }
171
+ function isUpdateJSON(json) {
172
+ const is = (j) => !!(j && j.minimumVersion && j.signature && j.size && j.version);
173
+ return is(json) && is(json?.beta);
174
+ }
175
+
176
+ // src/utils/crypto/decrypt.ts
177
+ import { createDecipheriv, createVerify } from "node:crypto";
178
+
179
+ // src/utils/crypto/utils.ts
180
+ import { createHash } from "node:crypto";
181
+ function hashString(data, length) {
182
+ const hash = createHash("SHA256").update(data).digest("binary");
183
+ return Buffer.from(hash).subarray(0, length);
184
+ }
185
+
186
+ // src/utils/crypto/decrypt.ts
187
+ function decrypt(encryptedText, key, iv) {
188
+ const decipher = createDecipheriv("aes-256-cbc", key, iv);
189
+ let decrypted = decipher.update(encryptedText, "base64url", "utf8");
190
+ decrypted += decipher.final("utf8");
191
+ return decrypted;
192
+ }
193
+ function verifySignatureDefault(buffer, signature2, cert) {
194
+ try {
195
+ const [sig, version] = decrypt(signature2, hashString(cert, 32), hashString(buffer, 16)).split("%");
196
+ const result = createVerify("RSA-SHA256").update(buffer).verify(cert, sig, "base64");
197
+ return result ? version : void 0;
198
+ } catch (error) {
199
+ return void 0;
200
+ }
201
+ }
202
+
203
+ // src/utils/crypto/encrypt.ts
204
+ import { createCipheriv, createPrivateKey, createSign } from "node:crypto";
205
+ function encrypt(plainText, key, iv) {
206
+ const cipher = createCipheriv("aes-256-cbc", key, iv);
207
+ let encrypted = cipher.update(plainText, "utf8", "base64url");
208
+ encrypted += cipher.final("base64url");
209
+ return encrypted;
210
+ }
211
+ function signature(buffer, privateKey, cert, version) {
212
+ const sig = createSign("RSA-SHA256").update(buffer).sign(createPrivateKey(privateKey), "base64");
213
+ return encrypt(`${sig}%${version}`, hashString(cert, 32), hashString(buffer, 16));
214
+ }
215
+
216
+ export {
217
+ __require,
218
+ handleUnexpectedErrors,
219
+ parseVersion,
220
+ isLowerVersionDefault,
221
+ isUpdateJSON,
222
+ isDev,
223
+ isWin,
224
+ isMac,
225
+ isLinux,
226
+ getPathFromAppNameAsar,
227
+ getAppVersion,
228
+ getEntryVersion,
229
+ requireNative,
230
+ restartApp,
231
+ setAppUserModelId,
232
+ disableHWAccForWin7,
233
+ singleInstance,
234
+ setPortableAppDataPath,
235
+ waitAppReady,
236
+ loadPage,
237
+ getPathFromPreload,
238
+ getPathFromPublic,
239
+ getPathFromEntryAsar,
240
+ unzipFile,
241
+ zipFile,
242
+ hashString,
243
+ decrypt,
244
+ verifySignatureDefault,
245
+ encrypt,
246
+ signature
247
+ };
@@ -0,0 +1,4 @@
1
+ declare function decrypt(encryptedText: string, key: Buffer, iv: Buffer): string;
2
+ declare function verifySignatureDefault(buffer: Buffer, signature: string, cert: string): string | undefined | Promise<string | undefined>;
3
+
4
+ export { decrypt as d, verifySignatureDefault as v };
@@ -0,0 +1,4 @@
1
+ declare function decrypt(encryptedText: string, key: Buffer, iv: Buffer): string;
2
+ declare function verifySignatureDefault(buffer: Buffer, signature: string, cert: string): string | undefined | Promise<string | undefined>;
3
+
4
+ export { decrypt as d, verifySignatureDefault as v };