electron-incremental-update 1.3.0 → 2.0.0-beta.2

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
@@ -1,186 +1,97 @@
1
1
  import {
2
2
  __require,
3
+ getAppVersion,
4
+ getEntryVersion,
3
5
  getPathFromAppNameAsar,
4
- getVersions,
5
- is,
6
+ isDev,
6
7
  isUpdateJSON,
7
- parseVersion,
8
8
  restartApp,
9
- unzipFile,
10
- waitAppReady
11
- } from "./chunk-SBPTSLG7.js";
9
+ unzipFile
10
+ } from "./chunk-BG22XZAB.js";
12
11
 
13
- // src/index.ts
14
- import { resolve } from "node:path";
12
+ // src/entry.ts
13
+ import { join } from "node:path";
15
14
  import { existsSync as existsSync2, renameSync } from "node:fs";
16
15
  import { app as app2 } from "electron";
17
16
 
18
17
  // src/updater/core.ts
19
18
  import { existsSync, rmSync, writeFileSync } from "node:fs";
19
+ import { EventEmitter } from "node:stream";
20
20
  import { app } from "electron";
21
21
 
22
- // src/crypto/dec.ts
23
- import { createDecipheriv, createVerify } from "node:crypto";
24
-
25
- // src/crypto/utils.ts
26
- import { createHash } from "node:crypto";
27
- function hashString(data, length) {
28
- const hash = createHash("SHA256").update(data).digest("binary");
29
- return Buffer.from(hash).subarray(0, length);
30
- }
31
-
32
- // src/crypto/dec.ts
33
- function decrypt(encryptedText, key, iv) {
34
- const decipher = createDecipheriv("aes-256-cbc", key, iv);
35
- let decrypted = decipher.update(encryptedText, "base64url", "utf8");
36
- decrypted += decipher.final("utf8");
37
- return decrypted;
38
- }
39
- var verify = (buffer, signature, cert) => {
40
- try {
41
- const [sig, version] = decrypt(signature, hashString(cert, 32), hashString(buffer, 16)).split("%");
42
- const result = createVerify("RSA-SHA256").update(buffer).verify(cert, sig, "base64");
43
- return result ? version : false;
44
- } catch (error) {
45
- return false;
46
- }
47
- };
48
-
49
22
  // src/updater/types.ts
50
23
  var ErrorInfo = {
51
- downlaod: "Download failed",
24
+ download: "Download failed",
52
25
  validate: "Validate failed",
53
- param: "Missing params",
54
- version: "Unsatisfied version"
26
+ param: "Missing params"
55
27
  };
56
28
  var UpdaterError = class extends Error {
29
+ code;
57
30
  constructor(msg, info) {
58
- super(msg + ": " + info);
31
+ super(ErrorInfo[msg] + ": " + info);
32
+ this.code = msg;
59
33
  }
60
34
  };
61
35
 
62
- // src/updater/defaultFunctions/download.ts
63
- import { net } from "electron";
64
- async function downlaodFn(url, headers, onResponse) {
65
- await waitAppReady();
66
- return new Promise((resolve2, reject) => {
67
- const request = net.request({ url, method: "GET", redirect: "follow" });
68
- Object.keys(headers).forEach((key) => request.setHeader(key, headers[key]));
69
- request.on("response", (res) => onResponse(res, resolve2, reject));
70
- request.on("error", reject);
71
- request.end();
72
- });
73
- }
74
- var downloadJSONDefault = async (url, headers) => {
75
- return await downlaodFn(url, headers, (resp, resolve2, reject) => {
76
- let data = "";
77
- resp.on("data", (chunk) => data += chunk);
78
- resp.on("end", () => {
79
- try {
80
- const json = JSON.parse(data);
81
- if (isUpdateJSON(json)) {
82
- resolve2(json);
83
- } else {
84
- throw Error;
85
- }
86
- } catch (ignore) {
87
- reject(new Error("invalid update json"));
88
- }
89
- });
90
- resp.on("aborted", () => reject(new Error("aborted")));
91
- resp.on("error", () => reject(new Error("download error")));
92
- });
93
- };
94
- var downloadBufferDefault = async (url, headers, total, onDownloading) => {
95
- let current = 0;
96
- return await downlaodFn(url, headers, (resp, resolve2, reject) => {
97
- let data = [];
98
- resp.on("data", (chunk) => {
99
- current += chunk.length;
100
- onDownloading?.({ percent: `${+(current / total).toFixed(2) * 100}%`, total, current });
101
- data.push(chunk);
102
- });
103
- resp.on("end", () => resolve2(Buffer.concat(data)));
104
- resp.on("aborted", () => reject(new Error("aborted")));
105
- resp.on("error", () => reject(new Error("download error")));
106
- });
107
- };
108
-
109
- // src/updater/defaultFunctions/compareVersion.ts
110
- var isLowerVersionDefault = (version1, version2) => {
111
- const oldV = parseVersion(version1);
112
- const newV = parseVersion(version2);
113
- function compareStrings(str1, str2) {
114
- if (str1 === "") {
115
- return str2 !== "";
116
- } else if (str2 === "") {
117
- return true;
118
- }
119
- return str1 < str2;
120
- }
121
- for (let key of Object.keys(oldV)) {
122
- if (key === "stage" && compareStrings(oldV[key], newV[key])) {
123
- return true;
124
- } else if (oldV[key] !== newV[key]) {
125
- return oldV[key] < newV[key];
126
- }
127
- }
128
- return false;
129
- };
130
-
131
36
  // src/updater/core.ts
132
- var Updater = class {
133
- CERT = __SIGNATURE_CERT__;
37
+ var Updater = class extends EventEmitter {
38
+ CERT = __EIU_SIGNATURE_CERT__;
134
39
  info;
135
- option;
40
+ options;
136
41
  asarPath;
137
42
  gzipPath;
138
43
  tmpFilePath;
44
+ provider;
139
45
  /**
140
46
  * updater logger
141
47
  */
142
48
  logger;
143
49
  /**
144
- * downloading progress hook
145
- * @param progress download progress
146
- * @example
147
- * updater.onDownloading = ({ percent, total, current }) => {
148
- * console.log(`download progress: ${percent}, total: ${total}, current: ${current}`)
149
- * }
50
+ * URL handler hook
51
+ *
52
+ * for Github, there are some {@link https://github.com/XIU2/UserScript/blob/master/GithubEnhanced-High-Speed-Download.user.js#L34 public CDNs}
53
+ * @param url source url
54
+ * @param isDownloadAsar whether is download asar
150
55
  */
151
- onDownloading;
56
+ handleURL;
152
57
  /**
153
58
  * whether receive beta version
154
59
  */
155
60
  get receiveBeta() {
156
- return !!this.option.receiveBeta;
61
+ return !!this.options.receiveBeta;
157
62
  }
158
63
  set receiveBeta(receiveBeta) {
159
- this.option.receiveBeta = receiveBeta;
64
+ this.options.receiveBeta = receiveBeta;
160
65
  }
161
66
  /**
162
67
  * initialize incremental updater
68
+ * @param provider update provider
163
69
  * @param option UpdaterOption
164
70
  */
165
- constructor(option = {}) {
166
- this.option = option;
71
+ constructor(provider, option = {}) {
72
+ super();
73
+ this.provider = provider;
74
+ this.options = option;
167
75
  if (option.SIGNATURE_CERT) {
168
76
  this.CERT = option.SIGNATURE_CERT;
169
77
  }
78
+ if (option.logger) {
79
+ this.logger = option.logger;
80
+ }
81
+ if (isDev && !this.logger) {
82
+ this.logger = {
83
+ info: (...args) => console.log("[EIU-INFO ]", ...args),
84
+ debug: (...args) => console.log("[EIU-DEBUG]", ...args),
85
+ warn: (...args) => console.log("[EIU-WARN ]", ...args),
86
+ error: (...args) => console.error("[EIU-ERROR]", ...args)
87
+ };
88
+ this.logger.info("no logger set, enable dev-only logger");
89
+ }
170
90
  this.asarPath = getPathFromAppNameAsar();
171
91
  this.gzipPath = `${this.asarPath}.gz`;
172
92
  this.tmpFilePath = `${this.asarPath}.tmp`;
173
93
  }
174
- async needUpdate(version, minVersion) {
175
- const isLowerVersion = this.option.overrideFunctions?.isLowerVersion ?? isLowerVersionDefault;
176
- const { appVersion, entryVersion } = getVersions();
177
- if (await isLowerVersion(entryVersion, minVersion)) {
178
- throw new UpdaterError(ErrorInfo.version, `entry version (${entryVersion}) < minimumVersion (${minVersion})`);
179
- }
180
- this.logger?.info(`check update: current version is ${appVersion}, new version is ${version}`);
181
- return await isLowerVersion(appVersion, version);
182
- }
183
- async parseData(format, data) {
94
+ async fetch(format, data) {
184
95
  if (existsSync(this.tmpFilePath)) {
185
96
  this.logger?.warn(`remove tmp file: ${this.tmpFilePath}`);
186
97
  rmSync(this.tmpFilePath);
@@ -193,99 +104,96 @@ var Updater = class {
193
104
  if (format === "json" && isUpdateJSON(data) || format === "buffer" && Buffer.isBuffer(data)) {
194
105
  return data;
195
106
  } else {
196
- throw new UpdaterError(ErrorInfo.param, `invalid type at format '${format}': ${JSON.stringify(data)}`);
107
+ this.err("invalid type", "param", `invalid type at format '${format}': ${JSON.stringify(data)}`);
108
+ return;
197
109
  }
198
110
  }
199
- 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";
200
- const headers = {
201
- Accept: `application/${format === "json" ? "json" : "octet-stream"}`,
202
- UserAgent: ua,
203
- ...this.option.downloadConfig?.extraHeader
204
- };
205
- this.logger?.debug(`download headers: ${JSON.stringify(headers)}`);
206
- const config = format === "json" ? {
207
- name: "updateJsonURL",
208
- url: this.option.updateJsonURL,
209
- repoFallback: `${this.option.repository?.replace("github.com", "raw.githubusercontent.com")}/master/version.json`,
210
- fn: this.option.overrideFunctions?.downloadJSON ?? downloadJSONDefault
211
- } : {
212
- name: "releaseAsarURL",
213
- url: this.option.releaseAsarURL,
214
- repoFallback: `${this.option.repository}/releases/download/v${this.info?.version}/${app.name}-${this.info?.version}.asar.gz`,
215
- fn: this.option.overrideFunctions?.downloadBuffer ?? downloadBufferDefault
216
- };
217
- data ??= config.url;
218
- if (!data) {
219
- this.logger?.debug(`no ${config.name}, fallback to use repository`);
220
- if (!this.option.repository) {
221
- throw new UpdaterError(ErrorInfo.param, `${config.name} or repository is not set`);
222
- }
223
- if (format === "buffer" && !this.info?.version) {
224
- throw new UpdaterError(ErrorInfo.param, "version is not set");
225
- }
226
- data = config.repoFallback;
227
- }
228
- this.logger?.debug(`download ${format} from ${data}`);
111
+ this.logger?.debug(`download from ${this.provider.name}`);
229
112
  try {
230
- const ret = format === "json" ? await config.fn(data, headers) : await config.fn(data, headers, this.info.size, this.onDownloading);
231
- this.logger?.debug(`download ${format} success${format === "buffer" ? `, file size: ${ret.length}` : ""}`);
232
- return ret;
113
+ const result = format === "json" ? await this.provider.downloadJSON(data ?? __EIU_VERSION_PATH__) : await this.provider.downloadAsar(app.name, this.info, (data2) => this.emit("download-progress", data2));
114
+ this.logger?.debug(`download ${format} success${format === "buffer" ? `, file size: ${result.length}` : ""}`);
115
+ return result;
233
116
  } catch (e) {
234
- throw new UpdaterError(ErrorInfo.downlaod, e.toString());
117
+ this.err(`download ${format} failed`, "download", `download ${format} failed: ${e}`);
235
118
  }
236
119
  }
120
+ /**
121
+ * handle error message and emit error event
122
+ */
123
+ err(msg, code, errorInfo) {
124
+ const err = new UpdaterError(code, errorInfo);
125
+ this.logger?.error(msg, err);
126
+ this.emit("error", err);
127
+ }
237
128
  async checkUpdate(data) {
238
- try {
239
- let { signature, size, version, minimumVersion, beta } = await this.parseData("json", data);
240
- if (this.receiveBeta) {
241
- version = beta.version;
242
- signature = beta.signature;
243
- minimumVersion = beta.minimumVersion;
244
- size = beta.size;
245
- }
246
- this.logger?.debug(`checked version: ${version}, size: ${size}, signature: ${signature}`);
247
- if (!await this.needUpdate(version, minimumVersion)) {
248
- this.logger?.info(`update unavailable: ${version} is the latest version`);
249
- return void 0;
250
- } else {
251
- this.logger?.info(`update available: ${version}`);
252
- this.info = {
253
- signature,
254
- minimumVersion,
255
- version,
256
- size
257
- };
258
- return this.info;
259
- }
260
- } catch (error) {
261
- this.logger?.error("check update failed", error);
262
- return error;
129
+ const emitUnavailable = (msg) => {
130
+ this.logger?.info(msg);
131
+ this.emit("update-unavailable", msg);
132
+ };
133
+ const _data = await this.fetch("json", data);
134
+ if (!_data) {
135
+ emitUnavailable("failed to get update info");
136
+ return false;
137
+ }
138
+ let { signature, size, version, minimumVersion, beta } = _data;
139
+ if (this.receiveBeta) {
140
+ version = beta.version;
141
+ signature = beta.signature;
142
+ minimumVersion = beta.minimumVersion;
143
+ size = beta.size;
144
+ }
145
+ this.logger?.debug(`checked update, version: ${version}, size: ${size}, signature: ${signature}`);
146
+ if (isDev) {
147
+ emitUnavailable("in dev mode, skip check update");
148
+ return false;
263
149
  }
150
+ const isLowerVersion = this.provider.isLowerVersion;
151
+ const entryVersion = getEntryVersion();
152
+ const appVersion = getAppVersion();
153
+ if (isLowerVersion(entryVersion, minimumVersion)) {
154
+ emitUnavailable(`entry version (${entryVersion}) < minimumVersion (${minimumVersion})`);
155
+ return false;
156
+ }
157
+ this.logger?.info(`check update: current version is ${appVersion}, new version is ${version}`);
158
+ if (!isLowerVersion(appVersion, version)) {
159
+ emitUnavailable(`current version (${appVersion}) < new version (${version})`);
160
+ return false;
161
+ }
162
+ this.logger?.info(`update available: ${version}`);
163
+ this.info = { signature, minimumVersion, version, size };
164
+ this.emit("update-available", this.info);
165
+ return true;
264
166
  }
265
167
  async download(data, sig) {
168
+ if (!this.info) {
169
+ this.err("download failed", "param", "no update info, call `checkUpdate` first");
170
+ return false;
171
+ }
172
+ const _sig = sig ?? this.info.signature;
173
+ const buffer = await this.fetch("buffer", data ? Buffer.from(data) : void 0);
174
+ if (!buffer) {
175
+ this.err("download failed", "param", "no update asar file buffer");
176
+ return false;
177
+ }
178
+ this.logger?.debug("verify start");
179
+ const _ver = await this.provider.verifySignaure(buffer, _sig, this.CERT);
180
+ if (!_ver) {
181
+ this.err("verify failed", "validate", "invalid signature / certificate pair");
182
+ return false;
183
+ }
184
+ this.logger?.debug("verify success");
266
185
  try {
267
- const _sig = sig ?? this.info?.signature;
268
- if (!_sig) {
269
- throw new UpdaterError(ErrorInfo.param, "signature is empty");
270
- }
271
- const buffer = await this.parseData("buffer", data);
272
- this.logger?.debug("verify start");
273
- const _verify = this.option.overrideFunctions?.verifySignaure ?? verify;
274
- const _ver = await _verify(buffer, _sig, this.CERT);
275
- if (!_ver) {
276
- throw new UpdaterError(ErrorInfo.validate, "invalid signature or certificate");
277
- }
278
- this.logger?.debug("verify success");
279
186
  this.logger?.debug(`write to ${this.gzipPath}`);
280
187
  writeFileSync(this.gzipPath, buffer);
281
188
  this.logger?.debug(`extract to ${this.tmpFilePath}`);
282
189
  await unzipFile(this.gzipPath, this.tmpFilePath);
283
190
  this.logger?.info(`download success, version: ${_ver}`);
284
191
  this.info = void 0;
192
+ this.emit("update-downloaded");
285
193
  return true;
286
194
  } catch (error) {
287
- this.logger?.error("download asar failed", error);
288
- return error;
195
+ this.err("unwrap asar failed", "download", `fail to unwrap asar file, ${error}`);
196
+ return false;
289
197
  }
290
198
  }
291
199
  /**
@@ -297,66 +205,53 @@ var Updater = class {
297
205
  }
298
206
  };
299
207
 
300
- // src/updater/index.ts
301
- function createUpdater(option) {
302
- return new Updater(option);
303
- }
304
-
305
- // src/index.ts
208
+ // src/entry.ts
306
209
  function startupWithUpdater(fn) {
307
210
  return fn;
308
211
  }
309
212
  var defaultOnInstall = (install, _, __, logger) => {
310
213
  install();
311
- logger?.info(`update success!`);
214
+ logger.info(`update success!`);
312
215
  };
313
- async function initApp(appOptions = {}) {
216
+ async function initApp(appOptions) {
314
217
  const {
218
+ provider,
315
219
  updater,
316
- electronDevDistPath = "../dist-electron",
317
- mainPath = "main/index.js",
318
- hooks
319
- } = appOptions || {};
320
- const {
321
220
  onInstall = defaultOnInstall,
322
221
  beforeStart,
323
222
  onStartError
324
- } = hooks || {};
325
- function handleError(err, logger2) {
326
- console.error(err);
327
- onStartError?.(err, logger2);
328
- app2.quit();
329
- }
223
+ } = appOptions;
330
224
  let updaterInstance;
331
225
  if (typeof updater === "object" || !updater) {
332
- updaterInstance = createUpdater(updater);
226
+ updaterInstance = new Updater(provider, updater);
333
227
  } else {
334
228
  updaterInstance = await updater();
335
229
  }
336
- const logger = updaterInstance.logger;
230
+ const logger = updaterInstance.logger || console;
337
231
  try {
338
232
  const appNameAsarPath = getPathFromAppNameAsar();
339
233
  const tempAsarPath = `${appNameAsarPath}.tmp`;
340
234
  if (existsSync2(tempAsarPath)) {
341
- logger?.info(`installing new asar: ${tempAsarPath}`);
235
+ logger.info(`installing new asar: ${tempAsarPath}`);
342
236
  await onInstall(() => renameSync(tempAsarPath, appNameAsarPath), tempAsarPath, appNameAsarPath, logger);
343
237
  }
344
- const mainDir = is.dev ? electronDevDistPath : appNameAsarPath;
345
- const entry = resolve(__dirname, mainDir, mainPath);
346
- await beforeStart?.(entry, logger);
347
- __require(entry)(updaterInstance);
238
+ const mainFilePath = join(
239
+ isDev ? join(app2.getAppPath(), __EIU_MAIN_DEV_DIR__) : appNameAsarPath,
240
+ "main",
241
+ __EIU_MAIN_FILE__
242
+ );
243
+ await beforeStart?.(mainFilePath, logger);
244
+ __require(mainFilePath)(updaterInstance);
348
245
  } catch (error) {
349
- handleError(error, logger);
246
+ logger.error("startup error", error);
247
+ onStartError?.(error, logger);
248
+ app2.quit();
350
249
  }
351
250
  }
352
251
  export {
353
252
  ErrorInfo,
354
253
  Updater,
355
254
  UpdaterError,
356
- createUpdater,
357
- downloadBufferDefault,
358
- downloadJSONDefault,
359
255
  initApp,
360
- isLowerVersionDefault,
361
256
  startupWithUpdater
362
257
  };