browser-cookie-bridge 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  isCodexRunning,
10
10
  } from "./codex-direct-import.js";
11
11
  import { readChromiumProfile } from "./chromium-reader.js";
12
+ import { uploadBrowserlessProfile } from "./browserless.js";
12
13
  import { installConfig, installRuntime, readConfig, updatePreferences } from "./config.js";
13
14
  import {
14
15
  braveCookiePaths,
@@ -19,6 +20,9 @@ import {
19
20
  installedExtensionDir,
20
21
  launchAgentPath,
21
22
  loginSyncLaunchAgentPath,
23
+ systemInstalledAppPath,
24
+ userInstalledAppPath,
25
+ APP_ID,
22
26
  SOURCE_BROWSERS,
23
27
  TARGET_BROWSERS,
24
28
  } from "./paths.js";
@@ -39,8 +43,9 @@ Local cookie and session transfer between Chromium browsers and into ChatGPT Cod
39
43
  Commands:
40
44
  setup [--hour 9] [--minute 0] [--no-schedule]
41
45
  install-app [--no-open]
46
+ bootstrap-bundled --app-path /Applications/Browser Cookie Bridge.app
42
47
  preferences --source brave --target codex --cookies on --history off --menu-bar on --auto-check-updates on
43
- sync [--timeout 300]
48
+ sync [--timeout 300] [--allow-cloud-upload]
44
49
  doctor
45
50
  enable-login-sync
46
51
  disable-login-sync
@@ -59,6 +64,8 @@ export async function main(argv) {
59
64
  return sync(args);
60
65
  case "install-app":
61
66
  return installDesktopApp(args);
67
+ case "bootstrap-bundled":
68
+ return bootstrapBundledApp(args);
62
69
  case "preferences":
63
70
  return preferences(args);
64
71
  case "install-update":
@@ -97,9 +104,9 @@ function installUpdate(args) {
97
104
  console.log(`Update worker started: ${result.workerPID}`);
98
105
  }
99
106
 
100
- function performUpdateWorker(args) {
107
+ async function performUpdateWorker(args) {
101
108
  assertMacOS();
102
- const result = performUpdate({
109
+ const result = await performUpdate({
103
110
  version: stringFlag(args, "--version", ""),
104
111
  appPath: stringFlag(args, "--app-path", ""),
105
112
  appPID: integerFlag(args, "--app-pid", 0, 2, Number.MAX_SAFE_INTEGER),
@@ -117,6 +124,9 @@ function preferences(args) {
117
124
  menuBar: booleanFlag(args, "--menu-bar", existing.ui?.menuBar === true),
118
125
  openAtLogin: booleanFlag(args, "--open-at-login", existing.ui?.openAtLogin !== false),
119
126
  autoCheckUpdates: booleanFlag(args, "--auto-check-updates", existing.ui?.autoCheckUpdates !== false),
127
+ browserlessProfileName: stringFlag(args, "--browserless-profile", existing.browserless?.profileName || "browser-cookie-bridge"),
128
+ browserlessRegion: stringFlag(args, "--browserless-region", existing.browserless?.region || "sfo"),
129
+ browserlessOnlyDomains: optionalStringFlag(args, "--browserless-domains", (existing.browserless?.onlyDomains || []).join(",")),
120
130
  });
121
131
  console.log(
122
132
  `Saved: source=${config.sourceBrowser}, target=${config.targetBrowser}, cookies=${config.imports.cookies ? "on" : "off"}, history=${config.imports.history ? "on" : "off"}, menu-bar=${config.ui.menuBar ? "on" : "off"}, open-at-login=${config.ui.openAtLogin ? "on" : "off"}, auto-check-updates=${config.ui.autoCheckUpdates ? "on" : "off"}`,
@@ -150,6 +160,67 @@ function installDesktopApp(args) {
150
160
  : "The app is available in your user Applications folder and Spotlight.");
151
161
  }
152
162
 
163
+ function bootstrapBundledApp(args) {
164
+ assertMacOS();
165
+ const appPath = path.resolve(stringFlag(args, "--app-path", ""));
166
+ if (!appPath.endsWith(".app") || !fs.existsSync(appPath)) {
167
+ throw new Error("--app-path must point to the running Browser Cookie Bridge app");
168
+ }
169
+
170
+ const firstInstall = !fs.existsSync(configPath());
171
+ const runtime = installRuntime();
172
+ const existing = firstInstall ? null : readConfig();
173
+ const config = installConfig({
174
+ hour: existing?.schedule?.hour ?? 9,
175
+ minute: existing?.schedule?.minute ?? 0,
176
+ nodePath: process.execPath,
177
+ });
178
+
179
+ if (config.ui.openAtLogin) {
180
+ installAppLogin({ appPath, bootstrapNow: false });
181
+ } else {
182
+ removeAppLogin();
183
+ }
184
+
185
+ const bundledCLI = path.join(runtime, "bin", "brave-codex-cookie-sync.js");
186
+ if (firstInstall || fs.existsSync(loginSyncLaunchAgentPath())) {
187
+ installLoginSync({
188
+ cliPath: bundledCLI,
189
+ nodePath: process.execPath,
190
+ });
191
+ }
192
+ if (fs.existsSync(launchAgentPath())) {
193
+ installSchedule({
194
+ hour: config.schedule.hour,
195
+ minute: config.schedule.minute,
196
+ cliPath: bundledCLI,
197
+ nodePath: process.execPath,
198
+ });
199
+ }
200
+
201
+ archiveDuplicateUserApp(appPath);
202
+
203
+ console.log(`Bundled runtime ready: ${runtime}`);
204
+ }
205
+
206
+ function archiveDuplicateUserApp(currentAppPath) {
207
+ if (path.resolve(currentAppPath) !== path.resolve(systemInstalledAppPath())) return;
208
+ const duplicate = userInstalledAppPath();
209
+ if (!fs.existsSync(duplicate)) return;
210
+ const infoPath = path.join(duplicate, "Contents", "Info.plist");
211
+ const info = fs.existsSync(infoPath) ? fs.readFileSync(infoPath, "utf8") : "";
212
+ if (!info.includes(`<string>${APP_ID}</string>`)) return;
213
+
214
+ const trash = path.join(os.homedir(), ".Trash");
215
+ fs.mkdirSync(trash, { recursive: true, mode: 0o700 });
216
+ let archived = path.join(trash, "Browser Cookie Bridge previous installation.app");
217
+ for (let suffix = 2; fs.existsSync(archived); suffix += 1) {
218
+ archived = path.join(trash, `Browser Cookie Bridge previous installation ${suffix}.app`);
219
+ }
220
+ fs.renameSync(duplicate, archived);
221
+ console.log(`Archived duplicate app: ${archived}`);
222
+ }
223
+
153
224
  function setup(args) {
154
225
  assertMacOS();
155
226
  const hour = integerFlag(args, "--hour", 9, 0, 23);
@@ -172,6 +243,10 @@ function setup(args) {
172
243
  if (config.targetBrowser === "codex") {
173
244
  console.log(`Source browser: ${config.sourceBrowser} (read locally; no extension required)`);
174
245
  console.log("Target integration: direct local Codex merge (Codex must be closed)");
246
+ } else if (config.targetBrowser === "browserless") {
247
+ console.log(`Source browser: ${config.sourceBrowser} (captured locally by the Browserless CLI)`);
248
+ console.log(`Cloud destination: Browserless ${config.browserless?.region || "sfo"} / ${config.browserless?.profileName || "browser-cookie-bridge"}`);
249
+ console.log("Browserless uploads are manual-only and require --allow-cloud-upload.");
175
250
  } else {
176
251
  console.log(`Source extension (${config.sourceBrowser}): ${installedExtensionDir(undefined, config.sourceBrowser)}`);
177
252
  console.log(`Target extension (${config.targetBrowser}): ${installedExtensionDir(undefined, config.targetBrowser)}`);
@@ -188,7 +263,25 @@ async function sync(args) {
188
263
  assertMacOS();
189
264
  const seconds = integerFlag(args, "--timeout", 300, 5, 3600);
190
265
  const config = readConfig();
191
- const isCodexTarget = (config.targetBrowser || "codex") === "codex";
266
+ const target = config.targetBrowser || "codex";
267
+ const isCodexTarget = target === "codex";
268
+ if (target === "browserless") {
269
+ if (!args.includes("--allow-cloud-upload")) {
270
+ throw new Error("Browserless cloud uploads are manual-only. Start one from the app or pass --allow-cloud-upload explicitly.");
271
+ }
272
+ const source = config.sourceBrowser || "brave";
273
+ const local = readChromiumProfile({ browser: source, imports: { cookies: false, history: false } });
274
+ console.log(`Preparing ${source} profile ${local.profileName} for an explicit Browserless cloud upload…`);
275
+ const result = await uploadBrowserlessProfile({
276
+ browser: source,
277
+ localProfile: local.profileName,
278
+ profileName: config.browserless?.profileName || "browser-cookie-bridge",
279
+ region: config.browserless?.region || "sfo",
280
+ onlyDomains: config.browserless?.onlyDomains || [],
281
+ });
282
+ console.log(result.summary);
283
+ return result;
284
+ }
192
285
  if (isCodexTarget) {
193
286
  if (isCodexRunning()) throw new Error(CODEX_RUNNING_ERROR);
194
287
  const payload = readChromiumProfile({
@@ -264,6 +357,8 @@ function doctor() {
264
357
  console.log(
265
358
  target === "codex"
266
359
  ? "Target integration: direct local Codex merge (Codex must be closed)"
360
+ : target === "browserless"
361
+ ? `Target integration: optional Browserless cloud upload (${config?.browserless?.region || "sfo"}); manual only`
267
362
  : `Target extension: ${status(installedExtensionDir(home, target))}`,
268
363
  );
269
364
  console.log(`Daily schedule: ${status(launchAgentPath(home))}`);
@@ -288,6 +383,8 @@ function enableLoginSync() {
288
383
  const config = readConfig();
289
384
  console.log(config.targetBrowser === "codex"
290
385
  ? "A sync starts when you sign in and updates Codex only when Codex is closed."
386
+ : config.targetBrowser === "browserless"
387
+ ? "Browserless uploads remain manual-only; login sync will not send data to the cloud."
291
388
  : "A sync starts when you sign in and waits up to five minutes for both browser extensions.");
292
389
  }
293
390
 
@@ -306,6 +403,9 @@ function setAppLogin(enabled) {
306
403
  menuBar: existing.ui?.menuBar === true,
307
404
  openAtLogin: enabled,
308
405
  autoCheckUpdates: existing.ui?.autoCheckUpdates !== false,
406
+ browserlessProfileName: existing.browserless?.profileName,
407
+ browserlessRegion: existing.browserless?.region,
408
+ browserlessOnlyDomains: existing.browserless?.onlyDomains,
309
409
  });
310
410
  if (enabled) {
311
411
  const appPath = installedAppPath();
@@ -355,6 +455,11 @@ function stringFlag(args, name, fallback) {
355
455
  return value;
356
456
  }
357
457
 
458
+ function optionalStringFlag(args, name, fallback) {
459
+ const index = args.indexOf(name);
460
+ return index === -1 ? fallback : (args[index + 1] ?? "");
461
+ }
462
+
358
463
  function existing(paths) {
359
464
  return paths.filter((candidate) => fs.existsSync(candidate));
360
465
  }
package/src/config.js CHANGED
@@ -22,7 +22,7 @@ export function readConfig(home) {
22
22
  return config;
23
23
  }
24
24
 
25
- export function installConfig({ home, hour = 9, minute = 0 }) {
25
+ export function installConfig({ home, hour = 9, minute = 0, nodePath = process.execPath }) {
26
26
  const target = configPath(home);
27
27
  const support = path.dirname(target);
28
28
  fs.mkdirSync(support, { recursive: true, mode: 0o700 });
@@ -35,10 +35,10 @@ export function installConfig({ home, hour = 9, minute = 0 }) {
35
35
  const sourceBrowser = SOURCE_BROWSERS.includes(existing.sourceBrowser) ? existing.sourceBrowser : "brave";
36
36
  const configuredTarget = TARGET_BROWSERS.includes(existing.targetBrowser) ? existing.targetBrowser : "codex";
37
37
  const config = {
38
- version: 1,
38
+ version: 2,
39
39
  token: existing.token || crypto.randomBytes(32).toString("base64url"),
40
40
  port: existing.port || DEFAULT_PORT,
41
- nodePath: process.execPath,
41
+ nodePath,
42
42
  sourceBrowser,
43
43
  targetBrowser: configuredTarget === sourceBrowser ? "codex" : configuredTarget,
44
44
  imports: {
@@ -51,6 +51,11 @@ export function installConfig({ home, hour = 9, minute = 0 }) {
51
51
  openAtLogin: existing.ui?.openAtLogin !== false,
52
52
  autoCheckUpdates: existing.ui?.autoCheckUpdates !== false,
53
53
  },
54
+ browserless: {
55
+ profileName: cleanProfileName(existing.browserless?.profileName) || "browser-cookie-bridge",
56
+ region: ["sfo", "lon", "ams"].includes(existing.browserless?.region) ? existing.browserless.region : "sfo",
57
+ onlyDomains: cleanDomains(existing.browserless?.onlyDomains),
58
+ },
54
59
  schedule: { hour, minute },
55
60
  createdAt: existing.createdAt || new Date().toISOString(),
56
61
  updatedAt: new Date().toISOString(),
@@ -63,7 +68,19 @@ export function installConfig({ home, hour = 9, minute = 0 }) {
63
68
  return config;
64
69
  }
65
70
 
66
- export function updatePreferences({ home, cookies, history, sourceBrowser, targetBrowser, menuBar, openAtLogin, autoCheckUpdates }) {
71
+ export function updatePreferences({
72
+ home,
73
+ cookies,
74
+ history,
75
+ sourceBrowser,
76
+ targetBrowser,
77
+ menuBar,
78
+ openAtLogin,
79
+ autoCheckUpdates,
80
+ browserlessProfileName,
81
+ browserlessRegion,
82
+ browserlessOnlyDomains,
83
+ }) {
67
84
  const config = readConfig(home);
68
85
  if (!SOURCE_BROWSERS.includes(sourceBrowser)) {
69
86
  throw new Error(`Unsupported source browser: ${sourceBrowser}`);
@@ -82,38 +99,56 @@ export function updatePreferences({ home, cookies, history, sourceBrowser, targe
82
99
  openAtLogin: Boolean(openAtLogin),
83
100
  autoCheckUpdates: Boolean(autoCheckUpdates),
84
101
  };
102
+ const region = browserlessRegion ?? config.browserless?.region ?? "sfo";
103
+ if (!["sfo", "lon", "ams"].includes(region)) throw new Error("Browserless region must be sfo, lon, or ams");
104
+ config.browserless = {
105
+ profileName: cleanProfileName(browserlessProfileName ?? config.browserless?.profileName) || "browser-cookie-bridge",
106
+ region,
107
+ onlyDomains: cleanDomains(browserlessOnlyDomains ?? config.browserless?.onlyDomains),
108
+ };
85
109
  config.updatedAt = new Date().toISOString();
86
110
  writePrivateJson(configPath(home), config);
87
111
  return config;
88
112
  }
89
113
 
90
- export function installRuntime(home) {
91
- const source = projectRoot();
114
+ export function installRuntime(home, source = projectRoot()) {
92
115
  const target = path.join(path.dirname(configPath(home)), "runtime");
93
116
  if (path.resolve(source) === path.resolve(target)) return target;
94
117
 
95
118
  fs.mkdirSync(target, { recursive: true, mode: 0o700 });
96
- for (const name of ["bin", "src", "extension-template"]) {
119
+ for (const name of ["bin", "src", "extension-template", "node_modules"]) {
97
120
  fs.rmSync(path.join(target, name), { recursive: true, force: true });
98
121
  fs.cpSync(path.join(source, name), path.join(target, name), { recursive: true, force: true });
99
122
  }
123
+ fs.rmSync(path.join(target, "node_modules", ".bin"), { recursive: true, force: true });
100
124
  const appSource = path.join(source, "macos-app");
101
125
  const appTarget = path.join(target, "macos-app");
102
126
  fs.rmSync(appTarget, { recursive: true, force: true });
103
- fs.mkdirSync(appTarget, { recursive: true, mode: 0o700 });
104
- for (const name of ["Sources", "Resources"]) {
105
- fs.cpSync(path.join(appSource, name), path.join(appTarget, name), { recursive: true, force: true });
127
+ if (fs.existsSync(appSource)) {
128
+ fs.mkdirSync(appTarget, { recursive: true, mode: 0o700 });
129
+ for (const name of ["Sources", "Resources"]) {
130
+ fs.cpSync(path.join(appSource, name), path.join(appTarget, name), { recursive: true, force: true });
131
+ }
132
+ for (const name of ["Package.swift", "Info.plist"]) {
133
+ fs.copyFileSync(path.join(appSource, name), path.join(appTarget, name));
134
+ }
106
135
  }
107
- for (const name of ["Package.swift", "Info.plist"]) {
108
- fs.copyFileSync(path.join(appSource, name), path.join(appTarget, name));
109
- }
110
- for (const name of ["package.json", "README.md", "LICENSE"]) {
136
+ for (const name of ["package.json", "README.md", "LICENSE", "THIRD_PARTY_NOTICES.md"]) {
111
137
  fs.copyFileSync(path.join(source, name), path.join(target, name));
112
138
  }
113
139
  fs.chmodSync(path.join(target, "bin", "brave-codex-cookie-sync.js"), 0o700);
114
140
  return target;
115
141
  }
116
142
 
143
+ function cleanProfileName(value) {
144
+ return typeof value === "string" ? value.trim().slice(0, 100) : "";
145
+ }
146
+
147
+ function cleanDomains(value) {
148
+ const items = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [];
149
+ return [...new Set(items.map((item) => String(item).trim().toLowerCase()).filter(Boolean))].slice(0, 50);
150
+ }
151
+
117
152
  function installExtension(config, home, browser, role) {
118
153
  const source = path.join(projectRoot(), "extension-template");
119
154
  const target = installedExtensionDir(home, browser);
package/src/paths.js CHANGED
@@ -10,7 +10,7 @@ export const DEFAULT_PORT = 43128;
10
10
  export const EXTENSION_ID = "ihanfnkcipmlhmokbcinlkdfcfheofjb";
11
11
  export const EXTENSION_ORIGIN = `chrome-extension://${EXTENSION_ID}`;
12
12
  export const SOURCE_BROWSERS = ["brave", "chrome", "edge", "arc", "vivaldi", "opera", "comet"];
13
- export const TARGET_BROWSERS = [...SOURCE_BROWSERS, "codex"];
13
+ export const TARGET_BROWSERS = [...SOURCE_BROWSERS, "codex", "browserless"];
14
14
 
15
15
  export function projectRoot() {
16
16
  return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
package/src/updater.js CHANGED
@@ -2,10 +2,12 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { spawn, spawnSync } from "node:child_process";
5
+ import crypto from "node:crypto";
6
+ import { Readable } from "node:stream";
7
+ import { finished } from "node:stream/promises";
5
8
  import { fileURLToPath } from "node:url";
6
9
  import {
7
10
  appSupportDir,
8
- installedAppPath,
9
11
  systemInstalledAppPath,
10
12
  userInstalledAppPath,
11
13
  } from "./paths.js";
@@ -35,33 +37,28 @@ export function startDetachedUpdate({ version, appPath, appPID, home = os.homedi
35
37
  return { workerPID: child.pid, logPath };
36
38
  }
37
39
 
38
- export function performUpdate({ version, appPath, appPID, home = os.homedir() }) {
40
+ export async function performUpdate({ version, appPath, appPID, home = os.homedir() }) {
39
41
  const destination = validateUpdateRequest({ version, appPath, appPID, home });
40
- const resultPath = path.join(appSupportDir(home), "update-result.json");
41
- const currentUserApp = installedAppPath(home);
42
+ const support = appSupportDir(home);
43
+ const resultPath = path.join(support, "update-result.json");
44
+ let mounted = null;
42
45
  try {
43
46
  waitForProcessToExit(appPID, 30_000);
44
- const npxPath = findNpx();
45
- const result = spawnSync(npxPath, [
46
- "--yes",
47
- `browser-cookie-bridge@${version}`,
48
- "install-app",
49
- "--no-open",
50
- ], {
51
- encoding: "utf8",
52
- maxBuffer: 32 * 1024 * 1024,
53
- env: { ...process.env, npm_config_yes: "true" },
54
- });
55
- if (result.status !== 0) {
56
- throw new Error(result.stderr.trim() || result.stdout.trim() || `npx exited with status ${result.status}`);
57
- }
58
- if (!fs.existsSync(currentUserApp)) throw new Error("The downloaded app was not installed");
59
- if (destination !== currentUserApp) replaceAppContents(currentUserApp, destination);
47
+ const release = await downloadReleaseDMG({ version, architecture: process.arch, support });
48
+ mounted = mountReleaseDMG(release.dmgPath, support);
49
+ const sourceApp = path.join(mounted.mountPoint, "Browser Cookie Bridge.app");
50
+ if (!fs.existsSync(sourceApp)) throw new Error("The downloaded DMG does not contain Browser Cookie Bridge.app");
51
+ replaceAppContents(sourceApp, destination);
60
52
  installAppLogin({ appPath: destination, bootstrapNow: false });
61
53
  writeUpdateResult(resultPath, { status: "success", version });
54
+ unmountReleaseDMG(mounted);
55
+ mounted = null;
56
+ fs.rmSync(release.dmgPath, { force: true });
57
+ fs.rmSync(release.checksumPath, { force: true });
62
58
  relaunch(destination);
63
59
  return { destination, version };
64
60
  } catch (error) {
61
+ if (mounted) unmountReleaseDMG(mounted);
65
62
  writeUpdateResult(resultPath, { status: "failed", version, message: error.message });
66
63
  if (fs.existsSync(destination)) relaunch(destination);
67
64
  throw error;
@@ -104,13 +101,78 @@ export function validateUpdateRequest({ version, appPath, appPID, home = os.home
104
101
  return resolved;
105
102
  }
106
103
 
107
- function findNpx() {
108
- const sibling = path.join(path.dirname(process.execPath), "npx");
109
- if (fs.existsSync(sibling)) return sibling;
110
- const result = spawnSync("/usr/bin/which", ["npx"], { encoding: "utf8" });
111
- const discovered = result.status === 0 ? result.stdout.trim() : "";
112
- if (!discovered || !fs.existsSync(discovered)) throw new Error("npx was not found beside the configured Node.js runtime");
113
- return discovered;
104
+ export function releaseAssetName(version, architecture = process.arch) {
105
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version ?? "")) throw new Error("Invalid update version");
106
+ if (!["arm64", "x64"].includes(architecture)) throw new Error(`Unsupported macOS architecture: ${architecture}`);
107
+ return `Browser-Cookie-Bridge-${architecture}.dmg`;
108
+ }
109
+
110
+ export function parseChecksum(text, expectedFilename) {
111
+ const match = text.trim().match(/^([a-f0-9]{64})\s+\*?(.+)$/i);
112
+ if (!match || match[2] !== expectedFilename) throw new Error("Invalid release checksum file");
113
+ return match[1].toLowerCase();
114
+ }
115
+
116
+ async function downloadReleaseDMG({ version, architecture, support }) {
117
+ const updates = path.join(support, "updates");
118
+ fs.mkdirSync(updates, { recursive: true, mode: 0o700 });
119
+ const filename = releaseAssetName(version, architecture);
120
+ const base = `https://github.com/apoorvdarshan/browser-cookie-bridge/releases/download/v${version}`;
121
+ const dmgPath = path.join(updates, filename);
122
+ const checksumPath = `${dmgPath}.sha256`;
123
+ await downloadFile(`${base}/${filename}`, dmgPath);
124
+ await downloadFile(`${base}/${filename}.sha256`, checksumPath);
125
+ const expected = parseChecksum(fs.readFileSync(checksumPath, "utf8"), filename);
126
+ const actual = await fileSHA256(dmgPath);
127
+ if (actual !== expected) throw new Error("The downloaded update failed its SHA-256 verification");
128
+ return { dmgPath, checksumPath };
129
+ }
130
+
131
+ async function downloadFile(url, target) {
132
+ const response = await fetch(url, {
133
+ headers: { "User-Agent": "Browser-Cookie-Bridge-Updater" },
134
+ redirect: "follow",
135
+ });
136
+ if (!response.ok || !response.body) throw new Error(`Could not download update (${response.status})`);
137
+ const temporary = `${target}.${process.pid}.tmp`;
138
+ const output = fs.createWriteStream(temporary, { mode: 0o600 });
139
+ try {
140
+ await finished(Readable.fromWeb(response.body).pipe(output));
141
+ fs.renameSync(temporary, target);
142
+ } catch (error) {
143
+ fs.rmSync(temporary, { force: true });
144
+ throw error;
145
+ }
146
+ }
147
+
148
+ async function fileSHA256(target) {
149
+ const hash = crypto.createHash("sha256");
150
+ const input = fs.createReadStream(target);
151
+ input.on("data", (chunk) => hash.update(chunk));
152
+ await finished(input);
153
+ return hash.digest("hex");
154
+ }
155
+
156
+ function mountReleaseDMG(dmgPath, support) {
157
+ const mountPoint = path.join(support, "updates", `mounted-${process.pid}`);
158
+ fs.rmSync(mountPoint, { recursive: true, force: true });
159
+ fs.mkdirSync(mountPoint, { recursive: true, mode: 0o700 });
160
+ const result = spawnSync("/usr/bin/hdiutil", [
161
+ "attach", dmgPath,
162
+ "-mountpoint", mountPoint,
163
+ "-nobrowse",
164
+ "-readonly",
165
+ ], { encoding: "utf8" });
166
+ if (result.status !== 0) {
167
+ fs.rmSync(mountPoint, { recursive: true, force: true });
168
+ throw new Error(result.stderr.trim() || "The update DMG could not be mounted");
169
+ }
170
+ return { mountPoint };
171
+ }
172
+
173
+ function unmountReleaseDMG({ mountPoint }) {
174
+ spawnSync("/usr/bin/hdiutil", ["detach", mountPoint, "-force"], { stdio: "ignore" });
175
+ fs.rmSync(mountPoint, { recursive: true, force: true });
114
176
  }
115
177
 
116
178
  function waitForProcessToExit(pid, timeoutMilliseconds) {