browser-cookie-bridge 1.0.0 → 1.1.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 CHANGED
@@ -9,9 +9,10 @@
9
9
  <p>Brave, Chrome, Edge, Arc, Vivaldi, Opera, and Comet · browser-to-browser transfer · import into ChatGPT Codex.</p>
10
10
 
11
11
  <p>
12
- <img src="https://img.shields.io/badge/macOS-13%2B-111111?logo=apple&logoColor=white" alt="macOS 13+" />
12
+ <img src="https://img.shields.io/badge/macOS-13.5%2B-111111?logo=apple&logoColor=white" alt="macOS 13.5+" />
13
13
  <img src="https://img.shields.io/badge/Swift-6-8E2735?logo=swift&logoColor=white" alt="Swift 6" />
14
14
  <img src="https://img.shields.io/badge/Node.js-22.5%2B-3C873A?logo=nodedotjs&logoColor=white" alt="Node.js 22.5+" />
15
+ <a href="https://www.npmjs.com/package/browser-cookie-bridge"><img src="https://img.shields.io/npm/v/browser-cookie-bridge?logo=npm&color=CB3837" alt="browser-cookie-bridge on npm" /></a>
15
16
  <img src="https://img.shields.io/badge/local--first-no%20cloud-C68B3C" alt="Local-first, no cloud" />
16
17
  <img src="https://img.shields.io/github/stars/apoorvdarshan/browser-cookie-bridge?logo=github&color=C68B3C" alt="GitHub stars" />
17
18
  <a href="https://www.producthunt.com/products/browser-cookie-bridge"><img src="https://img.shields.io/badge/Product%20Hunt-View%20launch-DA552F?logo=producthunt&logoColor=white" alt="Browser Cookie Bridge on Product Hunt" /></a>
@@ -20,6 +21,8 @@
20
21
 
21
22
  <p>
22
23
  <a href="#installation"><b>Install</b></a> ·
24
+ <a href="https://github.com/apoorvdarshan/browser-cookie-bridge/releases/latest">DMG</a> ·
25
+ <a href="https://www.npmjs.com/package/browser-cookie-bridge">npm</a> ·
23
26
  <a href="#how-it-works">How it works</a> ·
24
27
  <a href="#security--privacy">Security</a> ·
25
28
  <a href="CONTRIBUTING.md">Contribute</a> ·
@@ -28,7 +31,10 @@
28
31
  <a href="#support">Support</a>
29
32
  </p>
30
33
 
31
- <p><code>npm run build:app</code></p>
34
+ <p>
35
+ <a href="https://github.com/apoorvdarshan/browser-cookie-bridge/releases/latest/download/Browser-Cookie-Bridge-arm64.dmg"><b>Download for Apple silicon</b></a>
36
+ · <a href="https://github.com/apoorvdarshan/browser-cookie-bridge/releases/latest/download/Browser-Cookie-Bridge-x64.dmg">Download for Intel</a>
37
+ </p>
32
38
 
33
39
  <br />
34
40
 
@@ -38,7 +44,7 @@
38
44
 
39
45
  ---
40
46
 
41
- > **Status local beta.** The native app and transfer engine work locally; the first public npm release has not been published yet. Importing into ChatGPT Codex is an intentionally unsupported direct integration and requires Codex to be completely closed.
47
+ > **Version 1.1.0 adds self-contained DMG downloads.** Importing into ChatGPT Codex is an intentionally unsupported direct integration and requires Codex to be completely closed.
42
48
 
43
49
  ## Why Browser Cookie Bridge
44
50
 
@@ -54,7 +60,7 @@ Browser Cookie Bridge gives those local browser profiles a small, native control
54
60
  - 🕘 **Background automation** — sync when you sign in, at a fixed daily time, or whenever you choose.
55
61
  - ◉ **Native menu-bar app** — closing the window removes the Dock icon while the helper continues running.
56
62
  - 🧯 **Backup and rollback** — Codex's database is backed up, modified on a separate copy, integrity-checked, and restored if replacement fails.
57
- - ⬆️ **Built-in updates** — automatically checks npm release metadata and can install an update and relaunch the app.
63
+ - ⬆️ **Built-in updates** — checks for GitHub releases, verifies the DMG checksum, installs in place, and relaunches the app.
58
64
  - 🔒 **Local-first** — no account, analytics, cloud service, cookie logs, or remote relay.
59
65
 
60
66
  ## What it transfers
@@ -71,27 +77,23 @@ Some websites bind sessions to a specific device or browser and may ask you to s
71
77
 
72
78
  ## Requirements
73
79
 
74
- - **macOS 13 (Ventura)+**
75
- - **Node.js 22.5+**
76
- - **Xcode Command Line Tools** — install with `xcode-select --install`
80
+ - **macOS 13.5+**
77
81
  - A supported Chromium browser, or ChatGPT Codex as the destination
78
82
 
83
+ The DMG is self-contained. Node.js 22.5+ and Xcode Command Line Tools are required only for npm or source installation.
84
+
79
85
  ## Installation
80
86
 
81
- ### From source
87
+ ### Download a DMG — recommended
82
88
 
83
- ```bash
84
- git clone https://github.com/apoorvdarshan/browser-cookie-bridge.git
85
- cd browser-cookie-bridge
86
- npm test
87
- npm run build:app
88
- ```
89
+ - **[Apple silicon DMG](https://github.com/apoorvdarshan/browser-cookie-bridge/releases/latest/download/Browser-Cookie-Bridge-arm64.dmg)** — M1, M2, M3, M4, and newer
90
+ - **[Intel DMG](https://github.com/apoorvdarshan/browser-cookie-bridge/releases/latest/download/Browser-Cookie-Bridge-x64.dmg)** — Intel-based Macs
89
91
 
90
- The final command compiles the native SwiftUI app, enables **Open at login**, **Sync at login**, and the **menu-bar helper**, then launches it. A first install uses `~/Applications/Browser Cookie Bridge.app` without requesting administrator access. If an existing `/Applications/Browser Cookie Bridge.app` is present, later builds and updates keep using that system Applications copy instead of creating a duplicate. Daily sync stays off until you enable it.
92
+ Open the DMG, drag **Browser Cookie Bridge** onto **Applications**, then open it from Applications. The current public build is checksum-verified and ad-hoc signed but not Apple-notarized, so macOS may require **Control-click Open** on first launch. It does not require Node.js, Xcode, Terminal, or an administrator password.
91
93
 
92
94
  ### Via npm
93
95
 
94
- The package will be installable after the first tagged release:
96
+ Run the published package directly:
95
97
 
96
98
  ```bash
97
99
  npx browser-cookie-bridge install-app
@@ -99,6 +101,21 @@ npx browser-cookie-bridge install-app
99
101
 
100
102
  No administrator password is needed. The app is built from source on your Mac and installed in your user Applications folder unless an existing writable system Applications copy is being updated.
101
103
 
104
+ Both installation methods use the same bundle identifier and the same settings under `~/Library/Application Support/BraveCodexCookieSync`. Installing the DMG after npm does not create a separate product identity: the system Applications copy becomes canonical, and a stale matching user Applications copy is moved to the Trash on launch. Future updates replace that same app in place.
105
+
106
+ **[View `browser-cookie-bridge` on npm →](https://www.npmjs.com/package/browser-cookie-bridge)**
107
+
108
+ ### From source
109
+
110
+ ```bash
111
+ git clone https://github.com/apoorvdarshan/browser-cookie-bridge.git
112
+ cd browser-cookie-bridge
113
+ npm test
114
+ npm run build:app
115
+ ```
116
+
117
+ The final command compiles the native SwiftUI app, enables **Open at login**, **Sync at login**, and the **menu-bar helper**, then launches it. A first install uses `~/Applications/Browser Cookie Bridge.app` without requesting administrator access. If an existing `/Applications/Browser Cookie Bridge.app` is present, later builds and updates keep using that system Applications copy instead of creating a duplicate. Daily sync stays off until you enable it.
118
+
102
119
  ## Setup
103
120
 
104
121
  ### Browser → ChatGPT Codex
@@ -135,7 +152,7 @@ Generated extensions live under `~/Library/Application Support/BraveCodexCookieS
135
152
  | **Sync at login** | Runs once whenever you sign in; on by default |
136
153
  | **Open at login** | Starts the background app after macOS login; on by default |
137
154
  | **Show in menu bar** | Keeps sync, status, updates, and support actions close at hand |
138
- | **Check for updates** | Finds a newer npm release and offers install + relaunch |
155
+ | **Check for updates** | Finds a newer GitHub release, verifies its DMG, and offers install + relaunch |
139
156
 
140
157
  Automation always uses the source, destination, and data choices currently saved in the app. A scheduled Codex sync safely exits without making changes when Codex is open.
141
158
 
@@ -180,12 +197,12 @@ Found a vulnerability? Read [SECURITY.md](SECURITY.md) and report it privately.
180
197
 
181
198
  ## Releases
182
199
 
183
- Pushing a semantic version tag runs tests, validates that package and app versions match, publishes to npm, and creates a GitHub Release with the tarball attached.
200
+ Pushing a semantic version tag runs tests, validates that package and app versions match, builds separate Apple-silicon and Intel DMGs, publishes to npm, and creates a GitHub Release with the tarball, DMGs, and SHA-256 files attached.
184
201
 
185
202
  ```bash
186
203
  npm run release:check
187
- git tag v1.0.0
188
- git push origin v1.0.0
204
+ git tag vX.Y.Z
205
+ git push origin vX.Y.Z
189
206
  ```
190
207
 
191
208
  A normal branch push does not publish anything.
@@ -199,6 +216,8 @@ Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for local setu
199
216
  If Browser Cookie Bridge is useful to you:
200
217
 
201
218
  - ⭐ **Star** the repository
219
+ - ⬇️ **[Download the latest DMG](https://github.com/apoorvdarshan/browser-cookie-bridge/releases/latest)**
220
+ - 📦 **[Install from npm](https://www.npmjs.com/package/browser-cookie-bridge)**
202
221
  - 🐛 **[Report a bug](https://github.com/apoorvdarshan/browser-cookie-bridge/issues/new?template=bug_report.yml)** — never include cookie values or tokens
203
222
  - ☕ **[Support on Ko-fi](https://ko-fi.com/apoorvdarshan)**
204
223
  - 𝕏 **Follow [@apoorvdarshan](https://x.com/apoorvdarshan)**
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Browser Cookie Bridge",
4
- "version": "1.0.0",
4
+ "version": "1.1.0",
5
5
  "description": "Transfers selected browser data locally between supported browsers and into ChatGPT Codex.",
6
6
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5o6lDkXzU/23Jm2QRk4mfuxNnr3VlEIjRHcZr93Gdo7PI7asHJQhigaMGT6UOz0lkaTn1F+TGIPlsJo9GNBSsxbCfxWoBSXcFQFrPolY5LEfKaMtZQQ6eb4UAnp+bbUwb1den4On0piWU47Yzl6i0Jcdg4EG5H1kCSF/6nJsbKgF7OTaR/3drULu40yRvflzSXRhly3UDO7c5mgHJsaMwVV+VILsxbcPOoRINjuo3zDC737HFs67dpf38xF3tjSVL0HURAWKKobMvfABiDfVEm5O0Lvf+57d1Ib12QLT2qtVXNDy3iYwX+w2S0wH2vIMsfvoFPlRdmeqft70kZOqXwIDAQAB",
7
7
  "permissions": ["alarms", "cookies", "history"],
@@ -10,9 +10,9 @@
10
10
  <key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
11
11
  <key>CFBundleName</key><string>Browser Cookie Bridge</string>
12
12
  <key>CFBundlePackageType</key><string>APPL</string>
13
- <key>CFBundleShortVersionString</key><string>1.0.0</string>
14
- <key>CFBundleVersion</key><string>24</string>
15
- <key>LSMinimumSystemVersion</key><string>13.0</string>
13
+ <key>CFBundleShortVersionString</key><string>1.1.0</string>
14
+ <key>CFBundleVersion</key><string>25</string>
15
+ <key>LSMinimumSystemVersion</key><string>13.5</string>
16
16
  <key>NSHighResolutionCapable</key><true/>
17
17
  </dict>
18
18
  </plist>
@@ -3,6 +3,8 @@ import SwiftUI
3
3
 
4
4
  private enum ProjectLinks {
5
5
  static let repository = URL(string: "https://github.com/apoorvdarshan/browser-cookie-bridge")!
6
+ static let downloads = URL(string: "https://github.com/apoorvdarshan/browser-cookie-bridge/releases/latest")!
7
+ static let npm = URL(string: "https://www.npmjs.com/package/browser-cookie-bridge")!
6
8
  static let issues = URL(string: "https://github.com/apoorvdarshan/browser-cookie-bridge/issues/new?template=bug_report.yml")!
7
9
  static let license = URL(string: "https://github.com/apoorvdarshan/browser-cookie-bridge/blob/main/LICENSE")!
8
10
  static let koFi = URL(string: "https://ko-fi.com/apoorvdarshan")!
@@ -168,6 +170,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
168
170
  private func addProjectLinks(to menu: NSMenu) {
169
171
  menu.addItem(.separator())
170
172
  addLink("Open-source repository", url: ProjectLinks.repository, to: menu)
173
+ addLink("Download latest DMG", url: ProjectLinks.downloads, to: menu)
174
+ addLink("View on npm", url: ProjectLinks.npm, to: menu)
171
175
  addLink("Report a bug…", url: ProjectLinks.issues, to: menu)
172
176
  addLink("MIT license", url: ProjectLinks.license, to: menu)
173
177
  menu.addItem(.separator())
@@ -503,6 +507,10 @@ struct PreferencesPanel: View {
503
507
  SectionLabel(title: "Project & support", detail: "Open source", separated: true)
504
508
  ProjectLinkRow(icon: "chevron.left.forwardslash.chevron.right", title: "Open-source repository", detail: "View the code on GitHub", url: ProjectLinks.repository)
505
509
  RowDivider()
510
+ ProjectLinkRow(icon: "arrow.down.circle", title: "Download latest DMG", detail: "Apple silicon and Intel builds", url: ProjectLinks.downloads)
511
+ RowDivider()
512
+ ProjectLinkRow(icon: "shippingbox", title: "View on npm", detail: "Install the latest public release", url: ProjectLinks.npm)
513
+ RowDivider()
506
514
  ProjectLinkRow(icon: "ladybug", title: "Report a bug", detail: "Open a GitHub issue", url: ProjectLinks.issues)
507
515
  RowDivider()
508
516
  ProjectLinkRow(icon: "doc.text", title: "MIT license", detail: "Read the open-source license", url: ProjectLinks.license)
@@ -116,6 +116,7 @@ final class SyncModel: ObservableObject {
116
116
  }
117
117
 
118
118
  init() {
119
+ bootstrapBundledRuntimeIfNeeded()
119
120
  let calendar = Calendar.current
120
121
  scheduleTime = calendar.date(bySettingHour: 9, minute: 0, second: 0, of: Date()) ?? Date()
121
122
  updateCodexRunningStatus()
@@ -130,6 +131,51 @@ final class SyncModel: ObservableObject {
130
131
  }
131
132
  }
132
133
 
134
+ private func bootstrapBundledRuntimeIfNeeded() {
135
+ guard let resources = Bundle.main.resourceURL else { return }
136
+ let bundledRuntime = resources.appending(path: "runtime")
137
+ let bundledNode = bundledRuntime.appending(path: "node/bin/node")
138
+ let bundledCLI = bundledRuntime.appending(path: "bin/brave-codex-cookie-sync.js")
139
+ guard FileManager.default.isExecutableFile(atPath: bundledNode.path),
140
+ FileManager.default.fileExists(atPath: bundledCLI.path) else { return }
141
+
142
+ if Bundle.main.bundlePath.hasPrefix("/Volumes/") {
143
+ state = .warning
144
+ primaryStatus = "Move the app to Applications"
145
+ secondaryStatus = "Drag Browser Cookie Bridge onto Applications in the DMG window, then open it there"
146
+ return
147
+ }
148
+
149
+ let process = Process()
150
+ let output = Pipe()
151
+ process.executableURL = bundledNode
152
+ process.arguments = [
153
+ bundledCLI.path,
154
+ "bootstrap-bundled",
155
+ "--app-path", Bundle.main.bundlePath,
156
+ ]
157
+ process.standardOutput = output
158
+ process.standardError = output
159
+
160
+ do {
161
+ try process.run()
162
+ process.waitUntilExit()
163
+ guard process.terminationStatus != 0 else { return }
164
+ let data = output.fileHandleForReading.readDataToEndOfFile()
165
+ let message = String(decoding: data, as: UTF8.self)
166
+ .split(separator: "\n")
167
+ .map(String.init)
168
+ .last(where: { !$0.isEmpty })
169
+ state = .error
170
+ primaryStatus = "Could not prepare the local runtime"
171
+ secondaryStatus = message ?? "Move the app to Applications and reopen it"
172
+ } catch {
173
+ state = .error
174
+ primaryStatus = "Could not prepare the local runtime"
175
+ secondaryStatus = error.localizedDescription
176
+ }
177
+ }
178
+
133
179
  func refresh() {
134
180
  dailyEnabled = FileManager.default.fileExists(atPath: launchAgent.path)
135
181
  loginSyncEnabled = FileManager.default.fileExists(atPath: loginSyncAgent.path)
@@ -204,11 +250,12 @@ final class SyncModel: ObservableObject {
204
250
 
205
251
  func checkForUpdates(showAlert: Bool = false) {
206
252
  guard !isCheckingForUpdates, !isInstallingUpdate else { return }
207
- guard let url = URL(string: "https://registry.npmjs.org/browser-cookie-bridge/latest") else { return }
253
+ guard let url = URL(string: "https://api.github.com/repos/apoorvdarshan/browser-cookie-bridge/releases/latest") else { return }
208
254
  isCheckingForUpdates = true
209
255
  postUpdateState()
210
256
  var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 15)
211
257
  request.setValue("Browser-Cookie-Bridge/\(currentVersion)", forHTTPHeaderField: "User-Agent")
258
+ request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept")
212
259
  URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
213
260
  Task { @MainActor in
214
261
  guard let self else { return }
@@ -575,6 +622,16 @@ final class SyncModel: ObservableObject {
575
622
 
576
623
  private struct PackageRelease: Decodable {
577
624
  let version: String
625
+
626
+ private enum CodingKeys: String, CodingKey {
627
+ case tagName = "tag_name"
628
+ }
629
+
630
+ init(from decoder: Decoder) throws {
631
+ let values = try decoder.container(keyedBy: CodingKeys.self)
632
+ let tag = try values.decode(String.self, forKey: .tagName)
633
+ version = tag.hasPrefix("v") ? String(tag.dropFirst()) : tag
634
+ }
578
635
  }
579
636
 
580
637
  private struct UpdateResult: Decodable {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-cookie-bridge",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Private local cookie and session transfer for macOS",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,9 +8,10 @@
8
8
  },
9
9
  "scripts": {
10
10
  "test": "node --test test/*.test.js",
11
- "check": "node --check bin/brave-codex-cookie-sync.js && node --check src/app-installer.js && node --check src/broker.js && node --check src/chromium-reader.js && node --check src/cli.js && node --check src/codex-direct-import.js && node --check src/config.js && node --check src/paths.js && node --check src/scheduler.js && node --check src/updater.js && node --check extension-template/background.js && node --check web/server.js && node --check web/script.js",
11
+ "check": "node --check bin/brave-codex-cookie-sync.js && node --check src/app-installer.js && node --check src/broker.js && node --check src/chromium-reader.js && node --check src/cli.js && node --check src/codex-direct-import.js && node --check src/config.js && node --check src/paths.js && node --check src/scheduler.js && node --check src/updater.js && node --check scripts/build-dmg.js && node --check extension-template/background.js && node --check web/server.js && node --check web/script.js",
12
12
  "release:check": "node scripts/check-release-version.js",
13
13
  "build:app": "node bin/brave-codex-cookie-sync.js install-app --no-open",
14
+ "build:dmg": "node scripts/build-dmg.js",
14
15
  "web": "node web/server.js",
15
16
  "web:deploy": "wrangler deploy --config web/wrangler.jsonc"
16
17
  },
@@ -22,7 +23,7 @@
22
23
  "type": "git",
23
24
  "url": "git+https://github.com/apoorvdarshan/browser-cookie-bridge.git"
24
25
  },
25
- "homepage": "https://github.com/apoorvdarshan/browser-cookie-bridge#readme",
26
+ "homepage": "https://cookiebridge.apoorvdarshan.com/",
26
27
  "bugs": {
27
28
  "url": "https://github.com/apoorvdarshan/browser-cookie-bridge/issues"
28
29
  },
@@ -30,6 +31,16 @@
30
31
  "access": "public",
31
32
  "registry": "https://registry.npmjs.org/"
32
33
  },
34
+ "keywords": [
35
+ "browser",
36
+ "cookies",
37
+ "sessions",
38
+ "macos",
39
+ "chromium",
40
+ "brave",
41
+ "codex"
42
+ ],
43
+ "funding": "https://ko-fi.com/apoorvdarshan",
33
44
  "files": [
34
45
  "bin",
35
46
  "src",
package/src/cli.js CHANGED
@@ -19,6 +19,9 @@ import {
19
19
  installedExtensionDir,
20
20
  launchAgentPath,
21
21
  loginSyncLaunchAgentPath,
22
+ systemInstalledAppPath,
23
+ userInstalledAppPath,
24
+ APP_ID,
22
25
  SOURCE_BROWSERS,
23
26
  TARGET_BROWSERS,
24
27
  } from "./paths.js";
@@ -39,6 +42,7 @@ Local cookie and session transfer between Chromium browsers and into ChatGPT Cod
39
42
  Commands:
40
43
  setup [--hour 9] [--minute 0] [--no-schedule]
41
44
  install-app [--no-open]
45
+ bootstrap-bundled --app-path /Applications/Browser Cookie Bridge.app
42
46
  preferences --source brave --target codex --cookies on --history off --menu-bar on --auto-check-updates on
43
47
  sync [--timeout 300]
44
48
  doctor
@@ -59,6 +63,8 @@ export async function main(argv) {
59
63
  return sync(args);
60
64
  case "install-app":
61
65
  return installDesktopApp(args);
66
+ case "bootstrap-bundled":
67
+ return bootstrapBundledApp(args);
62
68
  case "preferences":
63
69
  return preferences(args);
64
70
  case "install-update":
@@ -97,9 +103,9 @@ function installUpdate(args) {
97
103
  console.log(`Update worker started: ${result.workerPID}`);
98
104
  }
99
105
 
100
- function performUpdateWorker(args) {
106
+ async function performUpdateWorker(args) {
101
107
  assertMacOS();
102
- const result = performUpdate({
108
+ const result = await performUpdate({
103
109
  version: stringFlag(args, "--version", ""),
104
110
  appPath: stringFlag(args, "--app-path", ""),
105
111
  appPID: integerFlag(args, "--app-pid", 0, 2, Number.MAX_SAFE_INTEGER),
@@ -150,6 +156,67 @@ function installDesktopApp(args) {
150
156
  : "The app is available in your user Applications folder and Spotlight.");
151
157
  }
152
158
 
159
+ function bootstrapBundledApp(args) {
160
+ assertMacOS();
161
+ const appPath = path.resolve(stringFlag(args, "--app-path", ""));
162
+ if (!appPath.endsWith(".app") || !fs.existsSync(appPath)) {
163
+ throw new Error("--app-path must point to the running Browser Cookie Bridge app");
164
+ }
165
+
166
+ const firstInstall = !fs.existsSync(configPath());
167
+ const runtime = installRuntime();
168
+ const existing = firstInstall ? null : readConfig();
169
+ const config = installConfig({
170
+ hour: existing?.schedule?.hour ?? 9,
171
+ minute: existing?.schedule?.minute ?? 0,
172
+ nodePath: process.execPath,
173
+ });
174
+
175
+ if (config.ui.openAtLogin) {
176
+ installAppLogin({ appPath, bootstrapNow: false });
177
+ } else {
178
+ removeAppLogin();
179
+ }
180
+
181
+ const bundledCLI = path.join(runtime, "bin", "brave-codex-cookie-sync.js");
182
+ if (firstInstall || fs.existsSync(loginSyncLaunchAgentPath())) {
183
+ installLoginSync({
184
+ cliPath: bundledCLI,
185
+ nodePath: process.execPath,
186
+ });
187
+ }
188
+ if (fs.existsSync(launchAgentPath())) {
189
+ installSchedule({
190
+ hour: config.schedule.hour,
191
+ minute: config.schedule.minute,
192
+ cliPath: bundledCLI,
193
+ nodePath: process.execPath,
194
+ });
195
+ }
196
+
197
+ archiveDuplicateUserApp(appPath);
198
+
199
+ console.log(`Bundled runtime ready: ${runtime}`);
200
+ }
201
+
202
+ function archiveDuplicateUserApp(currentAppPath) {
203
+ if (path.resolve(currentAppPath) !== path.resolve(systemInstalledAppPath())) return;
204
+ const duplicate = userInstalledAppPath();
205
+ if (!fs.existsSync(duplicate)) return;
206
+ const infoPath = path.join(duplicate, "Contents", "Info.plist");
207
+ const info = fs.existsSync(infoPath) ? fs.readFileSync(infoPath, "utf8") : "";
208
+ if (!info.includes(`<string>${APP_ID}</string>`)) return;
209
+
210
+ const trash = path.join(os.homedir(), ".Trash");
211
+ fs.mkdirSync(trash, { recursive: true, mode: 0o700 });
212
+ let archived = path.join(trash, "Browser Cookie Bridge previous installation.app");
213
+ for (let suffix = 2; fs.existsSync(archived); suffix += 1) {
214
+ archived = path.join(trash, `Browser Cookie Bridge previous installation ${suffix}.app`);
215
+ }
216
+ fs.renameSync(duplicate, archived);
217
+ console.log(`Archived duplicate app: ${archived}`);
218
+ }
219
+
153
220
  function setup(args) {
154
221
  assertMacOS();
155
222
  const hour = integerFlag(args, "--hour", 9, 0, 23);
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 });
@@ -38,7 +38,7 @@ export function installConfig({ home, hour = 9, minute = 0 }) {
38
38
  version: 1,
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: {
@@ -87,8 +87,7 @@ export function updatePreferences({ home, cookies, history, sourceBrowser, targe
87
87
  return config;
88
88
  }
89
89
 
90
- export function installRuntime(home) {
91
- const source = projectRoot();
90
+ export function installRuntime(home, source = projectRoot()) {
92
91
  const target = path.join(path.dirname(configPath(home)), "runtime");
93
92
  if (path.resolve(source) === path.resolve(target)) return target;
94
93
 
@@ -100,12 +99,14 @@ export function installRuntime(home) {
100
99
  const appSource = path.join(source, "macos-app");
101
100
  const appTarget = path.join(target, "macos-app");
102
101
  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));
102
+ if (fs.existsSync(appSource)) {
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
+ }
109
110
  }
110
111
  for (const name of ["package.json", "README.md", "LICENSE"]) {
111
112
  fs.copyFileSync(path.join(source, name), path.join(target, name));
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) {