craft-native 0.0.87 → 0.0.88

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.cjs CHANGED
@@ -52,6 +52,7 @@ __export(exports_src, {
52
52
  watch: () => watch,
53
53
  vueOptimizations: () => vueOptimizations,
54
54
  verifyPassword: () => verifyPassword,
55
+ verifyBundleTrust: () => verifyBundleTrust,
55
56
  variants: () => variants,
56
57
  uuid: () => uuid,
57
58
  updater: () => exports_updater,
@@ -70,6 +71,7 @@ __export(exports_src, {
70
71
  tahoeStyles: () => tahoeStyles,
71
72
  tahoeStyle: () => tahoeStyle,
72
73
  tahoeDemoData: () => tahoeDemoData,
74
+ swapBundle: () => swapBundle,
73
75
  svelteOptimizations: () => svelteOptimizations,
74
76
  submenu: () => submenu,
75
77
  styles: () => styles,
@@ -117,6 +119,7 @@ __export(exports_src, {
117
119
  registerShortcut: () => registerShortcut,
118
120
  readText: () => readText,
119
121
  readHTML: () => readHTML,
122
+ readBundleIdentity: () => readBundleIdentity,
120
123
  readBinaryFile: () => readBinaryFile,
121
124
  reactOptimizations: () => reactOptimizations,
122
125
  randomString: () => randomString,
@@ -184,9 +187,13 @@ __export(exports_src, {
184
187
  foregroundService: () => foregroundService,
185
188
  focusFilters: () => focusFilters,
186
189
  focus: () => focus,
190
+ extractBundleFromZip: () => extractBundleFromZip,
191
+ extractBundleFromDmg: () => extractBundleFromDmg,
192
+ extractBundle: () => extractBundle,
187
193
  exit: () => exit,
188
194
  exec: () => exec,
189
195
  env: () => env,
196
+ dittoBundle: () => dittoBundle,
190
197
  dialog: () => dialog,
191
198
  device: () => device,
192
199
  desktopWidgets: () => desktopWidgets,
@@ -217,8 +224,10 @@ __export(exports_src, {
217
224
  commonOptimizations: () => commonOptimizations,
218
225
  clsx: () => cx,
219
226
  clipboard: () => clipboard,
227
+ clearQuarantine: () => clearQuarantine,
220
228
  checkbox: () => checkbox,
221
229
  carplay: () => carplay,
230
+ canReplaceBundle: () => canReplaceBundle,
222
231
  canQuickLook: () => canQuickLook,
223
232
  camera: () => camera,
224
233
  buildMenu: () => buildMenu,
@@ -9983,24 +9992,236 @@ var commonOptimizations = {
9983
9992
  // src/updater/index.ts
9984
9993
  var exports_updater = {};
9985
9994
  __export(exports_updater, {
9995
+ verifyBundleTrust: () => verifyBundleTrust,
9986
9996
  updaterCommand: () => updaterCommand,
9997
+ swapBundle: () => swapBundle,
9998
+ readBundleIdentity: () => readBundleIdentity,
9999
+ isMacOS: () => isMacOS,
9987
10000
  generateUpdateManifest: () => generateUpdateManifest,
10001
+ extractBundleFromZip: () => extractBundleFromZip,
10002
+ extractBundleFromDmg: () => extractBundleFromDmg,
10003
+ extractBundle: () => extractBundle,
10004
+ dittoBundle: () => dittoBundle,
9988
10005
  default: () => updater_default,
10006
+ clearQuarantine: () => clearQuarantine,
10007
+ canReplaceBundle: () => canReplaceBundle,
9989
10008
  DeltaGenerator: () => DeltaGenerator,
9990
10009
  AutoUpdater: () => AutoUpdater
9991
10010
  });
9992
- var import_fs3 = require("fs");
9993
- var import_path2 = require("path");
9994
- var import_child_process2 = require("child_process");
10011
+ var import_fs4 = require("fs");
10012
+ var import_path3 = require("path");
10013
+ var import_os3 = require("os");
10014
+ var import_child_process3 = require("child_process");
9995
10015
  var import_crypto3 = require("crypto");
9996
10016
  var import_events3 = require("events");
9997
- function findFirstByName(root, namePattern, kind) {
9998
- const out = import_child_process2.execFileSync("find", [root, "-name", namePattern, "-type", kind, "-print0"]);
9999
- const nullByte = String.fromCharCode(0);
10000
- const parts = out.toString("utf-8").split(nullByte).filter((s) => s.length > 0);
10001
- return parts[0] ?? "";
10017
+
10018
+ // src/updater/macos-bundle.ts
10019
+ var import_child_process2 = require("child_process");
10020
+ var import_fs3 = require("fs");
10021
+ var import_os2 = require("os");
10022
+ var import_path2 = require("path");
10023
+ var import_util = require("util");
10024
+ var execFileAsync = import_util.promisify(import_child_process2.execFile);
10025
+ function isMacOS() {
10026
+ return process.platform === "darwin";
10027
+ }
10028
+ async function readBundleIdentity(appPath) {
10029
+ const empty = { identifier: null, teamId: null, authority: null };
10030
+ if (!isMacOS())
10031
+ return empty;
10032
+ let report;
10033
+ try {
10034
+ const { stderr, stdout } = await execFileAsync("codesign", ["-dv", "--verbose=4", appPath]);
10035
+ report = `${stderr}
10036
+ ${stdout}`;
10037
+ } catch (error) {
10038
+ const e = error;
10039
+ report = `${e.stderr ?? ""}
10040
+ ${e.stdout ?? ""}`;
10041
+ if (!report.trim())
10042
+ return empty;
10043
+ }
10044
+ const field = (name) => {
10045
+ const match = report.match(new RegExp(`^${name}=(.+)$`, "m"));
10046
+ if (!match)
10047
+ return null;
10048
+ const value = match[1].trim();
10049
+ return value === "not set" || value.length === 0 ? null : value;
10050
+ };
10051
+ return {
10052
+ identifier: field("Identifier"),
10053
+ teamId: field("TeamIdentifier"),
10054
+ authority: field("Authority")
10055
+ };
10056
+ }
10057
+ async function verifyBundleTrust(appPath, policy = {}) {
10058
+ const identity = await readBundleIdentity(appPath);
10059
+ const requireNotarized = policy.requireNotarized ?? true;
10060
+ if (!import_fs3.existsSync(appPath)) {
10061
+ return { ok: false, identity, gatekeeperSource: null, reason: "unreadable", detail: `No such bundle: ${appPath}` };
10062
+ }
10063
+ try {
10064
+ await execFileAsync("codesign", ["--verify", "--deep", "--strict", "--verbose=2", appPath]);
10065
+ } catch (error) {
10066
+ return {
10067
+ ok: false,
10068
+ identity,
10069
+ gatekeeperSource: null,
10070
+ reason: "codesign-invalid",
10071
+ detail: describe(error)
10072
+ };
10073
+ }
10074
+ let gatekeeperSource = null;
10075
+ if (requireNotarized) {
10076
+ try {
10077
+ const { stderr, stdout } = await execFileAsync("spctl", ["-a", "-t", "exec", "-vv", appPath]);
10078
+ const report = `${stderr}
10079
+ ${stdout}`;
10080
+ gatekeeperSource = report.match(/^source=(.+)$/m)?.[1]?.trim() ?? null;
10081
+ } catch (error) {
10082
+ return {
10083
+ ok: false,
10084
+ identity,
10085
+ gatekeeperSource: null,
10086
+ reason: "gatekeeper-rejected",
10087
+ detail: describe(error)
10088
+ };
10089
+ }
10090
+ }
10091
+ if (policy.teamId && identity.teamId !== policy.teamId) {
10092
+ return {
10093
+ ok: false,
10094
+ identity,
10095
+ gatekeeperSource,
10096
+ reason: "team-mismatch",
10097
+ detail: `Expected Team ID ${policy.teamId}, bundle is signed by ${identity.teamId ?? "nobody"}`
10098
+ };
10099
+ }
10100
+ return { ok: true, identity, gatekeeperSource };
10101
+ }
10102
+ async function clearQuarantine(appPath) {
10103
+ if (!isMacOS())
10104
+ return;
10105
+ try {
10106
+ await execFileAsync("xattr", ["-d", "-r", "com.apple.quarantine", appPath]);
10107
+ } catch {}
10108
+ }
10109
+ async function dittoBundle(source, destination) {
10110
+ await execFileAsync("ditto", [source, destination]);
10111
+ }
10112
+ function findTopLevelApp(root) {
10113
+ const out = import_child_process2.execFileSync("find", [root, "-maxdepth", "2", "-name", "*.app", "-type", "d", "-print0"]);
10114
+ const parts = out.toString("utf-8").split("\x00").filter((s) => s.length > 0);
10115
+ parts.sort((a, b) => a.split("/").length - b.split("/").length);
10116
+ return parts[0] ?? null;
10117
+ }
10118
+ async function extractBundleFromDmg(dmgPath, stagingDir) {
10119
+ const staging = stagingDir ?? import_fs3.mkdtempSync(import_path2.join(import_os2.tmpdir(), "craft-update-"));
10120
+ const mountPoint = import_fs3.mkdtempSync(import_path2.join(import_os2.tmpdir(), "craft-mount-"));
10121
+ let mounted = false;
10122
+ try {
10123
+ await execFileAsync("hdiutil", ["attach", dmgPath, "-nobrowse", "-readonly", "-mountpoint", mountPoint]);
10124
+ mounted = true;
10125
+ const source = findTopLevelApp(mountPoint);
10126
+ if (!source)
10127
+ throw new Error(`No .app found in disk image: ${dmgPath}`);
10128
+ const appPath = import_path2.join(staging, import_path2.basename(source));
10129
+ await dittoBundle(source, appPath);
10130
+ return { appPath, stagingDir: staging };
10131
+ } finally {
10132
+ if (mounted) {
10133
+ try {
10134
+ await execFileAsync("hdiutil", ["detach", mountPoint, "-force"]);
10135
+ } catch {}
10136
+ }
10137
+ try {
10138
+ import_fs3.rmSync(mountPoint, { recursive: true, force: true });
10139
+ } catch {}
10140
+ }
10141
+ }
10142
+ async function extractBundleFromZip(zipPath, stagingDir) {
10143
+ const staging = stagingDir ?? import_fs3.mkdtempSync(import_path2.join(import_os2.tmpdir(), "craft-update-"));
10144
+ const unpacked = import_path2.join(staging, "payload");
10145
+ await execFileAsync("ditto", ["-x", "-k", zipPath, unpacked]);
10146
+ const source = findTopLevelApp(unpacked);
10147
+ if (!source)
10148
+ throw new Error(`No .app found in archive: ${zipPath}`);
10149
+ return { appPath: source, stagingDir: staging };
10150
+ }
10151
+ async function extractBundle(archivePath, stagingDir) {
10152
+ if (archivePath.endsWith(".dmg"))
10153
+ return extractBundleFromDmg(archivePath, stagingDir);
10154
+ if (archivePath.endsWith(".zip"))
10155
+ return extractBundleFromZip(archivePath, stagingDir);
10156
+ throw new Error(`Cannot extract an app bundle from ${archivePath}`);
10157
+ }
10158
+ async function swapBundle(stagedPath, installedPath) {
10159
+ if (!import_fs3.existsSync(stagedPath))
10160
+ throw new Error(`Staged bundle is missing: ${stagedPath}`);
10161
+ const parent = import_path2.dirname(installedPath);
10162
+ const stamp = `${Date.now()}.${process.pid}`;
10163
+ const incoming = import_path2.join(parent, `.${import_path2.basename(installedPath)}.${stamp}.incoming`);
10164
+ const retired = import_path2.join(parent, `.${import_path2.basename(installedPath)}.${stamp}.retired`);
10165
+ try {
10166
+ import_fs3.renameSync(stagedPath, incoming);
10167
+ } catch {
10168
+ await dittoBundle(stagedPath, incoming);
10169
+ }
10170
+ const hadPrevious = import_fs3.existsSync(installedPath);
10171
+ try {
10172
+ if (hadPrevious)
10173
+ import_fs3.renameSync(installedPath, retired);
10174
+ try {
10175
+ import_fs3.renameSync(incoming, installedPath);
10176
+ } catch (error) {
10177
+ if (hadPrevious) {
10178
+ try {
10179
+ import_fs3.renameSync(retired, installedPath);
10180
+ } catch {}
10181
+ }
10182
+ throw error;
10183
+ }
10184
+ } catch (error) {
10185
+ try {
10186
+ import_fs3.rmSync(incoming, { recursive: true, force: true });
10187
+ } catch {}
10188
+ throw error;
10189
+ }
10190
+ if (hadPrevious) {
10191
+ try {
10192
+ import_fs3.rmSync(retired, { recursive: true, force: true });
10193
+ return { previousPath: null };
10194
+ } catch {
10195
+ return { previousPath: retired };
10196
+ }
10197
+ }
10198
+ return { previousPath: null };
10199
+ }
10200
+ function canReplaceBundle(installedPath) {
10201
+ try {
10202
+ const parent = import_path2.dirname(installedPath);
10203
+ import_fs3.accessSync(parent, import_fs3.constants.W_OK);
10204
+ if (import_fs3.existsSync(installedPath))
10205
+ import_fs3.statSync(installedPath);
10206
+ return true;
10207
+ } catch {
10208
+ return false;
10209
+ }
10210
+ }
10211
+ function describe(error) {
10212
+ if (error && typeof error === "object") {
10213
+ const e = error;
10214
+ const stderr = e.stderr?.trim();
10215
+ if (stderr)
10216
+ return stderr;
10217
+ if (e.message)
10218
+ return e.message;
10219
+ }
10220
+ return String(error);
10002
10221
  }
10003
10222
 
10223
+ // src/updater/index.ts
10224
+ var PROGRESS_INTERVAL_MS = 100;
10004
10225
  class AutoUpdater extends import_events3.EventEmitter {
10005
10226
  config;
10006
10227
  updateInfo = null;
@@ -10009,12 +10230,22 @@ class AutoUpdater extends import_events3.EventEmitter {
10009
10230
  cachedEtag = null;
10010
10231
  cachedLastModified = null;
10011
10232
  cachedManifest = null;
10233
+ lastError = null;
10012
10234
  emitError(error) {
10235
+ this.lastError = error instanceof Error ? error : new Error(String(error));
10013
10236
  if (this.listenerCount("error") > 0)
10014
10237
  this.emit("error", error);
10015
10238
  else
10016
10239
  console.error("[Updater]", error);
10017
10240
  }
10241
+ downloadDir() {
10242
+ if (this.config.downloadDir)
10243
+ return this.config.downloadDir;
10244
+ return import_path3.join(import_os3.tmpdir(), "craft-updates", import_path3.basename(this.config.appPath) || "app");
10245
+ }
10246
+ getLastError() {
10247
+ return this.lastError;
10248
+ }
10018
10249
  constructor(config) {
10019
10250
  super();
10020
10251
  const url = config.updateUrl;
@@ -10048,6 +10279,7 @@ class AutoUpdater extends import_events3.EventEmitter {
10048
10279
  }
10049
10280
  async checkForUpdates() {
10050
10281
  this.emit("checking-for-update");
10282
+ this.lastError = null;
10051
10283
  try {
10052
10284
  const platform = this.getPlatform();
10053
10285
  const url = `${this.config.updateUrl}?v=${this.config.currentVersion}&channel=${this.config.channel}&platform=${platform}`;
@@ -10096,20 +10328,21 @@ class AutoUpdater extends import_events3.EventEmitter {
10096
10328
  if (!this.updateInfo) {
10097
10329
  throw new Error("No update available");
10098
10330
  }
10331
+ this.lastError = null;
10099
10332
  const platform = this.getPlatform();
10100
10333
  const platformUpdate = this.updateInfo.platforms[platform];
10101
10334
  if (!platformUpdate) {
10102
10335
  throw new Error(`No update available for platform: ${platform}`);
10103
10336
  }
10104
- const deltaUpdate = platformUpdate.delta?.find((d) => d.fromVersion === this.config.currentVersion);
10337
+ const deltaUpdate = DeltaGenerator.isSupported() ? platformUpdate.delta?.find((d) => d.fromVersion === this.config.currentVersion) : undefined;
10105
10338
  const _updateSource = deltaUpdate || platformUpdate;
10106
10339
  const url = deltaUpdate?.url || platformUpdate.url;
10107
10340
  const expectedSize = deltaUpdate?.size || platformUpdate.size;
10108
10341
  const expectedHash = deltaUpdate?.sha256 || platformUpdate.sha256;
10109
- const downloadDir = import_path2.join(this.config.appPath, "..", ".craft-updates");
10110
- import_fs3.mkdirSync(downloadDir, { recursive: true });
10111
- const fileName = import_path2.basename(url);
10112
- this.downloadPath = import_path2.join(downloadDir, fileName);
10342
+ const downloadDir = this.downloadDir();
10343
+ import_fs4.mkdirSync(downloadDir, { recursive: true });
10344
+ const fileName = import_path3.basename(url);
10345
+ this.downloadPath = import_path3.join(downloadDir, fileName);
10113
10346
  try {
10114
10347
  const response = await fetch(url);
10115
10348
  if (!response.ok) {
@@ -10119,63 +10352,83 @@ class AutoUpdater extends import_events3.EventEmitter {
10119
10352
  const total = contentLength || expectedSize;
10120
10353
  let downloaded = 0;
10121
10354
  const startTime = Date.now();
10122
- const fileStream = import_fs3.createWriteStream(this.downloadPath);
10355
+ const fileStream = import_fs4.createWriteStream(this.downloadPath);
10123
10356
  const reader = response.body?.getReader();
10124
10357
  if (!reader) {
10125
10358
  throw new Error("Failed to get response reader");
10126
10359
  }
10360
+ const digest = import_crypto3.createHash("sha256");
10361
+ let lastProgressAt = 0;
10127
10362
  while (true) {
10128
10363
  const { done, value } = await reader.read();
10129
10364
  if (done)
10130
10365
  break;
10131
- fileStream.write(value);
10366
+ digest.update(value);
10367
+ if (!fileStream.write(value)) {
10368
+ await new Promise((resolve, reject) => {
10369
+ const onDrain = () => {
10370
+ fileStream.off("error", onError);
10371
+ resolve();
10372
+ };
10373
+ const onError = (error) => {
10374
+ fileStream.off("drain", onDrain);
10375
+ reject(error);
10376
+ };
10377
+ fileStream.once("drain", onDrain);
10378
+ fileStream.once("error", onError);
10379
+ });
10380
+ }
10132
10381
  downloaded += value.length;
10133
- const elapsed = (Date.now() - startTime) / 1000;
10134
- const speed = downloaded / elapsed;
10135
- const progress = {
10136
- phase: "downloading",
10137
- percent: Math.round(downloaded / total * 100),
10138
- bytesDownloaded: downloaded,
10139
- bytesTotal: total,
10140
- speed
10141
- };
10142
- this.emit("download-progress", progress);
10382
+ const now = Date.now();
10383
+ if (now - lastProgressAt >= PROGRESS_INTERVAL_MS || downloaded === total) {
10384
+ lastProgressAt = now;
10385
+ const elapsed = (now - startTime) / 1000;
10386
+ const speed = elapsed > 0 ? downloaded / elapsed : 0;
10387
+ const progress = {
10388
+ phase: "downloading",
10389
+ percent: total > 0 ? Math.round(downloaded / total * 100) : 0,
10390
+ bytesDownloaded: downloaded,
10391
+ bytesTotal: total,
10392
+ speed
10393
+ };
10394
+ this.emit("download-progress", progress);
10395
+ }
10143
10396
  }
10144
10397
  fileStream.end();
10145
10398
  await new Promise((resolve, reject) => {
10146
10399
  fileStream.once("finish", resolve);
10147
10400
  fileStream.once("error", reject);
10148
10401
  });
10149
- const hash = await this.computeFileHash(this.downloadPath);
10402
+ const hash = digest.digest("hex");
10150
10403
  if (hash !== expectedHash) {
10151
- import_fs3.unlinkSync(this.downloadPath);
10404
+ import_fs4.unlinkSync(this.downloadPath);
10152
10405
  throw new Error("Download verification failed: hash mismatch");
10153
10406
  }
10154
10407
  if (deltaUpdate) {
10155
- if (!import_fs3.statSync(this.config.appPath).isFile()) {
10408
+ if (!import_fs4.statSync(this.config.appPath).isFile()) {
10156
10409
  throw new Error("Delta updates require appPath to point to the installed bundle file");
10157
10410
  }
10158
10411
  const patchPath = this.downloadPath;
10159
- const reconstructedPath = import_path2.join(downloadDir, `reconstructed-${import_path2.basename(platformUpdate.url)}`);
10412
+ const reconstructedPath = import_path3.join(downloadDir, `reconstructed-${import_path3.basename(platformUpdate.url)}`);
10160
10413
  await DeltaGenerator.apply(this.config.appPath, patchPath, reconstructedPath);
10161
10414
  const reconstructedHash = await this.computeFileHash(reconstructedPath);
10162
10415
  if (reconstructedHash !== platformUpdate.sha256) {
10163
10416
  try {
10164
- import_fs3.unlinkSync(reconstructedPath);
10417
+ import_fs4.unlinkSync(reconstructedPath);
10165
10418
  } catch {}
10166
10419
  throw new Error("Delta reconstruction failed: full bundle hash mismatch");
10167
10420
  }
10168
- import_fs3.unlinkSync(patchPath);
10421
+ import_fs4.unlinkSync(patchPath);
10169
10422
  this.downloadPath = reconstructedPath;
10170
10423
  }
10171
10424
  if (this.config.publicKeyPem && !platformUpdate.signature) {
10172
- import_fs3.unlinkSync(this.downloadPath);
10425
+ import_fs4.unlinkSync(this.downloadPath);
10173
10426
  throw new Error("Updater: publicKeyPem is configured but the manifest entry has no signature. " + "Refusing to install unsigned update.");
10174
10427
  }
10175
10428
  if (platformUpdate.signature) {
10176
10429
  const valid = await this.verifySignature(this.downloadPath, platformUpdate.signature);
10177
10430
  if (!valid) {
10178
- import_fs3.unlinkSync(this.downloadPath);
10431
+ import_fs4.unlinkSync(this.downloadPath);
10179
10432
  throw new Error("Download verification failed: invalid signature");
10180
10433
  }
10181
10434
  }
@@ -10194,7 +10447,7 @@ class AutoUpdater extends import_events3.EventEmitter {
10194
10447
  }
10195
10448
  }
10196
10449
  async installUpdate(restartAfter = true) {
10197
- if (!this.downloadPath || !import_fs3.existsSync(this.downloadPath)) {
10450
+ if (!this.downloadPath || !import_fs4.existsSync(this.downloadPath)) {
10198
10451
  throw new Error("No update downloaded");
10199
10452
  }
10200
10453
  if (this.updateInfo) {
@@ -10204,7 +10457,7 @@ class AutoUpdater extends import_events3.EventEmitter {
10204
10457
  const hash = await this.computeFileHash(this.downloadPath);
10205
10458
  if (hash !== platformUpdate.sha256) {
10206
10459
  try {
10207
- import_fs3.unlinkSync(this.downloadPath);
10460
+ import_fs4.unlinkSync(this.downloadPath);
10208
10461
  } catch {}
10209
10462
  throw new Error(`Updater.installUpdate: pre-install hash mismatch on ${this.downloadPath}. ` + "The downloaded bundle changed between download and install — refusing to run installer.");
10210
10463
  }
@@ -10215,7 +10468,7 @@ class AutoUpdater extends import_events3.EventEmitter {
10215
10468
  const valid = await this.verifySignature(this.downloadPath, platformUpdate.signature);
10216
10469
  if (!valid) {
10217
10470
  try {
10218
- import_fs3.unlinkSync(this.downloadPath);
10471
+ import_fs4.unlinkSync(this.downloadPath);
10219
10472
  } catch {}
10220
10473
  throw new Error("Updater.installUpdate: invalid signature on downloaded bundle");
10221
10474
  }
@@ -10238,14 +10491,17 @@ class AutoUpdater extends import_events3.EventEmitter {
10238
10491
  await this.installLinuxUpdate();
10239
10492
  break;
10240
10493
  }
10241
- if (this.downloadPath && import_fs3.existsSync(this.downloadPath)) {
10242
- import_fs3.unlinkSync(this.downloadPath);
10494
+ if (this.downloadPath && import_fs4.existsSync(this.downloadPath)) {
10495
+ import_fs4.unlinkSync(this.downloadPath);
10243
10496
  }
10497
+ try {
10498
+ import_fs4.rmdirSync(this.downloadDir());
10499
+ } catch {}
10244
10500
  progress.phase = "done";
10245
10501
  progress.percent = 100;
10246
10502
  this.emit("download-progress", progress);
10247
10503
  if (restartAfter) {
10248
- this.restartApp();
10504
+ await this.restartApp();
10249
10505
  }
10250
10506
  } catch (error) {
10251
10507
  progress.phase = "error";
@@ -10256,54 +10512,61 @@ class AutoUpdater extends import_events3.EventEmitter {
10256
10512
  async installMacOSUpdate() {
10257
10513
  const downloadPath = this.downloadPath;
10258
10514
  const appPath = this.config.appPath;
10259
- if (downloadPath.endsWith(".zip")) {
10260
- const tempDir2 = import_path2.join(import_path2.dirname(downloadPath), "extracted");
10261
- import_fs3.mkdirSync(tempDir2, { recursive: true });
10262
- import_child_process2.execFileSync("unzip", ["-o", downloadPath, "-d", tempDir2]);
10263
- const extractedApp = findFirstByName(tempDir2, "*.app", "d");
10264
- if (extractedApp) {
10265
- const { rmSync: rmSync3, renameSync } = await import("fs");
10266
- rmSync3(appPath, { recursive: true, force: true });
10267
- renameSync(extractedApp, appPath);
10268
- }
10269
- const { rmSync: rmSync2 } = await import("fs");
10270
- rmSync2(tempDir2, { recursive: true, force: true });
10271
- } else if (downloadPath.endsWith(".dmg")) {
10272
- const mountOutput = import_child_process2.execFileSync("hdiutil", ["attach", downloadPath, "-nobrowse"]).toString();
10273
- const mountPoint = mountOutput.match(/\/Volumes\/[^\n]+/)?.[0];
10274
- if (mountPoint) {
10275
- const dmgApp = findFirstByName(mountPoint, "*.app", "d");
10276
- if (dmgApp) {
10277
- const { rmSync: rmSync2, cpSync: cpSync2 } = await import("fs");
10278
- rmSync2(appPath, { recursive: true, force: true });
10279
- cpSync2(dmgApp, appPath, { recursive: true });
10280
- }
10281
- import_child_process2.execFileSync("hdiutil", ["detach", mountPoint]);
10282
- }
10283
- } else if (downloadPath.endsWith(".pkg")) {
10515
+ if (downloadPath.endsWith(".pkg")) {
10516
+ await this.installMacOSPackage(downloadPath);
10517
+ return;
10518
+ }
10519
+ if (!canReplaceBundle(appPath)) {
10520
+ throw new Error(`Cannot replace ${appPath}: ${import_path3.dirname(appPath)} is not writable by this user. ` + "Install the update manually, or move the app somewhere you own.");
10521
+ }
10522
+ const staged = await extractBundle(downloadPath);
10523
+ try {
10524
+ const trust = await this.assertBundleTrusted(staged.appPath);
10525
+ await clearQuarantine(staged.appPath);
10526
+ const { previousPath } = await swapBundle(staged.appPath, appPath);
10527
+ this.emit("update-installed", {
10528
+ path: appPath,
10529
+ version: this.updateInfo?.version,
10530
+ identity: trust.identity,
10531
+ leftoverPath: previousPath
10532
+ });
10533
+ } finally {
10284
10534
  try {
10285
- import_child_process2.execFileSync("pkgutil", ["--check-signature", downloadPath], { stdio: "pipe" });
10286
- } catch (e) {
10287
- throw new Error(`Refusing to install ${downloadPath}: pkgutil --check-signature failed. The .pkg is not signed by a trusted Apple certificate chain. Underlying error: ${e.message}`);
10288
- }
10289
- import_child_process2.execFileSync("sudo", ["installer", "-pkg", downloadPath, "-target", "/"]);
10535
+ import_fs4.rmSync(staged.stagingDir, { recursive: true, force: true });
10536
+ } catch {}
10537
+ }
10538
+ }
10539
+ async assertBundleTrusted(bundlePath) {
10540
+ const policy = this.config.macos ?? {};
10541
+ const trust = await verifyBundleTrust(bundlePath, policy);
10542
+ if (!trust.ok) {
10543
+ throw new Error(`Refusing to install ${bundlePath}: ${trust.reason} (${trust.detail ?? "no detail"}). ` + "The downloaded bundle is not one macOS will run as this application.");
10544
+ }
10545
+ return trust;
10546
+ }
10547
+ async installMacOSPackage(downloadPath) {
10548
+ try {
10549
+ import_child_process3.execFileSync("pkgutil", ["--check-signature", downloadPath], { stdio: "pipe" });
10550
+ } catch (e) {
10551
+ throw new Error(`Refusing to install ${downloadPath}: pkgutil --check-signature failed. ` + "The .pkg is not signed by a trusted Apple certificate chain. " + `Underlying error: ${e.message}`);
10290
10552
  }
10553
+ import_child_process3.execFileSync("sudo", ["installer", "-pkg", downloadPath, "-target", "/"]);
10291
10554
  }
10292
10555
  async installWindowsUpdate() {
10293
10556
  const downloadPath = this.downloadPath;
10294
10557
  if (downloadPath.endsWith(".exe")) {
10295
- import_child_process2.spawn(downloadPath, ["/S", "/SILENT", "/VERYSILENT"], {
10558
+ import_child_process3.spawn(downloadPath, ["/S", "/SILENT", "/VERYSILENT"], {
10296
10559
  detached: true,
10297
10560
  stdio: "ignore"
10298
10561
  });
10299
10562
  } else if (downloadPath.endsWith(".msi")) {
10300
- import_child_process2.spawn("msiexec", ["/i", downloadPath, "/quiet", "/norestart"], {
10563
+ import_child_process3.spawn("msiexec", ["/i", downloadPath, "/quiet", "/norestart"], {
10301
10564
  detached: true,
10302
10565
  stdio: "ignore"
10303
10566
  });
10304
10567
  } else if (downloadPath.endsWith(".zip")) {
10305
- const appDir = import_path2.dirname(this.config.appPath);
10306
- import_child_process2.execFileSync("powershell", [
10568
+ const appDir = import_path3.dirname(this.config.appPath);
10569
+ import_child_process3.execFileSync("powershell", [
10307
10570
  "-NoProfile",
10308
10571
  "-NonInteractive",
10309
10572
  "-Command",
@@ -10316,30 +10579,31 @@ class AutoUpdater extends import_events3.EventEmitter {
10316
10579
  async installLinuxUpdate() {
10317
10580
  const downloadPath = this.downloadPath;
10318
10581
  if (downloadPath.endsWith(".AppImage")) {
10319
- import_child_process2.execFileSync("chmod", ["+x", downloadPath]);
10320
- const { renameSync } = await import("fs");
10321
- renameSync(downloadPath, this.config.appPath);
10582
+ import_child_process3.execFileSync("chmod", ["+x", downloadPath]);
10583
+ const { renameSync: renameSync2 } = await import("fs");
10584
+ renameSync2(downloadPath, this.config.appPath);
10322
10585
  } else if (downloadPath.endsWith(".deb")) {
10323
- import_child_process2.execFileSync("sudo", ["dpkg", "-i", downloadPath]);
10586
+ import_child_process3.execFileSync("sudo", ["dpkg", "-i", downloadPath]);
10324
10587
  } else if (downloadPath.endsWith(".rpm")) {
10325
- import_child_process2.execFileSync("sudo", ["rpm", "-U", downloadPath]);
10588
+ import_child_process3.execFileSync("sudo", ["rpm", "-U", downloadPath]);
10326
10589
  } else if (downloadPath.endsWith(".tar.gz")) {
10327
- const appDir = import_path2.dirname(this.config.appPath);
10328
- import_child_process2.execFileSync("tar", ["-xzf", downloadPath, "-C", appDir]);
10590
+ const appDir = import_path3.dirname(this.config.appPath);
10591
+ import_child_process3.execFileSync("tar", ["-xzf", downloadPath, "-C", appDir]);
10329
10592
  }
10330
10593
  }
10331
- restartApp() {
10332
- const platform = this.getPlatform();
10594
+ async restartApp() {
10333
10595
  const appPath = this.config.appPath;
10334
- switch (platform) {
10596
+ if (this.config.relaunch) {
10597
+ await this.config.relaunch(appPath);
10598
+ return;
10599
+ }
10600
+ switch (this.getPlatform()) {
10335
10601
  case "darwin":
10336
- import_child_process2.spawn("open", ["-n", appPath], { detached: true, stdio: "ignore" });
10602
+ import_child_process3.spawn("open", ["-n", appPath], { detached: true, stdio: "ignore" });
10337
10603
  break;
10338
10604
  case "win32":
10339
- import_child_process2.spawn(appPath, [], { detached: true, stdio: "ignore" });
10340
- break;
10341
10605
  case "linux":
10342
- import_child_process2.spawn(appPath, [], { detached: true, stdio: "ignore" });
10606
+ import_child_process3.spawn(appPath, [], { detached: true, stdio: "ignore" });
10343
10607
  break;
10344
10608
  }
10345
10609
  process.exit(0);
@@ -10370,7 +10634,7 @@ class AutoUpdater extends import_events3.EventEmitter {
10370
10634
  async computeFileHash(filePath) {
10371
10635
  return new Promise((resolve, reject) => {
10372
10636
  const hash = import_crypto3.createHash("sha256");
10373
- const stream = import_fs3.createReadStream(filePath);
10637
+ const stream = import_fs4.createReadStream(filePath);
10374
10638
  stream.on("data", (chunk) => hash.update(chunk));
10375
10639
  stream.on("end", () => resolve(hash.digest("hex")));
10376
10640
  stream.on("error", reject);
@@ -10395,7 +10659,7 @@ class AutoUpdater extends import_events3.EventEmitter {
10395
10659
  } catch {
10396
10660
  return false;
10397
10661
  }
10398
- const data = import_fs3.readFileSync(filePath);
10662
+ const data = import_fs4.readFileSync(filePath);
10399
10663
  if (algo === "ed25519") {
10400
10664
  return verify(null, data, publicKey, sigBytes);
10401
10665
  }
@@ -10413,18 +10677,32 @@ class AutoUpdater extends import_events3.EventEmitter {
10413
10677
  }
10414
10678
 
10415
10679
  class DeltaGenerator {
10680
+ static supported = null;
10681
+ static isSupported() {
10682
+ if (DeltaGenerator.supported !== null)
10683
+ return DeltaGenerator.supported;
10684
+ DeltaGenerator.supported = ["bspatch", "xdelta3"].some((tool) => {
10685
+ try {
10686
+ import_child_process3.execFileSync("command", ["-v", tool], { stdio: "ignore", shell: true });
10687
+ return true;
10688
+ } catch {
10689
+ return false;
10690
+ }
10691
+ });
10692
+ return DeltaGenerator.supported;
10693
+ }
10416
10694
  static async generate(oldPath, newPath, outputPath) {
10417
10695
  try {
10418
- import_child_process2.execFileSync("bsdiff", [oldPath, newPath, outputPath]);
10419
- const stats = import_fs3.statSync(outputPath);
10696
+ import_child_process3.execFileSync("bsdiff", [oldPath, newPath, outputPath]);
10697
+ const stats = import_fs4.statSync(outputPath);
10420
10698
  const hash = await DeltaGenerator.computeHash(outputPath);
10421
10699
  return {
10422
10700
  size: stats.size,
10423
10701
  sha256: hash
10424
10702
  };
10425
10703
  } catch {
10426
- import_child_process2.execFileSync("xdelta3", ["-e", "-s", oldPath, newPath, outputPath]);
10427
- const stats = import_fs3.statSync(outputPath);
10704
+ import_child_process3.execFileSync("xdelta3", ["-e", "-s", oldPath, newPath, outputPath]);
10705
+ const stats = import_fs4.statSync(outputPath);
10428
10706
  const hash = await DeltaGenerator.computeHash(outputPath);
10429
10707
  return {
10430
10708
  size: stats.size,
@@ -10434,15 +10712,15 @@ class DeltaGenerator {
10434
10712
  }
10435
10713
  static async apply(basePath, deltaPath, outputPath) {
10436
10714
  try {
10437
- import_child_process2.execFileSync("bspatch", [basePath, outputPath, deltaPath]);
10715
+ import_child_process3.execFileSync("bspatch", [basePath, outputPath, deltaPath]);
10438
10716
  } catch {
10439
- import_child_process2.execFileSync("xdelta3", ["-d", "-s", basePath, deltaPath, outputPath]);
10717
+ import_child_process3.execFileSync("xdelta3", ["-d", "-s", basePath, deltaPath, outputPath]);
10440
10718
  }
10441
10719
  }
10442
10720
  static async computeHash(filePath) {
10443
10721
  return new Promise((resolve, reject) => {
10444
10722
  const hash = import_crypto3.createHash("sha256");
10445
- const stream = import_fs3.createReadStream(filePath);
10723
+ const stream = import_fs4.createReadStream(filePath);
10446
10724
  stream.on("data", (chunk) => hash.update(chunk));
10447
10725
  stream.on("end", () => resolve(hash.digest("hex")));
10448
10726
  stream.on("error", reject);
@@ -10459,7 +10737,7 @@ function generateUpdateManifest(options) {
10459
10737
  for (const [platform, info] of Object.entries(options.platforms)) {
10460
10738
  if (!info)
10461
10739
  continue;
10462
- const bytes = import_fs3.readFileSync(info.path);
10740
+ const bytes = import_fs4.readFileSync(info.path);
10463
10741
  const hash = import_crypto3.createHash("sha256").update(bytes).digest("hex");
10464
10742
  manifest.platforms[platform] = {
10465
10743
  url: info.url,
@@ -10469,7 +10747,7 @@ function generateUpdateManifest(options) {
10469
10747
  };
10470
10748
  const platformDeltas = options.deltas?.filter((d) => d.platform === platform) || [];
10471
10749
  for (const delta of platformDeltas) {
10472
- const deltaBytes = import_fs3.readFileSync(delta.path);
10750
+ const deltaBytes = import_fs4.readFileSync(delta.path);
10473
10751
  const deltaHash = import_crypto3.createHash("sha256").update(deltaBytes).digest("hex");
10474
10752
  manifest.platforms[platform].delta.push({
10475
10753
  fromVersion: delta.fromVersion,
@@ -10538,7 +10816,7 @@ ${update.releaseNotes}`);
10538
10816
  releaseDate: new Date().toISOString(),
10539
10817
  platforms: {}
10540
10818
  };
10541
- import_fs3.writeFileSync(outputPath, JSON.stringify(manifest, null, 2));
10819
+ import_fs4.writeFileSync(outputPath, JSON.stringify(manifest, null, 2));
10542
10820
  console.log(`Manifest generated: ${outputPath}`);
10543
10821
  console.log("Edit the file to add platform-specific update URLs");
10544
10822
  break;
@@ -10564,7 +10842,7 @@ Examples:
10564
10842
  var updater_default = AutoUpdater;
10565
10843
 
10566
10844
  // src/index.ts
10567
- var execFileAsync = import_node_util.promisify(import_node_child_process.execFile);
10845
+ var execFileAsync2 = import_node_util.promisify(import_node_child_process.execFile);
10568
10846
  var versionMismatchWarned = false;
10569
10847
  function getSdkVersion() {
10570
10848
  try {
@@ -10578,7 +10856,7 @@ async function probeBinaryVersion(craftPath) {
10578
10856
  if (versionMismatchWarned)
10579
10857
  return;
10580
10858
  try {
10581
- const { stdout } = await execFileAsync(craftPath, ["--version"], { timeout: 2000 });
10859
+ const { stdout } = await execFileAsync2(craftPath, ["--version"], { timeout: 2000 });
10582
10860
  const nativeVersion = parseCraftVersionOutput(stdout);
10583
10861
  const sdkVersion = getSdkVersion();
10584
10862
  if (sdkVersion !== "unknown" && nativeVersion && nativeVersion !== sdkVersion) {