browser-cookie-bridge 1.0.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/LICENSE +21 -0
- package/README.md +221 -0
- package/bin/brave-codex-cookie-sync.js +8 -0
- package/extension-template/background.js +196 -0
- package/extension-template/manifest.json +12 -0
- package/macos-app/Info.plist +18 -0
- package/macos-app/Package.swift +17 -0
- package/macos-app/Resources/AppIcon.icns +0 -0
- package/macos-app/Resources/AppIcon.png +0 -0
- package/macos-app/Resources/BrowserIcons/arc.svg +1 -0
- package/macos-app/Resources/BrowserIcons/brave.svg +17 -0
- package/macos-app/Resources/BrowserIcons/chatgpt-codex.svg +4 -0
- package/macos-app/Resources/BrowserIcons/chrome.svg +1 -0
- package/macos-app/Resources/BrowserIcons/comet.svg +4 -0
- package/macos-app/Resources/BrowserIcons/edge.svg +1 -0
- package/macos-app/Resources/BrowserIcons/opera.svg +1 -0
- package/macos-app/Resources/BrowserIcons/vivaldi.svg +1 -0
- package/macos-app/Resources/MenuBarCookie.svg +15 -0
- package/macos-app/Resources/MenuBarCookieTemplate.png +0 -0
- package/macos-app/Sources/BraveCodexSyncApp/BraveCodexSyncApp.swift +771 -0
- package/macos-app/Sources/BraveCodexSyncApp/SyncModel.swift +609 -0
- package/package.json +44 -0
- package/src/app-installer.js +54 -0
- package/src/broker.js +248 -0
- package/src/chromium-reader.js +190 -0
- package/src/cli.js +368 -0
- package/src/codex-direct-import.js +320 -0
- package/src/config.js +141 -0
- package/src/paths.js +80 -0
- package/src/scheduler.js +150 -0
- package/src/updater.js +144 -0
package/src/config.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_PORT,
|
|
6
|
+
configPath,
|
|
7
|
+
installedExtensionDir,
|
|
8
|
+
projectRoot,
|
|
9
|
+
SOURCE_BROWSERS,
|
|
10
|
+
TARGET_BROWSERS,
|
|
11
|
+
} from "./paths.js";
|
|
12
|
+
|
|
13
|
+
export function readConfig(home) {
|
|
14
|
+
const target = configPath(home);
|
|
15
|
+
if (!fs.existsSync(target)) {
|
|
16
|
+
throw new Error("Not configured. Run `browser-cookie-bridge setup` first.");
|
|
17
|
+
}
|
|
18
|
+
const config = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
19
|
+
if (!config.token || !Number.isInteger(config.port)) {
|
|
20
|
+
throw new Error(`Invalid configuration at ${target}`);
|
|
21
|
+
}
|
|
22
|
+
return config;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function installConfig({ home, hour = 9, minute = 0 }) {
|
|
26
|
+
const target = configPath(home);
|
|
27
|
+
const support = path.dirname(target);
|
|
28
|
+
fs.mkdirSync(support, { recursive: true, mode: 0o700 });
|
|
29
|
+
|
|
30
|
+
let existing = {};
|
|
31
|
+
if (fs.existsSync(target)) {
|
|
32
|
+
existing = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const sourceBrowser = SOURCE_BROWSERS.includes(existing.sourceBrowser) ? existing.sourceBrowser : "brave";
|
|
36
|
+
const configuredTarget = TARGET_BROWSERS.includes(existing.targetBrowser) ? existing.targetBrowser : "codex";
|
|
37
|
+
const config = {
|
|
38
|
+
version: 1,
|
|
39
|
+
token: existing.token || crypto.randomBytes(32).toString("base64url"),
|
|
40
|
+
port: existing.port || DEFAULT_PORT,
|
|
41
|
+
nodePath: process.execPath,
|
|
42
|
+
sourceBrowser,
|
|
43
|
+
targetBrowser: configuredTarget === sourceBrowser ? "codex" : configuredTarget,
|
|
44
|
+
imports: {
|
|
45
|
+
cookies: existing.imports?.cookies !== false,
|
|
46
|
+
passwords: false,
|
|
47
|
+
history: existing.imports?.history === true,
|
|
48
|
+
},
|
|
49
|
+
ui: {
|
|
50
|
+
menuBar: existing.ui?.menuBar !== false,
|
|
51
|
+
openAtLogin: existing.ui?.openAtLogin !== false,
|
|
52
|
+
autoCheckUpdates: existing.ui?.autoCheckUpdates !== false,
|
|
53
|
+
},
|
|
54
|
+
schedule: { hour, minute },
|
|
55
|
+
createdAt: existing.createdAt || new Date().toISOString(),
|
|
56
|
+
updatedAt: new Date().toISOString(),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
writePrivateJson(target, config);
|
|
60
|
+
for (const browser of SOURCE_BROWSERS) installExtension(config, home, browser, "browser");
|
|
61
|
+
fs.rmSync(installedExtensionDir(home, "codex"), { recursive: true, force: true });
|
|
62
|
+
fs.rmSync(installedExtensionDir(home, "atlas"), { recursive: true, force: true });
|
|
63
|
+
return config;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function updatePreferences({ home, cookies, history, sourceBrowser, targetBrowser, menuBar, openAtLogin, autoCheckUpdates }) {
|
|
67
|
+
const config = readConfig(home);
|
|
68
|
+
if (!SOURCE_BROWSERS.includes(sourceBrowser)) {
|
|
69
|
+
throw new Error(`Unsupported source browser: ${sourceBrowser}`);
|
|
70
|
+
}
|
|
71
|
+
if (!TARGET_BROWSERS.includes(targetBrowser)) {
|
|
72
|
+
throw new Error(`Unsupported target browser: ${targetBrowser}`);
|
|
73
|
+
}
|
|
74
|
+
if (sourceBrowser === targetBrowser) {
|
|
75
|
+
throw new Error("Source and target browsers must be different");
|
|
76
|
+
}
|
|
77
|
+
config.sourceBrowser = sourceBrowser;
|
|
78
|
+
config.targetBrowser = targetBrowser;
|
|
79
|
+
config.imports = { cookies: Boolean(cookies), passwords: false, history: Boolean(history) };
|
|
80
|
+
config.ui = {
|
|
81
|
+
menuBar: Boolean(menuBar),
|
|
82
|
+
openAtLogin: Boolean(openAtLogin),
|
|
83
|
+
autoCheckUpdates: Boolean(autoCheckUpdates),
|
|
84
|
+
};
|
|
85
|
+
config.updatedAt = new Date().toISOString();
|
|
86
|
+
writePrivateJson(configPath(home), config);
|
|
87
|
+
return config;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function installRuntime(home) {
|
|
91
|
+
const source = projectRoot();
|
|
92
|
+
const target = path.join(path.dirname(configPath(home)), "runtime");
|
|
93
|
+
if (path.resolve(source) === path.resolve(target)) return target;
|
|
94
|
+
|
|
95
|
+
fs.mkdirSync(target, { recursive: true, mode: 0o700 });
|
|
96
|
+
for (const name of ["bin", "src", "extension-template"]) {
|
|
97
|
+
fs.rmSync(path.join(target, name), { recursive: true, force: true });
|
|
98
|
+
fs.cpSync(path.join(source, name), path.join(target, name), { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
const appSource = path.join(source, "macos-app");
|
|
101
|
+
const appTarget = path.join(target, "macos-app");
|
|
102
|
+
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 });
|
|
106
|
+
}
|
|
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"]) {
|
|
111
|
+
fs.copyFileSync(path.join(source, name), path.join(target, name));
|
|
112
|
+
}
|
|
113
|
+
fs.chmodSync(path.join(target, "bin", "brave-codex-cookie-sync.js"), 0o700);
|
|
114
|
+
return target;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function installExtension(config, home, browser, role) {
|
|
118
|
+
const source = path.join(projectRoot(), "extension-template");
|
|
119
|
+
const target = installedExtensionDir(home, browser);
|
|
120
|
+
fs.mkdirSync(target, { recursive: true, mode: 0o700 });
|
|
121
|
+
|
|
122
|
+
for (const name of ["manifest.json", "background.js"]) {
|
|
123
|
+
fs.copyFileSync(path.join(source, name), path.join(target, name));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const generated = [
|
|
127
|
+
"// Generated locally by Browser Cookie Bridge. Do not share this file.",
|
|
128
|
+
`globalThis.SYNC_CONFIG = ${JSON.stringify({ token: config.token, port: config.port })};`,
|
|
129
|
+
`globalThis.SYNC_ROLE = ${JSON.stringify(role)};`,
|
|
130
|
+
`globalThis.SYNC_BROWSER = ${JSON.stringify(browser)};`,
|
|
131
|
+
"",
|
|
132
|
+
].join("\n");
|
|
133
|
+
fs.writeFileSync(path.join(target, "config.js"), generated, { mode: 0o600 });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function writePrivateJson(target, value) {
|
|
137
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
138
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
139
|
+
fs.renameSync(temporary, target);
|
|
140
|
+
fs.chmodSync(target, 0o600);
|
|
141
|
+
}
|
package/src/paths.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
export const APP_ID = "com.apoorvdarshan.brave-codex-cookie-sync";
|
|
7
|
+
export const LOGIN_SYNC_APP_ID = `${APP_ID}.login-sync`;
|
|
8
|
+
export const APP_LOGIN_APP_ID = `${APP_ID}.app-login`;
|
|
9
|
+
export const DEFAULT_PORT = 43128;
|
|
10
|
+
export const EXTENSION_ID = "ihanfnkcipmlhmokbcinlkdfcfheofjb";
|
|
11
|
+
export const EXTENSION_ORIGIN = `chrome-extension://${EXTENSION_ID}`;
|
|
12
|
+
export const SOURCE_BROWSERS = ["brave", "chrome", "edge", "arc", "vivaldi", "opera", "comet"];
|
|
13
|
+
export const TARGET_BROWSERS = [...SOURCE_BROWSERS, "codex"];
|
|
14
|
+
|
|
15
|
+
export function projectRoot() {
|
|
16
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function appSupportDir(home = os.homedir()) {
|
|
20
|
+
return path.join(home, "Library", "Application Support", "BraveCodexCookieSync");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function configPath(home = os.homedir()) {
|
|
24
|
+
return path.join(appSupportDir(home), "config.json");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function installedExtensionDir(home = os.homedir(), role = "brave") {
|
|
28
|
+
return path.join(appSupportDir(home), `extension-${role}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function launchAgentPath(home = os.homedir()) {
|
|
32
|
+
return path.join(home, "Library", "LaunchAgents", `${APP_ID}.plist`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function loginSyncLaunchAgentPath(home = os.homedir()) {
|
|
36
|
+
return path.join(home, "Library", "LaunchAgents", `${LOGIN_SYNC_APP_ID}.plist`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function appLoginLaunchAgentPath(home = os.homedir()) {
|
|
40
|
+
return path.join(home, "Library", "LaunchAgents", `${APP_LOGIN_APP_ID}.plist`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function userInstalledAppPath(home = os.homedir()) {
|
|
44
|
+
return path.join(home, "Applications", "Browser Cookie Bridge.app");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function systemInstalledAppPath() {
|
|
48
|
+
return path.join("/Applications", "Browser Cookie Bridge.app");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function installedAppPath(home = os.homedir()) {
|
|
52
|
+
const systemApp = systemInstalledAppPath();
|
|
53
|
+
const isCurrentUser = path.resolve(home) === path.resolve(os.homedir());
|
|
54
|
+
return isCurrentUser && fs.existsSync(systemApp) ? systemApp : userInstalledAppPath(home);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function braveCookiePaths(home = os.homedir()) {
|
|
58
|
+
const root = path.join(
|
|
59
|
+
home,
|
|
60
|
+
"Library",
|
|
61
|
+
"Application Support",
|
|
62
|
+
"BraveSoftware",
|
|
63
|
+
"Brave-Browser",
|
|
64
|
+
);
|
|
65
|
+
return ["Default", ...Array.from({ length: 20 }, (_, index) => `Profile ${index + 1}`)]
|
|
66
|
+
.flatMap((profile) => [
|
|
67
|
+
path.join(root, profile, "Network", "Cookies"),
|
|
68
|
+
path.join(root, profile, "Cookies"),
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function codexCookiePaths(home = os.homedir()) {
|
|
73
|
+
const root = path.join(home, "Library", "Application Support", "Codex");
|
|
74
|
+
return [
|
|
75
|
+
path.join(root, "Default", "Partitions", "codex-browser-app", "Network", "Cookies"),
|
|
76
|
+
path.join(root, "Default", "Partitions", "codex-browser-app", "Cookies"),
|
|
77
|
+
path.join(root, "Default", "Cookies"),
|
|
78
|
+
path.join(root, "codex-browser-app", "Cookies"),
|
|
79
|
+
];
|
|
80
|
+
}
|
package/src/scheduler.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import {
|
|
6
|
+
APP_ID,
|
|
7
|
+
APP_LOGIN_APP_ID,
|
|
8
|
+
LOGIN_SYNC_APP_ID,
|
|
9
|
+
appSupportDir,
|
|
10
|
+
appLoginLaunchAgentPath,
|
|
11
|
+
launchAgentPath,
|
|
12
|
+
loginSyncLaunchAgentPath,
|
|
13
|
+
} from "./paths.js";
|
|
14
|
+
|
|
15
|
+
export function installSchedule({ hour, minute, cliPath, nodePath = process.execPath, home = os.homedir() }) {
|
|
16
|
+
const plist = launchAgentPath(home);
|
|
17
|
+
const support = appSupportDir(home);
|
|
18
|
+
fs.mkdirSync(path.dirname(plist), { recursive: true });
|
|
19
|
+
fs.mkdirSync(path.join(support, "logs"), { recursive: true, mode: 0o700 });
|
|
20
|
+
|
|
21
|
+
const content = `<?xml version="1.0" encoding="UTF-8"?>
|
|
22
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
23
|
+
<plist version="1.0">
|
|
24
|
+
<dict>
|
|
25
|
+
<key>Label</key><string>${APP_ID}</string>
|
|
26
|
+
<key>ProgramArguments</key>
|
|
27
|
+
<array>
|
|
28
|
+
<string>${xml(nodePath)}</string>
|
|
29
|
+
<string>${xml(cliPath)}</string>
|
|
30
|
+
<string>sync</string>
|
|
31
|
+
<string>--timeout</string>
|
|
32
|
+
<string>300</string>
|
|
33
|
+
</array>
|
|
34
|
+
<key>StartCalendarInterval</key>
|
|
35
|
+
<dict>
|
|
36
|
+
<key>Hour</key><integer>${hour}</integer>
|
|
37
|
+
<key>Minute</key><integer>${minute}</integer>
|
|
38
|
+
</dict>
|
|
39
|
+
<key>ProcessType</key><string>Background</string>
|
|
40
|
+
<key>StandardOutPath</key><string>${xml(path.join(support, "logs", "sync.log"))}</string>
|
|
41
|
+
<key>StandardErrorPath</key><string>${xml(path.join(support, "logs", "sync-error.log"))}</string>
|
|
42
|
+
</dict>
|
|
43
|
+
</plist>
|
|
44
|
+
`;
|
|
45
|
+
fs.writeFileSync(plist, content, { mode: 0o600 });
|
|
46
|
+
|
|
47
|
+
bootstrap(plist, "daily schedule");
|
|
48
|
+
return plist;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function installLoginSync({ cliPath, nodePath = process.execPath, home = os.homedir() }) {
|
|
52
|
+
const plist = loginSyncLaunchAgentPath(home);
|
|
53
|
+
const support = appSupportDir(home);
|
|
54
|
+
fs.mkdirSync(path.dirname(plist), { recursive: true });
|
|
55
|
+
fs.mkdirSync(path.join(support, "logs"), { recursive: true, mode: 0o700 });
|
|
56
|
+
fs.writeFileSync(plist, buildLoginSyncPlist({ cliPath, nodePath, support }), { mode: 0o600 });
|
|
57
|
+
bootstrap(plist, "login sync");
|
|
58
|
+
return plist;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function buildLoginSyncPlist({ cliPath, nodePath, support }) {
|
|
62
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
63
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
64
|
+
<plist version="1.0">
|
|
65
|
+
<dict>
|
|
66
|
+
<key>Label</key><string>${LOGIN_SYNC_APP_ID}</string>
|
|
67
|
+
<key>ProgramArguments</key>
|
|
68
|
+
<array>
|
|
69
|
+
<string>${xml(nodePath)}</string>
|
|
70
|
+
<string>${xml(cliPath)}</string>
|
|
71
|
+
<string>sync</string>
|
|
72
|
+
<string>--timeout</string>
|
|
73
|
+
<string>300</string>
|
|
74
|
+
</array>
|
|
75
|
+
<key>RunAtLoad</key><true/>
|
|
76
|
+
<key>ProcessType</key><string>Background</string>
|
|
77
|
+
<key>StandardOutPath</key><string>${xml(path.join(support, "logs", "login-sync.log"))}</string>
|
|
78
|
+
<key>StandardErrorPath</key><string>${xml(path.join(support, "logs", "login-sync-error.log"))}</string>
|
|
79
|
+
</dict>
|
|
80
|
+
</plist>
|
|
81
|
+
`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function installAppLogin({ appPath, home = os.homedir(), bootstrapNow = true }) {
|
|
85
|
+
const plist = appLoginLaunchAgentPath(home);
|
|
86
|
+
fs.mkdirSync(path.dirname(plist), { recursive: true });
|
|
87
|
+
fs.writeFileSync(plist, buildAppLoginPlist({ appPath }), { mode: 0o600 });
|
|
88
|
+
if (bootstrapNow) bootstrap(plist, "app login launch");
|
|
89
|
+
return plist;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function buildAppLoginPlist({ appPath }) {
|
|
93
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
94
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
95
|
+
<plist version="1.0">
|
|
96
|
+
<dict>
|
|
97
|
+
<key>Label</key><string>${APP_LOGIN_APP_ID}</string>
|
|
98
|
+
<key>ProgramArguments</key>
|
|
99
|
+
<array>
|
|
100
|
+
<string>/usr/bin/open</string>
|
|
101
|
+
<string>${xml(appPath)}</string>
|
|
102
|
+
</array>
|
|
103
|
+
<key>RunAtLoad</key><true/>
|
|
104
|
+
<key>ProcessType</key><string>Interactive</string>
|
|
105
|
+
</dict>
|
|
106
|
+
</plist>
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function removeSchedule(home = os.homedir()) {
|
|
111
|
+
const plist = launchAgentPath(home);
|
|
112
|
+
if (!fs.existsSync(plist)) return false;
|
|
113
|
+
spawnSync("launchctl", ["bootout", `gui/${process.getuid()}`, plist], { stdio: "ignore" });
|
|
114
|
+
fs.unlinkSync(plist);
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function removeLoginSync(home = os.homedir()) {
|
|
119
|
+
const plist = loginSyncLaunchAgentPath(home);
|
|
120
|
+
if (!fs.existsSync(plist)) return false;
|
|
121
|
+
spawnSync("launchctl", ["bootout", `gui/${process.getuid()}`, plist], { stdio: "ignore" });
|
|
122
|
+
fs.unlinkSync(plist);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function removeAppLogin(home = os.homedir()) {
|
|
127
|
+
const plist = appLoginLaunchAgentPath(home);
|
|
128
|
+
if (!fs.existsSync(plist)) return false;
|
|
129
|
+
spawnSync("launchctl", ["bootout", `gui/${process.getuid()}`, plist], { stdio: "ignore" });
|
|
130
|
+
fs.unlinkSync(plist);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function bootstrap(plist, description) {
|
|
135
|
+
const domain = `gui/${process.getuid()}`;
|
|
136
|
+
spawnSync("launchctl", ["bootout", domain, plist], { stdio: "ignore" });
|
|
137
|
+
const result = spawnSync("launchctl", ["bootstrap", domain, plist], { encoding: "utf8" });
|
|
138
|
+
if (result.status !== 0) {
|
|
139
|
+
throw new Error(result.stderr.trim() || `launchctl could not install the ${description}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function xml(value) {
|
|
144
|
+
return value
|
|
145
|
+
.replaceAll("&", "&")
|
|
146
|
+
.replaceAll("<", "<")
|
|
147
|
+
.replaceAll(">", ">")
|
|
148
|
+
.replaceAll('"', """)
|
|
149
|
+
.replaceAll("'", "'");
|
|
150
|
+
}
|
package/src/updater.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import {
|
|
7
|
+
appSupportDir,
|
|
8
|
+
installedAppPath,
|
|
9
|
+
systemInstalledAppPath,
|
|
10
|
+
userInstalledAppPath,
|
|
11
|
+
} from "./paths.js";
|
|
12
|
+
import { installAppLogin } from "./scheduler.js";
|
|
13
|
+
|
|
14
|
+
export function startDetachedUpdate({ version, appPath, appPID, home = os.homedir() }) {
|
|
15
|
+
validateUpdateRequest({ version, appPath, appPID, home });
|
|
16
|
+
const support = appSupportDir(home);
|
|
17
|
+
const logs = path.join(support, "logs");
|
|
18
|
+
fs.mkdirSync(logs, { recursive: true, mode: 0o700 });
|
|
19
|
+
const logPath = path.join(logs, "update.log");
|
|
20
|
+
const log = fs.openSync(logPath, "a", 0o600);
|
|
21
|
+
const cliPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "bin", "brave-codex-cookie-sync.js");
|
|
22
|
+
const child = spawn(process.execPath, [
|
|
23
|
+
cliPath,
|
|
24
|
+
"perform-update",
|
|
25
|
+
"--version", version,
|
|
26
|
+
"--app-path", appPath,
|
|
27
|
+
"--app-pid", String(appPID),
|
|
28
|
+
], {
|
|
29
|
+
detached: true,
|
|
30
|
+
stdio: ["ignore", log, log],
|
|
31
|
+
env: process.env,
|
|
32
|
+
});
|
|
33
|
+
child.unref();
|
|
34
|
+
fs.closeSync(log);
|
|
35
|
+
return { workerPID: child.pid, logPath };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function performUpdate({ version, appPath, appPID, home = os.homedir() }) {
|
|
39
|
+
const destination = validateUpdateRequest({ version, appPath, appPID, home });
|
|
40
|
+
const resultPath = path.join(appSupportDir(home), "update-result.json");
|
|
41
|
+
const currentUserApp = installedAppPath(home);
|
|
42
|
+
try {
|
|
43
|
+
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);
|
|
60
|
+
installAppLogin({ appPath: destination, bootstrapNow: false });
|
|
61
|
+
writeUpdateResult(resultPath, { status: "success", version });
|
|
62
|
+
relaunch(destination);
|
|
63
|
+
return { destination, version };
|
|
64
|
+
} catch (error) {
|
|
65
|
+
writeUpdateResult(resultPath, { status: "failed", version, message: error.message });
|
|
66
|
+
if (fs.existsSync(destination)) relaunch(destination);
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function replaceAppContents(source, destination) {
|
|
72
|
+
if (!fs.existsSync(destination)) throw new Error(`Installed app not found at ${destination}`);
|
|
73
|
+
const sourceContents = path.join(source, "Contents");
|
|
74
|
+
const destinationContents = path.join(destination, "Contents");
|
|
75
|
+
const stagedContents = path.join(destination, ".Contents.update");
|
|
76
|
+
const previousContents = path.join(destination, ".Contents.previous");
|
|
77
|
+
fs.rmSync(stagedContents, { recursive: true, force: true });
|
|
78
|
+
fs.rmSync(previousContents, { recursive: true, force: true });
|
|
79
|
+
fs.cpSync(sourceContents, stagedContents, { recursive: true, force: true });
|
|
80
|
+
fs.renameSync(destinationContents, previousContents);
|
|
81
|
+
try {
|
|
82
|
+
fs.renameSync(stagedContents, destinationContents);
|
|
83
|
+
fs.rmSync(previousContents, { recursive: true, force: true });
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (!fs.existsSync(destinationContents) && fs.existsSync(previousContents)) {
|
|
86
|
+
fs.renameSync(previousContents, destinationContents);
|
|
87
|
+
}
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
spawnSync("/usr/bin/xattr", ["-dr", "com.apple.quarantine", destination], { stdio: "ignore" });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function validateUpdateRequest({ version, appPath, appPID, home = os.homedir() }) {
|
|
94
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version ?? "")) {
|
|
95
|
+
throw new Error("Invalid update version");
|
|
96
|
+
}
|
|
97
|
+
if (!Number.isInteger(appPID) || appPID <= 1) throw new Error("Invalid app process ID");
|
|
98
|
+
const resolved = path.resolve(appPath ?? "");
|
|
99
|
+
const allowed = new Set([
|
|
100
|
+
path.resolve(systemInstalledAppPath()),
|
|
101
|
+
path.resolve(userInstalledAppPath(home)),
|
|
102
|
+
]);
|
|
103
|
+
if (!allowed.has(resolved)) throw new Error(`Refusing to replace unexpected app path: ${resolved}`);
|
|
104
|
+
return resolved;
|
|
105
|
+
}
|
|
106
|
+
|
|
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;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function waitForProcessToExit(pid, timeoutMilliseconds) {
|
|
117
|
+
const deadline = Date.now() + timeoutMilliseconds;
|
|
118
|
+
while (processExists(pid)) {
|
|
119
|
+
if (Date.now() >= deadline) throw new Error("The previous app did not quit in time");
|
|
120
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function processExists(pid) {
|
|
125
|
+
try {
|
|
126
|
+
process.kill(pid, 0);
|
|
127
|
+
return true;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
return error.code !== "ESRCH";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function relaunch(appPath) {
|
|
134
|
+
const result = spawnSync("/usr/bin/open", ["-g", appPath], { encoding: "utf8" });
|
|
135
|
+
if (result.status !== 0) throw new Error(result.stderr.trim() || "The updated app could not be relaunched");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function writeUpdateResult(target, value) {
|
|
139
|
+
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
140
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
141
|
+
fs.writeFileSync(temporary, `${JSON.stringify({ ...value, date: new Date().toISOString() })}\n`, { mode: 0o600 });
|
|
142
|
+
fs.renameSync(temporary, target);
|
|
143
|
+
fs.chmodSync(target, 0o600);
|
|
144
|
+
}
|