craft-native 0.0.87 → 0.0.89

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,
@@ -2603,9 +2612,12 @@ class WindowManager {
2603
2612
  return this._windows.get(id);
2604
2613
  }
2605
2614
  async create(options = {}) {
2606
- const id = `window_${++this._idCounter}_${Date.now()}`;
2615
+ const id = options.id || `window_${++this._idCounter}_${Date.now()}`;
2616
+ const existing = this._windows.get(id);
2607
2617
  const bridge = getBridge();
2608
- await bridge.request("window.create", { id, ...options });
2618
+ await bridge.request("window.create", { ...options, id });
2619
+ if (existing)
2620
+ return existing;
2609
2621
  const win = new Window(id);
2610
2622
  this._windows.set(id, win);
2611
2623
  return win;
@@ -9983,24 +9995,236 @@ var commonOptimizations = {
9983
9995
  // src/updater/index.ts
9984
9996
  var exports_updater = {};
9985
9997
  __export(exports_updater, {
9998
+ verifyBundleTrust: () => verifyBundleTrust,
9986
9999
  updaterCommand: () => updaterCommand,
10000
+ swapBundle: () => swapBundle,
10001
+ readBundleIdentity: () => readBundleIdentity,
10002
+ isMacOS: () => isMacOS,
9987
10003
  generateUpdateManifest: () => generateUpdateManifest,
10004
+ extractBundleFromZip: () => extractBundleFromZip,
10005
+ extractBundleFromDmg: () => extractBundleFromDmg,
10006
+ extractBundle: () => extractBundle,
10007
+ dittoBundle: () => dittoBundle,
9988
10008
  default: () => updater_default,
10009
+ clearQuarantine: () => clearQuarantine,
10010
+ canReplaceBundle: () => canReplaceBundle,
9989
10011
  DeltaGenerator: () => DeltaGenerator,
9990
10012
  AutoUpdater: () => AutoUpdater
9991
10013
  });
9992
- var import_fs3 = require("fs");
9993
- var import_path2 = require("path");
9994
- var import_child_process2 = require("child_process");
10014
+ var import_fs4 = require("fs");
10015
+ var import_path3 = require("path");
10016
+ var import_os3 = require("os");
10017
+ var import_child_process3 = require("child_process");
9995
10018
  var import_crypto3 = require("crypto");
9996
10019
  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] ?? "";
10020
+
10021
+ // src/updater/macos-bundle.ts
10022
+ var import_child_process2 = require("child_process");
10023
+ var import_fs3 = require("fs");
10024
+ var import_os2 = require("os");
10025
+ var import_path2 = require("path");
10026
+ var import_util = require("util");
10027
+ var execFileAsync = import_util.promisify(import_child_process2.execFile);
10028
+ function isMacOS() {
10029
+ return process.platform === "darwin";
10030
+ }
10031
+ async function readBundleIdentity(appPath) {
10032
+ const empty = { identifier: null, teamId: null, authority: null };
10033
+ if (!isMacOS())
10034
+ return empty;
10035
+ let report;
10036
+ try {
10037
+ const { stderr, stdout } = await execFileAsync("codesign", ["-dv", "--verbose=4", appPath]);
10038
+ report = `${stderr}
10039
+ ${stdout}`;
10040
+ } catch (error) {
10041
+ const e = error;
10042
+ report = `${e.stderr ?? ""}
10043
+ ${e.stdout ?? ""}`;
10044
+ if (!report.trim())
10045
+ return empty;
10046
+ }
10047
+ const field = (name) => {
10048
+ const match = report.match(new RegExp(`^${name}=(.+)$`, "m"));
10049
+ if (!match)
10050
+ return null;
10051
+ const value = match[1].trim();
10052
+ return value === "not set" || value.length === 0 ? null : value;
10053
+ };
10054
+ return {
10055
+ identifier: field("Identifier"),
10056
+ teamId: field("TeamIdentifier"),
10057
+ authority: field("Authority")
10058
+ };
10059
+ }
10060
+ async function verifyBundleTrust(appPath, policy = {}) {
10061
+ const identity = await readBundleIdentity(appPath);
10062
+ const requireNotarized = policy.requireNotarized ?? true;
10063
+ if (!import_fs3.existsSync(appPath)) {
10064
+ return { ok: false, identity, gatekeeperSource: null, reason: "unreadable", detail: `No such bundle: ${appPath}` };
10065
+ }
10066
+ try {
10067
+ await execFileAsync("codesign", ["--verify", "--deep", "--strict", "--verbose=2", appPath]);
10068
+ } catch (error) {
10069
+ return {
10070
+ ok: false,
10071
+ identity,
10072
+ gatekeeperSource: null,
10073
+ reason: "codesign-invalid",
10074
+ detail: describe(error)
10075
+ };
10076
+ }
10077
+ let gatekeeperSource = null;
10078
+ if (requireNotarized) {
10079
+ try {
10080
+ const { stderr, stdout } = await execFileAsync("spctl", ["-a", "-t", "exec", "-vv", appPath]);
10081
+ const report = `${stderr}
10082
+ ${stdout}`;
10083
+ gatekeeperSource = report.match(/^source=(.+)$/m)?.[1]?.trim() ?? null;
10084
+ } catch (error) {
10085
+ return {
10086
+ ok: false,
10087
+ identity,
10088
+ gatekeeperSource: null,
10089
+ reason: "gatekeeper-rejected",
10090
+ detail: describe(error)
10091
+ };
10092
+ }
10093
+ }
10094
+ if (policy.teamId && identity.teamId !== policy.teamId) {
10095
+ return {
10096
+ ok: false,
10097
+ identity,
10098
+ gatekeeperSource,
10099
+ reason: "team-mismatch",
10100
+ detail: `Expected Team ID ${policy.teamId}, bundle is signed by ${identity.teamId ?? "nobody"}`
10101
+ };
10102
+ }
10103
+ return { ok: true, identity, gatekeeperSource };
10104
+ }
10105
+ async function clearQuarantine(appPath) {
10106
+ if (!isMacOS())
10107
+ return;
10108
+ try {
10109
+ await execFileAsync("xattr", ["-d", "-r", "com.apple.quarantine", appPath]);
10110
+ } catch {}
10111
+ }
10112
+ async function dittoBundle(source, destination) {
10113
+ await execFileAsync("ditto", [source, destination]);
10114
+ }
10115
+ function findTopLevelApp(root) {
10116
+ const out = import_child_process2.execFileSync("find", [root, "-maxdepth", "2", "-name", "*.app", "-type", "d", "-print0"]);
10117
+ const parts = out.toString("utf-8").split("\x00").filter((s) => s.length > 0);
10118
+ parts.sort((a, b) => a.split("/").length - b.split("/").length);
10119
+ return parts[0] ?? null;
10120
+ }
10121
+ async function extractBundleFromDmg(dmgPath, stagingDir) {
10122
+ const staging = stagingDir ?? import_fs3.mkdtempSync(import_path2.join(import_os2.tmpdir(), "craft-update-"));
10123
+ const mountPoint = import_fs3.mkdtempSync(import_path2.join(import_os2.tmpdir(), "craft-mount-"));
10124
+ let mounted = false;
10125
+ try {
10126
+ await execFileAsync("hdiutil", ["attach", dmgPath, "-nobrowse", "-readonly", "-mountpoint", mountPoint]);
10127
+ mounted = true;
10128
+ const source = findTopLevelApp(mountPoint);
10129
+ if (!source)
10130
+ throw new Error(`No .app found in disk image: ${dmgPath}`);
10131
+ const appPath = import_path2.join(staging, import_path2.basename(source));
10132
+ await dittoBundle(source, appPath);
10133
+ return { appPath, stagingDir: staging };
10134
+ } finally {
10135
+ if (mounted) {
10136
+ try {
10137
+ await execFileAsync("hdiutil", ["detach", mountPoint, "-force"]);
10138
+ } catch {}
10139
+ }
10140
+ try {
10141
+ import_fs3.rmSync(mountPoint, { recursive: true, force: true });
10142
+ } catch {}
10143
+ }
10144
+ }
10145
+ async function extractBundleFromZip(zipPath, stagingDir) {
10146
+ const staging = stagingDir ?? import_fs3.mkdtempSync(import_path2.join(import_os2.tmpdir(), "craft-update-"));
10147
+ const unpacked = import_path2.join(staging, "payload");
10148
+ await execFileAsync("ditto", ["-x", "-k", zipPath, unpacked]);
10149
+ const source = findTopLevelApp(unpacked);
10150
+ if (!source)
10151
+ throw new Error(`No .app found in archive: ${zipPath}`);
10152
+ return { appPath: source, stagingDir: staging };
10153
+ }
10154
+ async function extractBundle(archivePath, stagingDir) {
10155
+ if (archivePath.endsWith(".dmg"))
10156
+ return extractBundleFromDmg(archivePath, stagingDir);
10157
+ if (archivePath.endsWith(".zip"))
10158
+ return extractBundleFromZip(archivePath, stagingDir);
10159
+ throw new Error(`Cannot extract an app bundle from ${archivePath}`);
10160
+ }
10161
+ async function swapBundle(stagedPath, installedPath) {
10162
+ if (!import_fs3.existsSync(stagedPath))
10163
+ throw new Error(`Staged bundle is missing: ${stagedPath}`);
10164
+ const parent = import_path2.dirname(installedPath);
10165
+ const stamp = `${Date.now()}.${process.pid}`;
10166
+ const incoming = import_path2.join(parent, `.${import_path2.basename(installedPath)}.${stamp}.incoming`);
10167
+ const retired = import_path2.join(parent, `.${import_path2.basename(installedPath)}.${stamp}.retired`);
10168
+ try {
10169
+ import_fs3.renameSync(stagedPath, incoming);
10170
+ } catch {
10171
+ await dittoBundle(stagedPath, incoming);
10172
+ }
10173
+ const hadPrevious = import_fs3.existsSync(installedPath);
10174
+ try {
10175
+ if (hadPrevious)
10176
+ import_fs3.renameSync(installedPath, retired);
10177
+ try {
10178
+ import_fs3.renameSync(incoming, installedPath);
10179
+ } catch (error) {
10180
+ if (hadPrevious) {
10181
+ try {
10182
+ import_fs3.renameSync(retired, installedPath);
10183
+ } catch {}
10184
+ }
10185
+ throw error;
10186
+ }
10187
+ } catch (error) {
10188
+ try {
10189
+ import_fs3.rmSync(incoming, { recursive: true, force: true });
10190
+ } catch {}
10191
+ throw error;
10192
+ }
10193
+ if (hadPrevious) {
10194
+ try {
10195
+ import_fs3.rmSync(retired, { recursive: true, force: true });
10196
+ return { previousPath: null };
10197
+ } catch {
10198
+ return { previousPath: retired };
10199
+ }
10200
+ }
10201
+ return { previousPath: null };
10202
+ }
10203
+ function canReplaceBundle(installedPath) {
10204
+ try {
10205
+ const parent = import_path2.dirname(installedPath);
10206
+ import_fs3.accessSync(parent, import_fs3.constants.W_OK);
10207
+ if (import_fs3.existsSync(installedPath))
10208
+ import_fs3.statSync(installedPath);
10209
+ return true;
10210
+ } catch {
10211
+ return false;
10212
+ }
10213
+ }
10214
+ function describe(error) {
10215
+ if (error && typeof error === "object") {
10216
+ const e = error;
10217
+ const stderr = e.stderr?.trim();
10218
+ if (stderr)
10219
+ return stderr;
10220
+ if (e.message)
10221
+ return e.message;
10222
+ }
10223
+ return String(error);
10002
10224
  }
10003
10225
 
10226
+ // src/updater/index.ts
10227
+ var PROGRESS_INTERVAL_MS = 100;
10004
10228
  class AutoUpdater extends import_events3.EventEmitter {
10005
10229
  config;
10006
10230
  updateInfo = null;
@@ -10009,12 +10233,22 @@ class AutoUpdater extends import_events3.EventEmitter {
10009
10233
  cachedEtag = null;
10010
10234
  cachedLastModified = null;
10011
10235
  cachedManifest = null;
10236
+ lastError = null;
10012
10237
  emitError(error) {
10238
+ this.lastError = error instanceof Error ? error : new Error(String(error));
10013
10239
  if (this.listenerCount("error") > 0)
10014
10240
  this.emit("error", error);
10015
10241
  else
10016
10242
  console.error("[Updater]", error);
10017
10243
  }
10244
+ downloadDir() {
10245
+ if (this.config.downloadDir)
10246
+ return this.config.downloadDir;
10247
+ return import_path3.join(import_os3.tmpdir(), "craft-updates", import_path3.basename(this.config.appPath) || "app");
10248
+ }
10249
+ getLastError() {
10250
+ return this.lastError;
10251
+ }
10018
10252
  constructor(config) {
10019
10253
  super();
10020
10254
  const url = config.updateUrl;
@@ -10048,6 +10282,7 @@ class AutoUpdater extends import_events3.EventEmitter {
10048
10282
  }
10049
10283
  async checkForUpdates() {
10050
10284
  this.emit("checking-for-update");
10285
+ this.lastError = null;
10051
10286
  try {
10052
10287
  const platform = this.getPlatform();
10053
10288
  const url = `${this.config.updateUrl}?v=${this.config.currentVersion}&channel=${this.config.channel}&platform=${platform}`;
@@ -10096,20 +10331,21 @@ class AutoUpdater extends import_events3.EventEmitter {
10096
10331
  if (!this.updateInfo) {
10097
10332
  throw new Error("No update available");
10098
10333
  }
10334
+ this.lastError = null;
10099
10335
  const platform = this.getPlatform();
10100
10336
  const platformUpdate = this.updateInfo.platforms[platform];
10101
10337
  if (!platformUpdate) {
10102
10338
  throw new Error(`No update available for platform: ${platform}`);
10103
10339
  }
10104
- const deltaUpdate = platformUpdate.delta?.find((d) => d.fromVersion === this.config.currentVersion);
10340
+ const deltaUpdate = DeltaGenerator.isSupported() ? platformUpdate.delta?.find((d) => d.fromVersion === this.config.currentVersion) : undefined;
10105
10341
  const _updateSource = deltaUpdate || platformUpdate;
10106
10342
  const url = deltaUpdate?.url || platformUpdate.url;
10107
10343
  const expectedSize = deltaUpdate?.size || platformUpdate.size;
10108
10344
  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);
10345
+ const downloadDir = this.downloadDir();
10346
+ import_fs4.mkdirSync(downloadDir, { recursive: true });
10347
+ const fileName = import_path3.basename(url);
10348
+ this.downloadPath = import_path3.join(downloadDir, fileName);
10113
10349
  try {
10114
10350
  const response = await fetch(url);
10115
10351
  if (!response.ok) {
@@ -10119,63 +10355,83 @@ class AutoUpdater extends import_events3.EventEmitter {
10119
10355
  const total = contentLength || expectedSize;
10120
10356
  let downloaded = 0;
10121
10357
  const startTime = Date.now();
10122
- const fileStream = import_fs3.createWriteStream(this.downloadPath);
10358
+ const fileStream = import_fs4.createWriteStream(this.downloadPath);
10123
10359
  const reader = response.body?.getReader();
10124
10360
  if (!reader) {
10125
10361
  throw new Error("Failed to get response reader");
10126
10362
  }
10363
+ const digest = import_crypto3.createHash("sha256");
10364
+ let lastProgressAt = 0;
10127
10365
  while (true) {
10128
10366
  const { done, value } = await reader.read();
10129
10367
  if (done)
10130
10368
  break;
10131
- fileStream.write(value);
10369
+ digest.update(value);
10370
+ if (!fileStream.write(value)) {
10371
+ await new Promise((resolve, reject) => {
10372
+ const onDrain = () => {
10373
+ fileStream.off("error", onError);
10374
+ resolve();
10375
+ };
10376
+ const onError = (error) => {
10377
+ fileStream.off("drain", onDrain);
10378
+ reject(error);
10379
+ };
10380
+ fileStream.once("drain", onDrain);
10381
+ fileStream.once("error", onError);
10382
+ });
10383
+ }
10132
10384
  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);
10385
+ const now = Date.now();
10386
+ if (now - lastProgressAt >= PROGRESS_INTERVAL_MS || downloaded === total) {
10387
+ lastProgressAt = now;
10388
+ const elapsed = (now - startTime) / 1000;
10389
+ const speed = elapsed > 0 ? downloaded / elapsed : 0;
10390
+ const progress = {
10391
+ phase: "downloading",
10392
+ percent: total > 0 ? Math.round(downloaded / total * 100) : 0,
10393
+ bytesDownloaded: downloaded,
10394
+ bytesTotal: total,
10395
+ speed
10396
+ };
10397
+ this.emit("download-progress", progress);
10398
+ }
10143
10399
  }
10144
10400
  fileStream.end();
10145
10401
  await new Promise((resolve, reject) => {
10146
10402
  fileStream.once("finish", resolve);
10147
10403
  fileStream.once("error", reject);
10148
10404
  });
10149
- const hash = await this.computeFileHash(this.downloadPath);
10405
+ const hash = digest.digest("hex");
10150
10406
  if (hash !== expectedHash) {
10151
- import_fs3.unlinkSync(this.downloadPath);
10407
+ import_fs4.unlinkSync(this.downloadPath);
10152
10408
  throw new Error("Download verification failed: hash mismatch");
10153
10409
  }
10154
10410
  if (deltaUpdate) {
10155
- if (!import_fs3.statSync(this.config.appPath).isFile()) {
10411
+ if (!import_fs4.statSync(this.config.appPath).isFile()) {
10156
10412
  throw new Error("Delta updates require appPath to point to the installed bundle file");
10157
10413
  }
10158
10414
  const patchPath = this.downloadPath;
10159
- const reconstructedPath = import_path2.join(downloadDir, `reconstructed-${import_path2.basename(platformUpdate.url)}`);
10415
+ const reconstructedPath = import_path3.join(downloadDir, `reconstructed-${import_path3.basename(platformUpdate.url)}`);
10160
10416
  await DeltaGenerator.apply(this.config.appPath, patchPath, reconstructedPath);
10161
10417
  const reconstructedHash = await this.computeFileHash(reconstructedPath);
10162
10418
  if (reconstructedHash !== platformUpdate.sha256) {
10163
10419
  try {
10164
- import_fs3.unlinkSync(reconstructedPath);
10420
+ import_fs4.unlinkSync(reconstructedPath);
10165
10421
  } catch {}
10166
10422
  throw new Error("Delta reconstruction failed: full bundle hash mismatch");
10167
10423
  }
10168
- import_fs3.unlinkSync(patchPath);
10424
+ import_fs4.unlinkSync(patchPath);
10169
10425
  this.downloadPath = reconstructedPath;
10170
10426
  }
10171
10427
  if (this.config.publicKeyPem && !platformUpdate.signature) {
10172
- import_fs3.unlinkSync(this.downloadPath);
10428
+ import_fs4.unlinkSync(this.downloadPath);
10173
10429
  throw new Error("Updater: publicKeyPem is configured but the manifest entry has no signature. " + "Refusing to install unsigned update.");
10174
10430
  }
10175
10431
  if (platformUpdate.signature) {
10176
10432
  const valid = await this.verifySignature(this.downloadPath, platformUpdate.signature);
10177
10433
  if (!valid) {
10178
- import_fs3.unlinkSync(this.downloadPath);
10434
+ import_fs4.unlinkSync(this.downloadPath);
10179
10435
  throw new Error("Download verification failed: invalid signature");
10180
10436
  }
10181
10437
  }
@@ -10194,7 +10450,7 @@ class AutoUpdater extends import_events3.EventEmitter {
10194
10450
  }
10195
10451
  }
10196
10452
  async installUpdate(restartAfter = true) {
10197
- if (!this.downloadPath || !import_fs3.existsSync(this.downloadPath)) {
10453
+ if (!this.downloadPath || !import_fs4.existsSync(this.downloadPath)) {
10198
10454
  throw new Error("No update downloaded");
10199
10455
  }
10200
10456
  if (this.updateInfo) {
@@ -10204,7 +10460,7 @@ class AutoUpdater extends import_events3.EventEmitter {
10204
10460
  const hash = await this.computeFileHash(this.downloadPath);
10205
10461
  if (hash !== platformUpdate.sha256) {
10206
10462
  try {
10207
- import_fs3.unlinkSync(this.downloadPath);
10463
+ import_fs4.unlinkSync(this.downloadPath);
10208
10464
  } catch {}
10209
10465
  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
10466
  }
@@ -10215,7 +10471,7 @@ class AutoUpdater extends import_events3.EventEmitter {
10215
10471
  const valid = await this.verifySignature(this.downloadPath, platformUpdate.signature);
10216
10472
  if (!valid) {
10217
10473
  try {
10218
- import_fs3.unlinkSync(this.downloadPath);
10474
+ import_fs4.unlinkSync(this.downloadPath);
10219
10475
  } catch {}
10220
10476
  throw new Error("Updater.installUpdate: invalid signature on downloaded bundle");
10221
10477
  }
@@ -10238,14 +10494,17 @@ class AutoUpdater extends import_events3.EventEmitter {
10238
10494
  await this.installLinuxUpdate();
10239
10495
  break;
10240
10496
  }
10241
- if (this.downloadPath && import_fs3.existsSync(this.downloadPath)) {
10242
- import_fs3.unlinkSync(this.downloadPath);
10497
+ if (this.downloadPath && import_fs4.existsSync(this.downloadPath)) {
10498
+ import_fs4.unlinkSync(this.downloadPath);
10243
10499
  }
10500
+ try {
10501
+ import_fs4.rmdirSync(this.downloadDir());
10502
+ } catch {}
10244
10503
  progress.phase = "done";
10245
10504
  progress.percent = 100;
10246
10505
  this.emit("download-progress", progress);
10247
10506
  if (restartAfter) {
10248
- this.restartApp();
10507
+ await this.restartApp();
10249
10508
  }
10250
10509
  } catch (error) {
10251
10510
  progress.phase = "error";
@@ -10256,54 +10515,61 @@ class AutoUpdater extends import_events3.EventEmitter {
10256
10515
  async installMacOSUpdate() {
10257
10516
  const downloadPath = this.downloadPath;
10258
10517
  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")) {
10518
+ if (downloadPath.endsWith(".pkg")) {
10519
+ await this.installMacOSPackage(downloadPath);
10520
+ return;
10521
+ }
10522
+ if (!canReplaceBundle(appPath)) {
10523
+ 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.");
10524
+ }
10525
+ const staged = await extractBundle(downloadPath);
10526
+ try {
10527
+ const trust = await this.assertBundleTrusted(staged.appPath);
10528
+ await clearQuarantine(staged.appPath);
10529
+ const { previousPath } = await swapBundle(staged.appPath, appPath);
10530
+ this.emit("update-installed", {
10531
+ path: appPath,
10532
+ version: this.updateInfo?.version,
10533
+ identity: trust.identity,
10534
+ leftoverPath: previousPath
10535
+ });
10536
+ } finally {
10284
10537
  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", "/"]);
10538
+ import_fs4.rmSync(staged.stagingDir, { recursive: true, force: true });
10539
+ } catch {}
10540
+ }
10541
+ }
10542
+ async assertBundleTrusted(bundlePath) {
10543
+ const policy = this.config.macos ?? {};
10544
+ const trust = await verifyBundleTrust(bundlePath, policy);
10545
+ if (!trust.ok) {
10546
+ 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.");
10547
+ }
10548
+ return trust;
10549
+ }
10550
+ async installMacOSPackage(downloadPath) {
10551
+ try {
10552
+ import_child_process3.execFileSync("pkgutil", ["--check-signature", downloadPath], { stdio: "pipe" });
10553
+ } catch (e) {
10554
+ 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
10555
  }
10556
+ import_child_process3.execFileSync("sudo", ["installer", "-pkg", downloadPath, "-target", "/"]);
10291
10557
  }
10292
10558
  async installWindowsUpdate() {
10293
10559
  const downloadPath = this.downloadPath;
10294
10560
  if (downloadPath.endsWith(".exe")) {
10295
- import_child_process2.spawn(downloadPath, ["/S", "/SILENT", "/VERYSILENT"], {
10561
+ import_child_process3.spawn(downloadPath, ["/S", "/SILENT", "/VERYSILENT"], {
10296
10562
  detached: true,
10297
10563
  stdio: "ignore"
10298
10564
  });
10299
10565
  } else if (downloadPath.endsWith(".msi")) {
10300
- import_child_process2.spawn("msiexec", ["/i", downloadPath, "/quiet", "/norestart"], {
10566
+ import_child_process3.spawn("msiexec", ["/i", downloadPath, "/quiet", "/norestart"], {
10301
10567
  detached: true,
10302
10568
  stdio: "ignore"
10303
10569
  });
10304
10570
  } else if (downloadPath.endsWith(".zip")) {
10305
- const appDir = import_path2.dirname(this.config.appPath);
10306
- import_child_process2.execFileSync("powershell", [
10571
+ const appDir = import_path3.dirname(this.config.appPath);
10572
+ import_child_process3.execFileSync("powershell", [
10307
10573
  "-NoProfile",
10308
10574
  "-NonInteractive",
10309
10575
  "-Command",
@@ -10316,30 +10582,31 @@ class AutoUpdater extends import_events3.EventEmitter {
10316
10582
  async installLinuxUpdate() {
10317
10583
  const downloadPath = this.downloadPath;
10318
10584
  if (downloadPath.endsWith(".AppImage")) {
10319
- import_child_process2.execFileSync("chmod", ["+x", downloadPath]);
10320
- const { renameSync } = await import("fs");
10321
- renameSync(downloadPath, this.config.appPath);
10585
+ import_child_process3.execFileSync("chmod", ["+x", downloadPath]);
10586
+ const { renameSync: renameSync2 } = await import("fs");
10587
+ renameSync2(downloadPath, this.config.appPath);
10322
10588
  } else if (downloadPath.endsWith(".deb")) {
10323
- import_child_process2.execFileSync("sudo", ["dpkg", "-i", downloadPath]);
10589
+ import_child_process3.execFileSync("sudo", ["dpkg", "-i", downloadPath]);
10324
10590
  } else if (downloadPath.endsWith(".rpm")) {
10325
- import_child_process2.execFileSync("sudo", ["rpm", "-U", downloadPath]);
10591
+ import_child_process3.execFileSync("sudo", ["rpm", "-U", downloadPath]);
10326
10592
  } 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]);
10593
+ const appDir = import_path3.dirname(this.config.appPath);
10594
+ import_child_process3.execFileSync("tar", ["-xzf", downloadPath, "-C", appDir]);
10329
10595
  }
10330
10596
  }
10331
- restartApp() {
10332
- const platform = this.getPlatform();
10597
+ async restartApp() {
10333
10598
  const appPath = this.config.appPath;
10334
- switch (platform) {
10599
+ if (this.config.relaunch) {
10600
+ await this.config.relaunch(appPath);
10601
+ return;
10602
+ }
10603
+ switch (this.getPlatform()) {
10335
10604
  case "darwin":
10336
- import_child_process2.spawn("open", ["-n", appPath], { detached: true, stdio: "ignore" });
10605
+ import_child_process3.spawn("open", ["-n", appPath], { detached: true, stdio: "ignore" });
10337
10606
  break;
10338
10607
  case "win32":
10339
- import_child_process2.spawn(appPath, [], { detached: true, stdio: "ignore" });
10340
- break;
10341
10608
  case "linux":
10342
- import_child_process2.spawn(appPath, [], { detached: true, stdio: "ignore" });
10609
+ import_child_process3.spawn(appPath, [], { detached: true, stdio: "ignore" });
10343
10610
  break;
10344
10611
  }
10345
10612
  process.exit(0);
@@ -10370,7 +10637,7 @@ class AutoUpdater extends import_events3.EventEmitter {
10370
10637
  async computeFileHash(filePath) {
10371
10638
  return new Promise((resolve, reject) => {
10372
10639
  const hash = import_crypto3.createHash("sha256");
10373
- const stream = import_fs3.createReadStream(filePath);
10640
+ const stream = import_fs4.createReadStream(filePath);
10374
10641
  stream.on("data", (chunk) => hash.update(chunk));
10375
10642
  stream.on("end", () => resolve(hash.digest("hex")));
10376
10643
  stream.on("error", reject);
@@ -10395,7 +10662,7 @@ class AutoUpdater extends import_events3.EventEmitter {
10395
10662
  } catch {
10396
10663
  return false;
10397
10664
  }
10398
- const data = import_fs3.readFileSync(filePath);
10665
+ const data = import_fs4.readFileSync(filePath);
10399
10666
  if (algo === "ed25519") {
10400
10667
  return verify(null, data, publicKey, sigBytes);
10401
10668
  }
@@ -10413,18 +10680,32 @@ class AutoUpdater extends import_events3.EventEmitter {
10413
10680
  }
10414
10681
 
10415
10682
  class DeltaGenerator {
10683
+ static supported = null;
10684
+ static isSupported() {
10685
+ if (DeltaGenerator.supported !== null)
10686
+ return DeltaGenerator.supported;
10687
+ DeltaGenerator.supported = ["bspatch", "xdelta3"].some((tool) => {
10688
+ try {
10689
+ import_child_process3.execFileSync("command", ["-v", tool], { stdio: "ignore", shell: true });
10690
+ return true;
10691
+ } catch {
10692
+ return false;
10693
+ }
10694
+ });
10695
+ return DeltaGenerator.supported;
10696
+ }
10416
10697
  static async generate(oldPath, newPath, outputPath) {
10417
10698
  try {
10418
- import_child_process2.execFileSync("bsdiff", [oldPath, newPath, outputPath]);
10419
- const stats = import_fs3.statSync(outputPath);
10699
+ import_child_process3.execFileSync("bsdiff", [oldPath, newPath, outputPath]);
10700
+ const stats = import_fs4.statSync(outputPath);
10420
10701
  const hash = await DeltaGenerator.computeHash(outputPath);
10421
10702
  return {
10422
10703
  size: stats.size,
10423
10704
  sha256: hash
10424
10705
  };
10425
10706
  } catch {
10426
- import_child_process2.execFileSync("xdelta3", ["-e", "-s", oldPath, newPath, outputPath]);
10427
- const stats = import_fs3.statSync(outputPath);
10707
+ import_child_process3.execFileSync("xdelta3", ["-e", "-s", oldPath, newPath, outputPath]);
10708
+ const stats = import_fs4.statSync(outputPath);
10428
10709
  const hash = await DeltaGenerator.computeHash(outputPath);
10429
10710
  return {
10430
10711
  size: stats.size,
@@ -10434,15 +10715,15 @@ class DeltaGenerator {
10434
10715
  }
10435
10716
  static async apply(basePath, deltaPath, outputPath) {
10436
10717
  try {
10437
- import_child_process2.execFileSync("bspatch", [basePath, outputPath, deltaPath]);
10718
+ import_child_process3.execFileSync("bspatch", [basePath, outputPath, deltaPath]);
10438
10719
  } catch {
10439
- import_child_process2.execFileSync("xdelta3", ["-d", "-s", basePath, deltaPath, outputPath]);
10720
+ import_child_process3.execFileSync("xdelta3", ["-d", "-s", basePath, deltaPath, outputPath]);
10440
10721
  }
10441
10722
  }
10442
10723
  static async computeHash(filePath) {
10443
10724
  return new Promise((resolve, reject) => {
10444
10725
  const hash = import_crypto3.createHash("sha256");
10445
- const stream = import_fs3.createReadStream(filePath);
10726
+ const stream = import_fs4.createReadStream(filePath);
10446
10727
  stream.on("data", (chunk) => hash.update(chunk));
10447
10728
  stream.on("end", () => resolve(hash.digest("hex")));
10448
10729
  stream.on("error", reject);
@@ -10459,7 +10740,7 @@ function generateUpdateManifest(options) {
10459
10740
  for (const [platform, info] of Object.entries(options.platforms)) {
10460
10741
  if (!info)
10461
10742
  continue;
10462
- const bytes = import_fs3.readFileSync(info.path);
10743
+ const bytes = import_fs4.readFileSync(info.path);
10463
10744
  const hash = import_crypto3.createHash("sha256").update(bytes).digest("hex");
10464
10745
  manifest.platforms[platform] = {
10465
10746
  url: info.url,
@@ -10469,7 +10750,7 @@ function generateUpdateManifest(options) {
10469
10750
  };
10470
10751
  const platformDeltas = options.deltas?.filter((d) => d.platform === platform) || [];
10471
10752
  for (const delta of platformDeltas) {
10472
- const deltaBytes = import_fs3.readFileSync(delta.path);
10753
+ const deltaBytes = import_fs4.readFileSync(delta.path);
10473
10754
  const deltaHash = import_crypto3.createHash("sha256").update(deltaBytes).digest("hex");
10474
10755
  manifest.platforms[platform].delta.push({
10475
10756
  fromVersion: delta.fromVersion,
@@ -10538,7 +10819,7 @@ ${update.releaseNotes}`);
10538
10819
  releaseDate: new Date().toISOString(),
10539
10820
  platforms: {}
10540
10821
  };
10541
- import_fs3.writeFileSync(outputPath, JSON.stringify(manifest, null, 2));
10822
+ import_fs4.writeFileSync(outputPath, JSON.stringify(manifest, null, 2));
10542
10823
  console.log(`Manifest generated: ${outputPath}`);
10543
10824
  console.log("Edit the file to add platform-specific update URLs");
10544
10825
  break;
@@ -10564,7 +10845,7 @@ Examples:
10564
10845
  var updater_default = AutoUpdater;
10565
10846
 
10566
10847
  // src/index.ts
10567
- var execFileAsync = import_node_util.promisify(import_node_child_process.execFile);
10848
+ var execFileAsync2 = import_node_util.promisify(import_node_child_process.execFile);
10568
10849
  var versionMismatchWarned = false;
10569
10850
  function getSdkVersion() {
10570
10851
  try {
@@ -10578,7 +10859,7 @@ async function probeBinaryVersion(craftPath) {
10578
10859
  if (versionMismatchWarned)
10579
10860
  return;
10580
10861
  try {
10581
- const { stdout } = await execFileAsync(craftPath, ["--version"], { timeout: 2000 });
10862
+ const { stdout } = await execFileAsync2(craftPath, ["--version"], { timeout: 2000 });
10582
10863
  const nativeVersion = parseCraftVersionOutput(stdout);
10583
10864
  const sdkVersion = getSdkVersion();
10584
10865
  if (sdkVersion !== "unknown" && nativeVersion && nativeVersion !== sdkVersion) {