electron-incremental-update 0.3.0 → 0.5.0

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
@@ -16,6 +16,8 @@ using RSA + Signature to sign the new `main.asar` downloaded from remote and rep
16
16
 
17
17
  develop with [vite-plugin-electron](https://github.com/electron-vite/vite-plugin-electron), and may be effect in other electron vite frameworks
18
18
 
19
+ **all options are documented in the jsdoc**
20
+
19
21
  ## install
20
22
 
21
23
  ### npm
@@ -50,21 +52,28 @@ src
50
52
 
51
53
  ### setup app
52
54
 
53
- more example see comment on `initApp()`
54
55
 
55
56
  ```ts
56
57
  // electron/app.ts
57
- import { createUpdater, initApp } from 'electron-incremental-update'
58
+ import { createUpdater, getGithubReleaseCdnGroup, initApp, parseGithubCdnURL } from 'electron-incremental-update'
58
59
  import { name, repository } from '../package.json'
59
60
 
60
61
  const SIGNATURE_PUB = '' // auto generate RSA public key when start app
61
62
 
63
+ // create updater when init, no need to set productName
64
+ initApp({ name }, { SIGNATURE_PUB, repository })
65
+
66
+ // or create updater manually
67
+ const { cdnPrefix } = getGithubReleaseCdnGroup()[0]
62
68
  const updater = createUpdater({
63
69
  SIGNATURE_PUB,
64
- repository,
65
70
  productName: name,
71
+ repository,
72
+ updateJsonURL: parseGithubCdnURL(repository, 'fastly.jsdelivr.net/gh', 'version.json'),
73
+ releaseAsarURL: parseGithubCdnURL(repository, cdnPrefix, `download/latest/${name}.asar.gz`),
74
+ debug: true,
66
75
  })
67
- initApp(name, updater)
76
+ initApp({ name }).setUpdater(updater)
68
77
  ```
69
78
 
70
79
  ### setup main
@@ -72,44 +81,36 @@ initApp(name, updater)
72
81
  ```ts
73
82
  // electron/main/index.ts
74
83
  import type { Updater } from 'electron-incremental-update'
75
- import { getAppAsarPath, getAppVersion, getElectronVersion } from 'electron-incremental-update'
84
+ import { getAppAsarPath, getAppVersion, getEntryVersion } from 'electron-incremental-update'
76
85
  import { app } from 'electron'
77
86
  import { name } from '../../package.json'
78
87
 
79
88
  export default function (updater: Updater) {
80
89
  console.log('\ncurrent:')
81
- console.log(`\telectron: ${getElectronVersion()}`)
82
90
  console.log(`\tasar path: ${getAppAsarPath(name)}`)
91
+ console.log(`\tentry: ${getEntryVersion()}`)
83
92
  console.log(`\tapp: ${getAppVersion(name)}`)
84
-
85
- updater.checkUpdate()
86
- updater.on('checkResult', async (result, err) => {
87
- switch (result) {
88
- case 'success':
89
- await dialog.showMessageBox({
90
- type: 'info',
91
- buttons: ['Restart', 'Later'],
92
- message: 'Application successfully updated!',
93
- }).then(({ response }) => {
94
- if (response === 0) {
95
- app.relaunch()
96
- app.quit()
97
- }
98
- })
99
- break
100
- case 'unavailable':
101
- console.log('Update Unavailable')
102
- break
103
- case 'fail':
104
- console.error(err)
105
- break
93
+ let size = 0
94
+ updater.on('downloading', (progress) => {
95
+ console.log(`${(progress / size).toFixed(2)}%`)
96
+ })
97
+ updater.on('debug', data => console.log('[updater]:', data))
98
+ updater.checkUpdate().then(async (result) => {
99
+ if (result === undefined) {
100
+ console.log('Update Unavailable')
101
+ } else if (result instanceof Error) {
102
+ console.error(result)
103
+ } else {
104
+ size = result.size
105
+ console.log('new version: ', result.version)
106
+ const { response } = await dialog.showMessageBox({
107
+ type: 'info',
108
+ buttons: ['Download', 'Later'],
109
+ message: 'Application update available!',
110
+ })
111
+ response === 0 && console.log(await updater.downloadUpdate())
106
112
  }
107
113
  })
108
- updater.on('downloadStart', console.log)
109
- updater.on('downloading', console.log)
110
- updater.on('downloadEnd', console.log)
111
- updater.on('donwnloadError', console.error)
112
-
113
114
  // app logics
114
115
  app.whenReady().then(() => {
115
116
  // ...
@@ -144,7 +145,12 @@ db.close()
144
145
 
145
146
  ### setup vite.config.ts
146
147
 
148
+ make sure the plugin is set in the **last** build task plugin option
149
+
150
+ - set it to preload task plugin, as the end of build task
151
+
147
152
  ```ts
153
+ // vite.config.ts
148
154
  export default defineConfig(({ command }) => {
149
155
 
150
156
  const isBuild = command === 'build'
@@ -162,7 +168,7 @@ export default defineConfig(({ command }) => {
162
168
  // ...
163
169
  vite: {
164
170
  plugins: [
165
- updater({ // !make sure the plugin run pack asar after all build finish
171
+ updater({
166
172
  productName: pkg.name,
167
173
  version: pkg.version,
168
174
  isBuild,
@@ -179,90 +185,6 @@ export default defineConfig(({ command }) => {
179
185
  })
180
186
  ```
181
187
 
182
- #### plugin options
183
-
184
- ```ts
185
- type Options = {
186
- /**
187
- * whether is in build mode
188
- */
189
- isBuild: boolean
190
- /**
191
- * the name of you application
192
- *
193
- * you can set as 'name' in package.json
194
- */
195
- productName: string
196
- /**
197
- * the version of you application
198
- *
199
- * you can set as 'version' in package.json
200
- */
201
- version: string
202
- /**
203
- * Whether to minify
204
- */
205
- minify?: boolean
206
- /**
207
- * path config
208
- */
209
- paths?: {
210
- /**
211
- * Path to app entry file
212
- * @default 'electron/app.ts'
213
- */
214
- entryPath?: string
215
- /**
216
- * Path to app entry output file
217
- * @default 'app.js'
218
- */
219
- entryOutputPath?: string
220
- /**
221
- * Path to asar file
222
- * @default `release/${ProductName}.asar`
223
- */
224
- asarOutputPath?: string
225
- /**
226
- * Path to electron build output
227
- * @default `dist-electron`
228
- */
229
- electronDistPath?: string
230
- /**
231
- * Path to renderer build output
232
- * @default `dist`
233
- */
234
- rendererDistPath?: string
235
- /**
236
- * Path to version info output
237
- * @default `version.json`
238
- */
239
- versionPath?: string
240
- }
241
- /**
242
- * signature config
243
- */
244
- keys?: {
245
- /**
246
- * Path to the pem file that contains private key
247
- * if not ended with .pem, it will be appended
248
- * @default 'public/private.pem'
249
- */
250
- privateKeyPath?: string
251
- /**
252
- * Path to the pem file that contains public key
253
- * if not ended with .pem, it will be appended
254
- * @default 'public/public.pem'
255
- */
256
- publicKeyPath?: string
257
- /**
258
- * Length of the key
259
- * @default 2048
260
- */
261
- keyLength?: number
262
- }
263
- }
264
- ```
265
-
266
188
  ### electron-builder config
267
189
 
268
190
  ```js
@@ -0,0 +1,61 @@
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")
5
+ return require.apply(this, arguments);
6
+ throw new Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+
9
+ // src/crypto.ts
10
+ import { constants, createCipheriv, createDecipheriv, createHash, createSign, createVerify, generateKeyPairSync } from "node:crypto";
11
+ import { Buffer as Buffer2 } from "node:buffer";
12
+ var aesEncode = "base64url";
13
+ function generateRSA(length = 2048) {
14
+ const pair = generateKeyPairSync("rsa", { modulusLength: length });
15
+ const privateKey = pair.privateKey.export({ type: "pkcs1", format: "pem" });
16
+ const publicKey = pair.publicKey.export({ type: "pkcs1", format: "pem" });
17
+ return {
18
+ privateKey,
19
+ publicKey
20
+ };
21
+ }
22
+ function encrypt(plainText, key, iv) {
23
+ const cipher = createCipheriv("aes-256-cbc", key, iv);
24
+ let encrypted = cipher.update(plainText, "utf8", aesEncode);
25
+ encrypted += cipher.final(aesEncode);
26
+ return encrypted;
27
+ }
28
+ function decrypt(encryptedText, key, iv) {
29
+ const decipher = createDecipheriv("aes-256-cbc", key, iv);
30
+ let decrypted = decipher.update(encryptedText, aesEncode, "utf8");
31
+ decrypted += decipher.final("utf8");
32
+ return decrypted;
33
+ }
34
+ function generateKey(buffer, str, length) {
35
+ str += createHash("md5").update(buffer.map((v, i) => i & length / 4 && v)).digest("hex");
36
+ const hash = createHash("SHA256").update(str).digest("binary");
37
+ return Buffer2.from(hash).subarray(0, length);
38
+ }
39
+ function signature(buffer, privateKey, publicKey, name) {
40
+ const sig = createSign("RSA-SHA256").update(buffer).sign({
41
+ key: privateKey,
42
+ padding: constants.RSA_PKCS1_PADDING,
43
+ saltLength: constants.RSA_PSS_SALTLEN_DIGEST
44
+ }, "base64");
45
+ return encrypt(sig, generateKey(buffer, publicKey, 32), generateKey(buffer, name, 16));
46
+ }
47
+ function verify(buffer, signature2, publicKey, name) {
48
+ try {
49
+ const sig = decrypt(signature2, generateKey(buffer, publicKey, 32), generateKey(buffer, name, 16));
50
+ return createVerify("RSA-SHA256").update(buffer).verify(publicKey, sig, "base64");
51
+ } catch (error) {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ export {
57
+ __require,
58
+ generateRSA,
59
+ signature,
60
+ verify
61
+ };
package/dist/index.cjs CHANGED
@@ -46,14 +46,37 @@ var import_electron3 = require("electron");
46
46
 
47
47
  // src/updater/index.ts
48
48
  var import_node_events = require("events");
49
- var import_node_crypto = require("crypto");
50
49
  var import_node_zlib = require("zlib");
51
50
  var import_node_fs2 = require("fs");
52
51
  var import_promises = require("fs/promises");
53
52
  var import_electron2 = require("electron");
54
53
 
55
- // src/updater/defaultFunctions.ts
54
+ // src/crypto.ts
55
+ var import_node_crypto = require("crypto");
56
56
  var import_node_buffer = require("buffer");
57
+ var aesEncode = "base64url";
58
+ function decrypt(encryptedText, key, iv) {
59
+ const decipher = (0, import_node_crypto.createDecipheriv)("aes-256-cbc", key, iv);
60
+ let decrypted = decipher.update(encryptedText, aesEncode, "utf8");
61
+ decrypted += decipher.final("utf8");
62
+ return decrypted;
63
+ }
64
+ function generateKey(buffer, str, length) {
65
+ str += (0, import_node_crypto.createHash)("md5").update(buffer.map((v, i) => i & length / 4 && v)).digest("hex");
66
+ const hash = (0, import_node_crypto.createHash)("SHA256").update(str).digest("binary");
67
+ return import_node_buffer.Buffer.from(hash).subarray(0, length);
68
+ }
69
+ function verify(buffer, signature, publicKey, name) {
70
+ try {
71
+ const sig = decrypt(signature, generateKey(buffer, publicKey, 32), generateKey(buffer, name, 16));
72
+ return (0, import_node_crypto.createVerify)("RSA-SHA256").update(buffer).verify(publicKey, sig, "base64");
73
+ } catch (error) {
74
+ return false;
75
+ }
76
+ }
77
+
78
+ // src/updater/defaultFunctions.ts
79
+ var import_node_buffer2 = require("buffer");
57
80
  var import_node_https = __toESM(require("https"), 1);
58
81
  function downloadJSONDefault(url, updater, headers) {
59
82
  return new Promise((resolve2, reject) => {
@@ -63,11 +86,15 @@ function downloadJSONDefault(url, updater, headers) {
63
86
  res.headers = headers;
64
87
  res.on("data", (chunk) => data += chunk);
65
88
  res.on("end", () => {
66
- const json = JSON.parse(data);
67
- if ("signature" in json && "version" in json && "size" in json) {
68
- resolve2(json);
69
- } else {
70
- throw new Error("invalid update json");
89
+ try {
90
+ const json = JSON.parse(data);
91
+ if ("signature" in json && "version" in json && "size" in json) {
92
+ resolve2(json);
93
+ } else {
94
+ throw Error;
95
+ }
96
+ } catch (e) {
97
+ reject(new Error("invalid json"));
71
98
  }
72
99
  });
73
100
  }).on("error", (e) => {
@@ -76,16 +103,18 @@ function downloadJSONDefault(url, updater, headers) {
76
103
  });
77
104
  }
78
105
  function downloadBufferDefault(url, updater, headers) {
106
+ let progress = 0;
79
107
  return new Promise((resolve2, reject) => {
80
108
  import_node_https.default.get(url, (res) => {
81
109
  let data = [];
82
110
  res.headers = headers;
83
111
  res.on("data", (chunk) => {
84
- updater.emit("downloading", chunk.length);
112
+ progress += chunk.length;
113
+ updater.emit("downloading", progress);
85
114
  data.push(chunk);
86
115
  });
87
116
  res.on("end", () => {
88
- resolve2(import_node_buffer.Buffer.concat(data));
117
+ resolve2(import_node_buffer2.Buffer.concat(data));
89
118
  });
90
119
  }).on("error", (e) => {
91
120
  reject(e);
@@ -177,6 +206,7 @@ function createUpdater({
177
206
  }) {
178
207
  const updater = new import_node_events.EventEmitter();
179
208
  let signature = "";
209
+ let version = "";
180
210
  const gzipPath = `../${productName}.asar.gz`;
181
211
  const tmpFile = gzipPath.replace(".asar.gz", ".tmp.gz");
182
212
  const { downloadBuffer, downloadJSON, extraHeader, userAgent } = downloadConfig || {};
@@ -190,12 +220,16 @@ function createUpdater({
190
220
  UserAgent: ua,
191
221
  ...extraHeader
192
222
  };
193
- log(`headers: ${headers}`);
223
+ log(`download headers: ${JSON.stringify(headers, null, 2)}`);
194
224
  const downloadFn = format === "json" ? downloadJSON ?? downloadJSONDefault : downloadBuffer ?? downloadBufferDefault;
195
- return await downloadFn(url, updater, headers);
225
+ log(`download ${format} from ${url}`);
226
+ const ret = await downloadFn(url, updater, headers);
227
+ log(`download ${format} success`);
228
+ return ret;
196
229
  }
197
230
  async function extractFile(gzipFilePath) {
198
231
  if (!gzipFilePath.endsWith(".asar.gz") || !(0, import_node_fs2.existsSync)(gzipFilePath)) {
232
+ log("update .asar.gz file not exist");
199
233
  return;
200
234
  }
201
235
  gzipFilePath = gzipFilePath.replace(".asar.gz", ".tmp.gz");
@@ -207,121 +241,119 @@ function createUpdater({
207
241
  log(`outputFilePath: ${outputFilePath}`);
208
242
  input.pipe(gunzip).pipe(output).on("finish", async () => {
209
243
  await (0, import_promises.rm)(gzipFilePath);
210
- log("finish");
244
+ log(`${gzipFilePath} unzipped`);
211
245
  resolve2(outputFilePath);
212
246
  }).on("error", async (err) => {
213
247
  await (0, import_promises.rm)(gzipFilePath);
214
- log(`error: ${err}`);
215
248
  output.destroy(err);
216
249
  reject(err);
217
250
  });
218
251
  });
219
252
  }
220
- function verify(buffer, signature2) {
221
- log(`signature: ${signature2}`);
222
- return (0, import_node_crypto.createVerify)("RSA-SHA256").update(buffer).verify(SIGNATURE_PUB, signature2, "base64");
223
- }
224
- function needUpdate(version) {
225
- if (!version || !import_electron2.app.isPackaged) {
253
+ function needUpdate(version2) {
254
+ if (!import_electron2.app.isPackaged) {
255
+ log("in dev mode, no need to update");
226
256
  return false;
227
257
  }
228
258
  const currentVersion = getEntryVersion();
229
- log(`currentVersion: ${currentVersion}`);
230
- log(`newVersion: ${version}`);
259
+ log(`check update:
260
+ current version is ${currentVersion},
261
+ new version is ${version2}`);
231
262
  const _compare = compareVersion ?? compareVersionDefault;
232
- return _compare(currentVersion, version);
263
+ return _compare(currentVersion, version2);
233
264
  }
234
- async function checkUpdate(url) {
235
- url ??= _update;
236
- if (!url) {
237
- log("no updateJsonURL, use repository");
238
- if (!repository) {
239
- throw new Error("updateJsonURL or repository are not set");
240
- }
241
- url = `${repository.replace("github.com", "raw.githubusercontent.com")}/version.json`;
242
- }
243
- log(`updateJsonURL: ${url}`);
244
- if ((0, import_node_fs2.existsSync)(tmpFile)) {
245
- log(`remove tmp file: ${tmpFile}`);
246
- await (0, import_promises.rm)(tmpFile);
247
- }
248
- const json = await download(url, "json");
249
- const {
250
- signature: _sig,
251
- version,
252
- size
253
- } = json;
254
- log(`UpdateJSON: ${JSON.stringify(json, null, 2)}`);
255
- if (!needUpdate(version)) {
256
- return false;
257
- } else {
258
- signature = _sig;
259
- return { size, version };
260
- }
261
- }
262
- async function downloadUpdate(src) {
263
- if (typeof src !== "object") {
264
- let _url = src ?? _release;
265
- if (!_url) {
266
- log("no releaseAsarURL, use repository");
265
+ updater.checkUpdate = async (url) => {
266
+ try {
267
+ url ??= _update;
268
+ if (!url) {
269
+ log("no updateJsonURL, fallback to use repository");
267
270
  if (!repository) {
268
- throw new Error("releaseAsarURL or repository are not set");
271
+ throw new Error("updateJsonURL or repository are not set");
269
272
  }
270
- _url = `${repository}/releases/download/latest/${productName}.asar.gz`;
273
+ url = `${repository.replace("github.com", "raw.githubusercontent.com")}/master/version.json`;
274
+ }
275
+ if ((0, import_node_fs2.existsSync)(tmpFile)) {
276
+ log(`remove tmp file: ${tmpFile}`);
277
+ await (0, import_promises.rm)(tmpFile);
278
+ }
279
+ if ((0, import_node_fs2.existsSync)(gzipPath)) {
280
+ log(`remove .gz file: ${gzipPath}`);
281
+ await (0, import_promises.rm)(gzipPath);
282
+ }
283
+ const json = await download(url, "json");
284
+ const {
285
+ signature: _sig,
286
+ version: _v,
287
+ size
288
+ } = json;
289
+ log(`update info: ${JSON.stringify(json, null, 2)}`);
290
+ if (!await needUpdate(_v)) {
291
+ log(`update unavailable: ${_v}`);
292
+ return void 0;
293
+ } else {
294
+ log(`update available: ${_v}`);
295
+ signature = _sig;
296
+ version = _v;
297
+ return { size, version };
271
298
  }
272
- log(`releaseAsarURL: ${_url}`);
273
- src = await download(_url, "buffer");
274
- }
275
- log("start verify");
276
- if (!verify(src, signature)) {
277
- throw new Error("file broken, invalid signature!");
278
- }
279
- log(`write file: ${gzipPath}`);
280
- await (0, import_promises.writeFile)(gzipPath, src);
281
- log(`extract file: ${gzipPath}`);
282
- await extractFile(gzipPath);
283
- updater.emit("downloaded");
284
- }
285
- const onCheck = async (url) => {
286
- try {
287
- const result = await checkUpdate(url);
288
- updater.emit("checkResult", result);
289
299
  } catch (error) {
290
300
  log(error);
291
- updater.emit("checkResult", error);
301
+ return error;
292
302
  }
293
303
  };
294
- updater.on("check", onCheck);
295
- updater.checkUpdate = onCheck;
296
- const onDownload = async (src) => {
304
+ updater.downloadUpdate = async (src) => {
297
305
  try {
298
- await downloadUpdate(src);
306
+ if (typeof src !== "object") {
307
+ let _url = src ?? _release;
308
+ if (!_url) {
309
+ log("no releaseAsarURL, fallback to use repository");
310
+ if (!repository) {
311
+ throw new Error("releaseAsarURL or repository are not set");
312
+ }
313
+ _url = `${repository}/releases/download/latest/${productName}.asar.gz`;
314
+ }
315
+ src = await download(_url, "buffer");
316
+ }
317
+ log("verify start");
318
+ if (!verify(src, signature, SIGNATURE_PUB, productName)) {
319
+ log("verify failed");
320
+ throw new Error("invalid signature");
321
+ }
322
+ log("verify success");
323
+ log(`write file: ${gzipPath}`);
324
+ await (0, import_promises.writeFile)(gzipPath, src);
325
+ log(`extract file: ${gzipPath}`);
326
+ await extractFile(gzipPath);
327
+ log(`update success, version: ${version}`);
328
+ return true;
299
329
  } catch (error) {
300
330
  log(error);
301
- updater.emit("donwnloadError", error);
331
+ return error;
302
332
  }
303
333
  };
304
- updater.on("download", onDownload);
305
- updater.downloadUpdate = onDownload;
306
334
  return updater;
307
335
  }
308
336
 
309
337
  // src/index.ts
310
- function initApp(productName, updater, option) {
338
+ function initApp(appOptions, updaterOptions) {
311
339
  const {
340
+ name: productName,
312
341
  electronDistPath = "dist-electron",
313
342
  mainPath = "main/index.js"
314
- } = option ?? {};
343
+ } = appOptions ?? {};
315
344
  const mainDir = import_electron3.app.isPackaged ? `../${productName}.asar` : electronDistPath;
316
345
  const entry = (0, import_node_path2.resolve)(__dirname, mainDir, mainPath);
317
- let _updater;
318
- if ("SIGNATURE_PUB" in updater) {
319
- const _option = updater.productName ? updater : { ...updater, productName };
320
- _updater = createUpdater(_option);
346
+ if (updaterOptions) {
347
+ require(entry)(
348
+ createUpdater({ ...updaterOptions, productName })
349
+ );
321
350
  } else {
322
- _updater = updater;
351
+ return {
352
+ setUpdater(updater) {
353
+ require(entry)(updater);
354
+ }
355
+ };
323
356
  }
324
- return require(entry)(_updater);
325
357
  }
326
358
  // Annotate the CommonJS export names for ESM import in node:
327
359
  0 && (module.exports = {