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 +39 -20
- package/extension-template/manifest.json +1 -1
- package/macos-app/Info.plist +3 -3
- package/macos-app/Sources/BraveCodexSyncApp/BraveCodexSyncApp.swift +8 -0
- package/macos-app/Sources/BraveCodexSyncApp/SyncModel.swift +58 -1
- package/package.json +14 -3
- package/src/cli.js +69 -2
- package/src/config.js +11 -10
- package/src/updater.js +89 -27
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
|
|
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
|
-
> **
|
|
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** —
|
|
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
|
|
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
|
-
###
|
|
87
|
+
### Download a DMG — recommended
|
|
82
88
|
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
188
|
-
git push origin
|
|
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.
|
|
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"],
|
package/macos-app/Info.plist
CHANGED
|
@@ -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.
|
|
14
|
-
<key>CFBundleVersion</key><string>
|
|
15
|
-
<key>LSMinimumSystemVersion</key><string>13.
|
|
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://
|
|
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.
|
|
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://
|
|
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
|
|
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.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
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) {
|