electron-incremental-update 0.7.7 → 0.7.9

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/dist/index.js CHANGED
@@ -20,6 +20,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
+ IncrementalUpdater: () => IncrementalUpdater,
24
+ MinimumVersionError: () => MinimumVersionError,
25
+ VerifyFailedError: () => VerifyFailedError,
23
26
  createUpdater: () => createUpdater,
24
27
  initApp: () => initApp
25
28
  });
@@ -29,34 +32,10 @@ var import_node_fs3 = require("fs");
29
32
  var import_electron3 = require("electron");
30
33
 
31
34
  // src/updater/index.ts
32
- var import_node_events = require("events");
33
- var import_node_buffer3 = require("buffer");
34
35
  var import_node_fs2 = require("fs");
36
+ var import_node_buffer3 = require("buffer");
35
37
  var import_promises = require("fs/promises");
36
38
 
37
- // src/crypto.ts
38
- var import_node_crypto = require("crypto");
39
- var import_node_buffer = require("buffer");
40
- function decrypt(encryptedText, key2, iv) {
41
- const decipher = (0, import_node_crypto.createDecipheriv)("aes-256-cbc", key2, iv);
42
- let decrypted = decipher.update(encryptedText, "base64url", "utf8");
43
- decrypted += decipher.final("utf8");
44
- return decrypted;
45
- }
46
- function key(data, length) {
47
- const hash = (0, import_node_crypto.createHash)("SHA256").update(data).digest("binary");
48
- return import_node_buffer.Buffer.from(hash).subarray(0, length);
49
- }
50
- var verify = (buffer, signature, cert) => {
51
- try {
52
- const [sig, version] = decrypt(signature, key(cert, 32), key(buffer, 16)).split("%");
53
- const result = (0, import_node_crypto.createVerify)("RSA-SHA256").update(buffer).verify(cert, sig, "base64");
54
- return result ? version : false;
55
- } catch (error) {
56
- return false;
57
- }
58
- };
59
-
60
39
  // src/utils.ts
61
40
  var import_node_fs = require("fs");
62
41
  var import_node_path = require("path");
@@ -68,6 +47,9 @@ function getProductAsarPath(name) {
68
47
  function getEntryVersion() {
69
48
  return import_electron.app.getVersion();
70
49
  }
50
+ function getProductVersion(name) {
51
+ return import_electron.app.isPackaged ? (0, import_node_fs.readFileSync)((0, import_node_path.join)(getProductAsarPath(name), "version"), "utf-8") : getEntryVersion();
52
+ }
71
53
  function waitAppReady(duration = 1e3) {
72
54
  return new Promise((resolve2, reject) => {
73
55
  const timeout = setTimeout(() => {
@@ -75,11 +57,11 @@ function waitAppReady(duration = 1e3) {
75
57
  }, duration);
76
58
  import_electron.app.whenReady().then(() => {
77
59
  clearTimeout(timeout);
78
- resolve2(null);
60
+ resolve2();
79
61
  });
80
62
  });
81
63
  }
82
- async function unzipFile(gzipPath, targetFilePath) {
64
+ async function unzipFile(gzipPath, targetFilePath = gzipPath.slice(0, -3)) {
83
65
  if (!(0, import_node_fs.existsSync)(gzipPath)) {
84
66
  throw new Error(`path to zipped file not exist: ${gzipPath}`);
85
67
  }
@@ -95,18 +77,64 @@ async function unzipFile(gzipPath, targetFilePath) {
95
77
  });
96
78
  });
97
79
  }
80
+ function parseVersion(version) {
81
+ const semver = /^(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9\.-]+))?/i;
82
+ const match = semver.exec(version);
83
+ if (!match) {
84
+ throw new TypeError(`invalid version: ${version}`);
85
+ }
86
+ const [major, minor, patch] = match.slice(1, 4).map(Number);
87
+ const ret = {
88
+ major,
89
+ minor,
90
+ patch,
91
+ stage: "",
92
+ stageVersion: -1
93
+ };
94
+ if (match[4]) {
95
+ let [stage, _v] = match[4].split(".");
96
+ ret.stage = stage;
97
+ ret.stageVersion = Number(_v) || -1;
98
+ }
99
+ if (isNaN(major) || isNaN(minor) || isNaN(patch) || isNaN(ret.stageVersion)) {
100
+ throw new TypeError(`invalid version: ${version}`);
101
+ }
102
+ return ret;
103
+ }
98
104
 
99
- // src/updater/defaultFunctions.ts
100
- var import_node_buffer2 = require("buffer");
101
- var import_electron2 = require("electron");
105
+ // src/crypto.ts
106
+ var import_node_crypto = require("crypto");
107
+ var import_node_buffer = require("buffer");
108
+ function decrypt(encryptedText, key2, iv) {
109
+ const decipher = (0, import_node_crypto.createDecipheriv)("aes-256-cbc", key2, iv);
110
+ let decrypted = decipher.update(encryptedText, "base64url", "utf8");
111
+ decrypted += decipher.final("utf8");
112
+ return decrypted;
113
+ }
114
+ function key(data, length) {
115
+ const hash = (0, import_node_crypto.createHash)("SHA256").update(data).digest("binary");
116
+ return import_node_buffer.Buffer.from(hash).subarray(0, length);
117
+ }
118
+ var verify = (buffer, signature, cert) => {
119
+ try {
120
+ const [sig, version] = decrypt(signature, key(cert, 32), key(buffer, 16)).split("%");
121
+ const result = (0, import_node_crypto.createVerify)("RSA-SHA256").update(buffer).verify(cert, sig, "base64");
122
+ return result ? version : false;
123
+ } catch (error) {
124
+ return false;
125
+ }
126
+ };
102
127
 
103
- // src/updater/types.ts
128
+ // src/updateJson.ts
104
129
  function isUpdateJSON(json) {
105
- return "signature" in json && "version" in json && "size" in json;
130
+ const is = (j) => "signature" in j && "version" in j && "size" in j && "minimumVersion" in j;
131
+ return is(json) && "beta" in json && is(json.beta);
106
132
  }
107
133
 
108
134
  // src/updater/defaultFunctions.ts
109
- async function downloadJSONDefault(url, updater, headers) {
135
+ var import_node_buffer2 = require("buffer");
136
+ var import_electron2 = require("electron");
137
+ var downloadJSONDefault = async (url, headers) => {
110
138
  await waitAppReady();
111
139
  return new Promise((resolve2, reject) => {
112
140
  const request = import_electron2.net.request({
@@ -138,10 +166,10 @@ async function downloadJSONDefault(url, updater, headers) {
138
166
  });
139
167
  request.end();
140
168
  });
141
- }
142
- async function downloadBufferDefault(url, updater, headers) {
169
+ };
170
+ var downloadBufferDefault = async (url, headers, total, onDownloading) => {
143
171
  await waitAppReady();
144
- let progress = 0;
172
+ let current = 0;
145
173
  return new Promise((resolve2, reject) => {
146
174
  const request = import_electron2.net.request({
147
175
  url,
@@ -154,8 +182,12 @@ async function downloadBufferDefault(url, updater, headers) {
154
182
  request.on("response", (res) => {
155
183
  let data = [];
156
184
  res.on("data", (chunk) => {
157
- progress += chunk.length;
158
- updater.emit("downloading", progress);
185
+ current += chunk.length;
186
+ onDownloading?.({
187
+ percent: `${+(current / total).toFixed(2) * 100}%`,
188
+ total,
189
+ current
190
+ });
159
191
  data.push(chunk);
160
192
  });
161
193
  res.on("end", () => {
@@ -166,168 +198,192 @@ async function downloadBufferDefault(url, updater, headers) {
166
198
  });
167
199
  request.end();
168
200
  });
169
- }
170
- var compareVersionDefault = (oldVersion, newVersion) => {
171
- if (!oldVersion || !newVersion || typeof oldVersion !== "string" || typeof newVersion !== "string") {
172
- throw new TypeError("invalid version");
173
- }
174
- const parseVersion = (version) => {
175
- const [versionNumber, stage] = version.split("-", 2);
176
- if (!versionNumber || !versionNumber.includes(".")) {
177
- throw new TypeError("invalid version");
178
- }
179
- const [major, minor, patch] = versionNumber.split(".").map(Number);
180
- if (isNaN(major) || isNaN(minor) || isNaN(patch)) {
181
- throw new TypeError("invalid version");
201
+ };
202
+ var compareVersionDefault = (version1, version2) => {
203
+ const oldV = parseVersion(version1);
204
+ const newV = parseVersion(version2);
205
+ function compareStrings(str1, str2) {
206
+ if (str1 === "") {
207
+ return str2 !== "";
208
+ } else if (str2 === "") {
209
+ return true;
182
210
  }
183
- return { major, minor, patch, stage };
184
- };
185
- const oldV = parseVersion(oldVersion);
186
- const newV = parseVersion(newVersion);
187
- if (oldV.major < newV.major || oldV.major === newV.major && oldV.minor < newV.minor || oldV.major === newV.major && oldV.minor === newV.minor && oldV.patch < newV.patch) {
188
- return true;
211
+ return str1 < str2;
189
212
  }
190
- if (oldV.stage < newV.stage || !newV.stage && oldV.stage) {
191
- return true;
213
+ for (let key2 of Object.keys(oldV)) {
214
+ if (key2 === "stage" && compareStrings(oldV[key2], newV[key2])) {
215
+ return true;
216
+ } else if (oldV[key2] !== newV[key2]) {
217
+ return oldV[key2] < newV[key2];
218
+ }
192
219
  }
193
220
  return false;
194
221
  };
195
222
 
196
223
  // src/updater/index.ts
197
- function createUpdater(updaterOptions) {
198
- const {
199
- SIGNATURE_CERT,
200
- repository,
201
- productName,
202
- releaseAsarURL: _release,
203
- updateJsonURL: _update,
204
- debug = false,
205
- downloadConfig: { extraHeader, userAgent } = {},
206
- overrideFunctions: {
207
- compareVersion,
208
- verifySignaure,
209
- downloadBuffer,
210
- downloadJSON
211
- } = {}
212
- } = updaterOptions;
213
- const updater = new import_node_events.EventEmitter();
214
- let signature;
215
- let version;
216
- const asarPath = getProductAsarPath(productName);
217
- const gzipPath = `${asarPath}.gz`;
218
- const tmpFilePath = `${asarPath}.tmp`;
219
- function log(msg) {
220
- debug && updater.emit("debug", msg);
224
+ var MinimumVersionError = class extends Error {
225
+ currentVersion;
226
+ minVersion;
227
+ constructor(version, minimumVersion) {
228
+ super(`current entry version is ${version}, less than the minimumVersion ${minimumVersion}`);
229
+ this.currentVersion = version;
230
+ this.minVersion = minimumVersion;
231
+ }
232
+ };
233
+ var VerifyFailedError = class extends Error {
234
+ signature;
235
+ cert;
236
+ constructor(signature, cert) {
237
+ super("verify failed, invalid signature or certificate");
238
+ this.signature = signature;
239
+ this.cert = cert;
240
+ }
241
+ };
242
+ var IncrementalUpdater = class {
243
+ info;
244
+ option;
245
+ asarPath;
246
+ gzipPath;
247
+ tmpFilePath;
248
+ logger;
249
+ onDownloading;
250
+ get productName() {
251
+ return this.option.productName;
252
+ }
253
+ set productName(name) {
254
+ this.option.productName = name;
221
255
  }
222
- function needUpdate(version2) {
223
- const currentVersion = getEntryVersion();
224
- log(`check update: current version is ${currentVersion}, new version is ${version2}`);
225
- const _compare = compareVersion ?? compareVersionDefault;
226
- return _compare(currentVersion, version2);
256
+ get receiveBeta() {
257
+ return !!this.option.receiveBeta;
227
258
  }
228
- async function parseData(format, data, version2) {
229
- if ((0, import_node_fs2.existsSync)(tmpFilePath)) {
230
- log(`remove tmp file: ${tmpFilePath}`);
231
- await (0, import_promises.rm)(tmpFilePath);
259
+ set receiveBeta(receiveBeta) {
260
+ this.option.receiveBeta = receiveBeta;
261
+ }
262
+ constructor(option) {
263
+ this.option = option;
264
+ this.asarPath = getProductAsarPath(this.productName);
265
+ this.gzipPath = `${this.asarPath}.gz`;
266
+ this.tmpFilePath = `${this.asarPath}.tmp`;
267
+ }
268
+ async needUpdate(version, minVersion) {
269
+ const compare = this.option.overrideFunctions?.compareVersion ?? compareVersionDefault;
270
+ const productVersion = getProductVersion(this.option.productName);
271
+ const entryVersion = getEntryVersion();
272
+ if (await compare(entryVersion, minVersion)) {
273
+ throw new MinimumVersionError(entryVersion, minVersion);
274
+ }
275
+ this.logger?.info(`check update: current version is ${productVersion}, new version is ${version}`);
276
+ return await compare(productVersion, version);
277
+ }
278
+ async parseData(format, data) {
279
+ if ((0, import_node_fs2.existsSync)(this.tmpFilePath)) {
280
+ this.logger?.warn(`remove tmp file: ${this.tmpFilePath}`);
281
+ await (0, import_promises.rm)(this.tmpFilePath);
282
+ }
283
+ if ((0, import_node_fs2.existsSync)(this.gzipPath)) {
284
+ this.logger?.warn(`remove .gz file: ${this.gzipPath}`);
285
+ await (0, import_promises.rm)(this.gzipPath);
286
+ }
287
+ if (!["string", "object", "undefined"].includes(typeof data)) {
288
+ throw new TypeError(`invalid type at format '${format}': ${data}`);
232
289
  }
233
- if ((0, import_node_fs2.existsSync)(gzipPath)) {
234
- log(`remove .gz file: ${gzipPath}`);
235
- await (0, import_promises.rm)(gzipPath);
290
+ if (typeof data === "object" && (format === "json" && isUpdateJSON(data) || format === "buffer" && import_node_buffer3.Buffer.isBuffer(data))) {
291
+ return data;
236
292
  }
237
293
  if (typeof data === "object") {
238
- if (format === "json" && isUpdateJSON(data) || format === "buffer" && import_node_buffer3.Buffer.isBuffer(data)) {
239
- return data;
240
- } else {
241
- throw new Error(`invalid type at format '${format}': ${data}`);
242
- }
243
- } else if (["string", "undefined"].includes(typeof data)) {
244
- const ua = userAgent || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36";
245
- const headers = {
246
- Accept: `application/${format === "json" ? "json" : "octet-stream"}`,
247
- UserAgent: ua,
248
- ...extraHeader
249
- };
250
- log(`download headers: ${JSON.stringify(headers, null, 2)}`);
251
- const info = format === "json" ? {
252
- name: "updateJsonURL",
253
- url: _update,
254
- repoFallback: `${repository.replace("github.com", "raw.githubusercontent.com")}/master/version.json`,
255
- fn: downloadJSON ?? downloadJSONDefault
256
- } : {
257
- name: "releaseAsarURL",
258
- url: _release,
259
- repoFallback: `${repository}/releases/download/v${version2}/${productName}-${version2}.asar.gz`,
260
- fn: downloadBuffer ?? downloadBufferDefault
261
- };
262
- data ??= info.url;
263
- if (!data) {
264
- log(`no ${info.name}, fallback to use repository`);
265
- if (!repository) {
266
- throw new Error(`${info.name} or repository are not set`);
267
- }
268
- if (format === "buffer" && !version2) {
269
- throw new Error("version are not set");
270
- }
271
- data = info.repoFallback;
294
+ throw new TypeError(`invalid type at format '${format}': ${data}`);
295
+ }
296
+ const ua = this.option.downloadConfig?.userAgent || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36";
297
+ const headers = {
298
+ Accept: `application/${format === "json" ? "json" : "octet-stream"}`,
299
+ UserAgent: ua,
300
+ ...this.option.downloadConfig?.extraHeader
301
+ };
302
+ this.logger?.info(`download headers: ${JSON.stringify(headers, null, 2)}`);
303
+ const config = format === "json" ? {
304
+ name: "updateJsonURL",
305
+ url: this.option.updateJsonURL,
306
+ repoFallback: `${this.option.repository.replace("github.com", "raw.githubusercontent.com")}/master/version.json`,
307
+ fn: this.option.overrideFunctions?.downloadJSON ?? downloadJSONDefault
308
+ } : {
309
+ name: "releaseAsarURL",
310
+ url: this.option.releaseAsarURL,
311
+ repoFallback: `${this.option.repository}/releases/download/v${this.info?.version}/${this.productName}-${this.info?.version}.asar.gz`,
312
+ fn: this.option.overrideFunctions?.downloadBuffer ?? downloadBufferDefault
313
+ };
314
+ data ??= config.url;
315
+ if (!data) {
316
+ this.logger?.debug(`no ${config.name}, fallback to use repository`);
317
+ if (!this.option.repository) {
318
+ throw new Error(`${config.name} or repository are not set`);
272
319
  }
273
- log(`download ${format} from ${data}`);
274
- const ret = await info.fn(data, updater, headers);
275
- log(`download ${format} success${format === "buffer" ? `, file size: ${ret.length}` : ""}`);
276
- if (format === "buffer") {
277
- updater.emit("downloadBuffer", ret);
320
+ if (format === "buffer" && !this.info?.version) {
321
+ throw new Error("version are not set");
278
322
  }
279
- return ret;
280
- } else {
281
- throw new Error(`invalid type at format '${format}': ${data}`);
323
+ data = config.repoFallback;
282
324
  }
325
+ this.logger?.info(`download ${format} from ${data}`);
326
+ const ret = format === "json" ? await config.fn(data, headers) : await config.fn(data, headers, this.info.size, this.onDownloading);
327
+ this.logger?.info(`download ${format} success${format === "buffer" ? `, file size: ${ret.length}` : ""}`);
328
+ return ret;
283
329
  }
284
- updater.productName = productName;
285
- updater.debugMode = debug;
286
- updater.checkUpdate = async (data) => {
330
+ async checkUpdate(data) {
287
331
  try {
288
- const { signature: _sig, size, version: _ver } = await parseData("json", data);
289
- log(`checked version: ${_ver}, size: ${size}, signature: ${_sig}`);
290
- if (!needUpdate(_ver)) {
291
- log(`update unavailable: ${_ver}`);
332
+ let { signature, size, version, minimumVersion, beta } = await this.parseData("json", data);
333
+ if (this.receiveBeta) {
334
+ version = beta.version;
335
+ signature = beta.signature;
336
+ minimumVersion = beta.minimumVersion;
337
+ size = beta.size;
338
+ }
339
+ this.logger?.info(`checked version: ${version}, size: ${size}, signature: ${signature}`);
340
+ if (!await this.needUpdate(version, minimumVersion)) {
341
+ this.logger?.info(`update unavailable: ${version} is the latest version`);
292
342
  return void 0;
293
343
  } else {
294
- log(`update available: ${_ver}`);
295
- signature = _sig;
296
- version = _ver;
297
- return { size, version: _ver };
344
+ this.logger?.info(`update available: ${version}`);
345
+ this.info = {
346
+ signature,
347
+ minimumVersion,
348
+ version,
349
+ size
350
+ };
351
+ return { size, version };
298
352
  }
299
353
  } catch (error) {
300
- log(error);
354
+ this.logger?.error("check update failed", error);
301
355
  return error;
302
356
  }
303
- };
304
- updater.download = async (data, sig) => {
357
+ }
358
+ async download(data, sig) {
305
359
  try {
306
- const _sig = sig ?? signature;
360
+ const _sig = sig ?? this.info?.signature;
307
361
  if (!_sig) {
308
362
  throw new Error("signature are not set, please checkUpdate first or set the second parameter");
309
363
  }
310
- const buffer = await parseData("buffer", data, version);
311
- log("verify start");
312
- const _verify = verifySignaure ?? verify;
313
- const _ver = _verify(buffer, _sig, SIGNATURE_CERT);
364
+ const buffer = await this.parseData("buffer", data);
365
+ this.logger?.info("verify start");
366
+ const _verify = this.option.overrideFunctions?.verifySignaure ?? verify;
367
+ const _ver = await _verify(buffer, _sig, this.option.SIGNATURE_CERT);
314
368
  if (!_ver) {
315
- throw new Error("verify failed, invalid signature");
369
+ throw new VerifyFailedError(_sig, this.option.SIGNATURE_CERT);
316
370
  }
317
- log("verify success");
318
- log(`write to ${gzipPath}`);
319
- await (0, import_promises.writeFile)(gzipPath, buffer);
320
- log(`extract to ${tmpFilePath}`);
321
- await unzipFile(gzipPath, tmpFilePath);
322
- log(`download success, version: ${_ver}`);
323
- signature = "";
371
+ this.logger?.info("verify success");
372
+ this.logger?.info(`write to ${this.gzipPath}`);
373
+ await (0, import_promises.writeFile)(this.gzipPath, buffer);
374
+ this.logger?.info(`extract to ${this.tmpFilePath}`);
375
+ await unzipFile(this.gzipPath, this.tmpFilePath);
376
+ this.logger?.info(`download success${typeof _ver === "string" ? `, version: ${_ver}` : ""}`);
377
+ this.info = void 0;
324
378
  return true;
325
379
  } catch (error) {
326
- log(error);
380
+ this.logger?.error("download asar failed", error);
327
381
  return error;
328
382
  }
329
- };
330
- return updater;
383
+ }
384
+ };
385
+ function createUpdater(option) {
386
+ return new IncrementalUpdater(option);
331
387
  }
332
388
 
333
389
  // src/index.ts
@@ -374,6 +430,9 @@ function initApp(appOptions) {
374
430
  }
375
431
  // Annotate the CommonJS export names for ESM import in node:
376
432
  0 && (module.exports = {
433
+ IncrementalUpdater,
434
+ MinimumVersionError,
435
+ VerifyFailedError,
377
436
  createUpdater,
378
437
  initApp
379
438
  });