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/README.md +67 -31
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/extension-template/manifest.json +1 -1
- package/macos-app/Info.plist +3 -3
- package/macos-app/Resources/BrowserIcons/browserless.svg +6 -0
- package/macos-app/Sources/BraveCodexSyncApp/BraveCodexSyncApp.swift +222 -43
- package/macos-app/Sources/BraveCodexSyncApp/SyncModel.swift +247 -27
- package/package.json +23 -7
- package/src/browserless-runner.js +13 -0
- package/src/browserless.js +108 -0
- package/src/cli.js +109 -4
- package/src/config.js +49 -14
- package/src/paths.js +1 -1
- package/src/updater.js +89 -27
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
|
|
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:
|
|
38
|
+
version: 2,
|
|
39
39
|
token: existing.token || crypto.randomBytes(32).toString("base64url"),
|
|
40
40
|
port: existing.port || DEFAULT_PORT,
|
|
41
|
-
nodePath
|
|
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({
|
|
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.
|
|
104
|
-
|
|
105
|
-
|
|
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 ["
|
|
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
|
|
41
|
-
const
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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) {
|